diff --git a/openspec/changes/chain-restructuring/.openspec.yaml b/openspec/changes/chain-restructuring/.openspec.yaml new file mode 100644 index 0000000..9696e00 --- /dev/null +++ b/openspec/changes/chain-restructuring/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-09-03 diff --git a/openspec/changes/chain-restructuring/design.md b/openspec/changes/chain-restructuring/design.md new file mode 100644 index 0000000..c3e845f --- /dev/null +++ b/openspec/changes/chain-restructuring/design.md @@ -0,0 +1,115 @@ +## Context + +The current implementation lets fully-allowed chains pass without interruption (`resolveChain` returns `allow` when every segment matches an allow rule; enforced by test "all segments allowed — chain let through"). Not-allowed multi-step commands surface to the human as an unreadable one-liner in the permission dialog. The README and the base design's Goals bullet still claim "multi-segment chains trigger ask (defense-in-depth)" — stale relative to implementation; to be corrected in docs. + +Prompt-level fixes (AGENTS.md instructions like "one command per tool call") are soft: probabilistic adherence, no verification, degradation in long sessions. They reduce frequency but cannot guarantee reviewability. + +**Review decision (recorded):** restructuring applies only to commands that are *not allowed* — the aim is human readability of commands a person must review. Allowed chains pass untouched. + +API facts verified against `@opencode-ai/plugin@1.18.6` types and the opencode monorepo source: + +- `permission.ask` output is `{ status: "ask" | "deny" | "allow" }` only — no reason/message field exists, so "deny with explanation" is impossible through that hook. +- Per repo search and issue anomalyco/opencode#19469, the `permission.ask` hook is not actually triggered by the permission engine in current opencode source — a separate reliability concern for this plugin's deny path, out of scope here but recorded. +- **`tool.execute.before` can block with a message by throwing** — thrown errors become tool results with `resultType: "error"`, and the error text reaches the model (`packages/core/src/session/runner/to-llm-message.ts:55-67`; official docs example: `throw new Error("Do not read .env files")`). + +## Goals / Non-Goals + +**Goals:** +- Deterministically reject unreadable one-liners that would otherwise go to human review as blobs, with an error message that teaches the model the compliant form +- Keep allowed commands untouched — zero friction on the allow path +- Keep the plugin's verification guarantee intact after restructuring: multi-line re-issues are parsed per line (newlines are already segment separators) and each line is checked individually +- Plugin tuning in a dedicated JSONC file, disabled by default +- Fix the stale README claim about defense-in-depth asks + +**Non-Goals:** +- Not enforcing AGENTS.md instructions — the plugin cannot and should not verify prompt compliance +- Not restructuring `allow` or `deny` flows (allowed = allowed; deny = forbidden regardless of format) +- Not adding length-based or token-based metrics — segment count and nesting depth cover the unreadable-mess cases +- Not adding retry counters or rate limits for repeated violations +- Not hot-reloading the config file — changes require an opencode restart (same as the existing config hook) + +## Decisions + +1. **Separate config file: `opencode-bash-guard.jsonc` in the opencode config dirs** + + Locations, in increasing precedence: + - Global: `~/.config/opencode/opencode-bash-guard.jsonc` (or `$XDG_CONFIG_HOME/opencode/…`) + - Project: `/.opencode/opencode-bash-guard.jsonc` + + Files are JSONC (comments, trailing commas) parsed with `jsonc-parser`. Objects deep-merge, project wins over global; scalars override. Missing file at a location is not an error. **Invalid JSONC → warning + `restructure` treated as disabled; the plugin's core chain-guard behavior is unaffected** (config failure must not brick or expand enforcement). + + Rationale: this revises the original "no custom config files" stance for *plugin-internal tuning* only. The split is principled: **what** is allowed/asked/denied stays in `opencode.json` `permission`; **how the plugin behaves** (thresholds, feature switches) lives in the plugin's own file, where comments can document the trade-offs. File reads happen once at plugin init (`input.directory` gives the project root); changes require a restart. + +2. **Schema: `restructure` with nested fields, disabled by default** + + ```jsonc + { + // Reject complex one-liners and ask the agent to restructure them + "restructure": { + "enabled": false, // default false — zero behavior change + "max_segments": 3, // max commands in a single-line chain + "max_depth": 2 // max $()/backtick/meta-command nesting + } + } + ``` + + - `enabled: false` (default) or missing `restructure` section → feature off, no behavior change. + - `max_segments` / `max_depth` must be positive integers; invalid values are dropped with a warning and the default applies. Missing fields fall back to defaults. + +3. **Scope: ask-resolving chains only** + + Rejection fires when the chain resolves to `ask` AND limits are exceeded. Rationale (review decision): allowed actions should be allowed; the aim is readable human review of not-allowed multi-step commands. Consequences: + - `allow` → untouched, always. + - `deny` → existing deny flow (wrap + stored deny). Restructuring a forbidden action is meaningless — the format is not the problem. + - Parse errors → existing fail-closed deny, unaffected. + - `null` (no plugin opinion) → untouched. Under the documented prerequisite (`"*": "ask"`), every uncovered segment matches the catch-all, so any chain that would reach a human resolves to `ask` — "not allowed" ⇔ `ask` in practice. Without the catch-all the plugin has no opinion and does not intervene. + + Flow in `beforeExecute`: parse → resolve chain (existing) → if action is `ask` AND `restructure.enabled` AND limits exceeded → **throw** (replaces wrap+store for that call). Otherwise existing flows verbatim. + +4. **One-liner targeting: segment limit applies to single-line commands only** + + - Command contains no newline → both `max_segments` and `max_depth` checks apply. + - Command contains a newline (multi-line script) → **exempt from the segment limit**; depth check still applies. + + Rationale: the compliant form ("multi-line, one command per line") must never violate the segment limit, otherwise the retry loop can deadlock (a 5-step task re-issued as 5 lines would still exceed `max_segments: 3` and be rejected forever). Multi-line IS the readable form the feature asks for. Deep nesting remains unreadable in any shape, so `max_depth` applies to both forms. Known edge, accepted for v1: a multi-line script with long `&&`-chains inside individual lines passes the segment check (ask still gates uncovered segments; a per-line segment check is possible future refinement). + +5. **Rejection message must be actionable** + + Thrown error text (single source of truth, exported constant): + + ``` + [opencode-bash-guard] Complex one-liner rejected (4 chained commands, nesting depth 2). + Re-issue as separate bash tool calls, or as a multi-line script with one command + per line — each command is then permission-checked individually. + ``` + + The message contains the actual counts (so the model can self-correct) and both compliant forms. Throwing means the command never executes and no permission dialog appears — the model retries on its own. + +6. **Restructured output is still fully verified** + + A multi-line re-issue is parsed by the existing `parseChain` into per-line segments; a separate-calls re-issue produces single-segment commands. Both paths run through the normal permission evaluation. The steering loop therefore cannot create a bypass — it only changes formatting. + +7. **README correction + AGENTS.md snippet** + + - Remove/correct the "multi-segment chains trigger ask (defense-in-depth)" claims in README (and the example table row `git status && git log → ask`) to match implemented behavior: fully-allowed chains pass through. + - New "Readable commands" section: `opencode-bash-guard.jsonc` example, threshold semantics, and a recommended AGENTS.md snippet (soft layer that reduces rejection frequency): + + ```markdown + ## Bash command style + - Issue one command per tool call. For sequences, use separate bash calls. + - Never write chained one-liners (`a && b && c`). If rejected, split and retry. + ``` + +8. **Repeated violations: same rejection every time** + + No attempt counter in v1. A compliant re-issue always exists (multi-line), so the loop terminates on compliance; the model can also give up or ask the user. Counters/escalation deferred until observed to be a problem. + +## Risks / Trade-offs + +- **[Retry loops burn tokens]** Mitigated structurally: a compliant multi-line form never violates `max_segments`, so the loop cannot deadlock. Depth-gated rejections may still retry; the message carries exact counts. +- **[False positives on legitimate pipelines]** `cat a | grep b | wc -l` (3 segments) passes defaults; a single-line 4-stage pipeline gets rejected. Mitigation: thresholds are user-configurable in the JSONC file; document raising `max_segments`. +- **[Throw suppresses the dialog for rejected ask-chains]** Intended: the model retries first; the human then reviews a readable form. Users who prefer to review the raw blob can disable the feature. +- **[JSONC dependency + two-location merge]** Adds `jsonc-parser`; merge precedence (project over global) must be documented to avoid confusion. Invalid files fail safe (feature off, warning). +- **[Config read once at startup]** Same limitation as the existing `config` hook; restart to apply. +- **[Multi-line scripts with per-line chains pass the segment check]** Accepted edge for v1 (see decision 4); ask still gates uncovered segments; per-line refinement possible later. +- **[Related but separate: `permission.ask` may never fire in current opencode]** Issue anomalyco/opencode#19469 suggests the deny path of this plugin may not hard-block. Out of scope here; needs its own verification and possibly a fix change. diff --git a/openspec/changes/chain-restructuring/proposal.md b/openspec/changes/chain-restructuring/proposal.md new file mode 100644 index 0000000..a961c84 --- /dev/null +++ b/openspec/changes/chain-restructuring/proposal.md @@ -0,0 +1,32 @@ +## Why + +Complex one-liners (`a && b && c`, nested `$()`, `eval`/`sh -c` wrappers) are hard for humans to review. Today a not-allowed multi-step command surfaces to the human as an unreadable blob in the permission dialog, and the agent has no incentive to write readable commands. Prompt-level guidance (AGENTS.md) is a soft constraint with no verification loop. The right fix is deterministic: reject the not-allowed one-liner with an instructive error so the agent re-issues it in a reviewable form. + +**Allowed actions stay allowed** — restructuring never touches chains that pass permission checks. The feature targets exactly the commands a human will be asked to review. + +The enforcement mechanism is verified against opencode's plugin API: an error thrown in `tool.execute.before` is converted into a tool result with `resultType: "error"`, and the error text is sent back to the model (official docs pattern: `throw new Error("Do not read .env files")`). The `permission.ask` hook cannot serve this role — its output carries only `status` with no message field. + +## What Changes + +- **Separate plugin config file `opencode-bash-guard.jsonc`** — read from the opencode config dirs (global `~/.config/opencode/`, project `.opencode/`), JSONC format (comments allowed), deep-merged with project over global. Permission *actions* stay in `opencode.json`; plugin *behavior tuning* lives here +- **`restructure` section with nested fields** — `{ "enabled": false, "max_segments": 3, "max_depth": 2 }`; **disabled by default** → zero behavior change +- **Complexity detection** — segment count and substitution nesting depth from the existing `unbash`-based chain parse; meta-commands (`eval`, `sh -c`, `bash -c`, `zsh -c`) count as segments +- **Reject-with-guidance on ask-resolving chains only** — when enabled, limits exceeded, and the chain resolves to `ask` (not allowed → human review), the plugin throws in `tool.execute.before`; nothing executes, and the error text tells the model to re-issue as separate bash tool calls or a multi-line script with one command per line +- **One-liner targeting** — the segment limit applies to single-line commands; multi-line scripts are the compliant form and are exempt from the segment limit (still depth-checked), so a compliant re-issue always exists +- **README correction** — remove the stale "multi-segment chains trigger ask (defense-in-depth)" claim (implementation and spec say fully-allowed chains pass through); document the JSONC config and an AGENTS.md snippet (soft layer reducing rejection frequency) + +## Capabilities + +### New Capabilities +- `chain-restructuring`: Detect overly complex one-liner bash commands that would require human review and reject them with actionable guidance so the agent re-issues readable, individually-checkable commands + +### Modified Capabilities + +None (README behavioral correction is documentation, not a spec change; the base `opencode-bash-guard` change is not yet archived). + +## Impact + +- New optional config file `opencode-bash-guard.jsonc` — absent or `restructure.enabled: false` means zero behavior change +- New dependency: `jsonc-parser` (JSONC parsing with comments/trailing commas) +- Code: `src/chain.ts` (expose nesting depth), new config-loader module (file discovery, JSONC parse, merge, validation), `src/enforce.ts` (reject path in `beforeExecute`) +- Docs: README new section + behavior correction + AGENTS.md snippet diff --git a/openspec/changes/chain-restructuring/specs/chain-restructuring/spec.md b/openspec/changes/chain-restructuring/specs/chain-restructuring/spec.md new file mode 100644 index 0000000..ab8753b --- /dev/null +++ b/openspec/changes/chain-restructuring/specs/chain-restructuring/spec.md @@ -0,0 +1,150 @@ +## ADDED Requirements + +### Requirement: Load plugin config from opencode-bash-guard.jsonc + +The system SHALL load its own configuration from `opencode-bash-guard.jsonc` files located in the opencode config dirs — global (`~/.config/opencode/` or `$XDG_CONFIG_HOME/opencode/`) and project (`/.opencode/`) — parsed as JSONC (comments and trailing commas allowed). Objects SHALL deep-merge with project precedence over global. A missing file SHALL not be an error. Invalid JSONC SHALL produce a warning and disable the `restructure` feature while leaving the plugin's core chain-guard behavior unchanged. Config SHALL be read once at plugin init; changes require a restart. + +#### Scenario: No config file anywhere — feature off + +- **WHEN** neither the global nor the project `opencode-bash-guard.jsonc` exists +- **THEN** `restructure` is disabled and plugin behavior is identical to before this change + +#### Scenario: Project overrides global + +- **WHEN** global config has `"restructure": { "enabled": true, "max_segments": 3 }` and project config has `"restructure": { "max_segments": 5 }` +- **THEN** the effective config is `restructure` enabled with `max_segments: 5` and `max_depth` from the global/defaults + +#### Scenario: JSONC syntax accepted + +- **WHEN** a config file contains `//` comments, a trailing comma, and valid JSON structure +- **THEN** it parses successfully + +#### Scenario: Invalid JSONC fails safe + +- **WHEN** a config file contains malformed JSONC +- **THEN** a warning is emitted naming the file, `restructure` is treated as disabled, and the core chain-guard plugin behavior is unchanged + +### Requirement: Restructure schema and defaults + +The `restructure` section SHALL have nested fields: `enabled` (boolean, default `false`), `max_segments` (positive integer, default `3`), `max_depth` (positive integer, default `2`). When `enabled` is `false` or the section is absent, plugin behavior SHALL be identical to before this change. Invalid `max_segments`/`max_depth` values (non-numeric or < 1) SHALL be dropped with a warning and the default SHALL apply. + +#### Scenario: Disabled by default + +- **WHEN** the config file exists but has no `restructure` section, or `restructure.enabled` is `false` +- **THEN** no complexity rejection occurs for any command + +#### Scenario: Enabled with defaults + +- **WHEN** the config contains `"restructure": { "enabled": true }` +- **THEN** thresholds are `max_segments: 3` and `max_depth: 2` + +#### Scenario: Explicit thresholds honored + +- **WHEN** the config contains `"restructure": { "enabled": true, "max_segments": 5, "max_depth": 1 }` +- **THEN** the thresholds are `5` segments and depth `1` + +#### Scenario: Invalid threshold values dropped + +- **WHEN** the config contains `"restructure": { "enabled": true, "max_segments": 0, "max_depth": "many" }` +- **THEN** both invalid values are dropped with warnings and defaults (`3`, `2`) apply + +### Requirement: Detect command complexity + +The system SHALL compute, per command, the segment count (existing `parseChain` output, including meta-command bodies) and the maximum substitution nesting depth reached while walking `$()`, backticks, and meta-command string arguments. Limits are exceeded when a metric is strictly greater than its threshold. The segment-count limit SHALL apply to single-line commands only; a command containing a newline SHALL be exempt from the segment-count limit but still subject to the depth limit. + +#### Scenario: Single-line chain over segment threshold + +- **WHEN** command is `a && b && c && d` (4 segments, no newline), `max_segments: 3` +- **THEN** limits are exceeded + +#### Scenario: Single-line chain at threshold — not exceeded + +- **WHEN** command is `a && b && c` (3 segments, no newline), `max_segments: 3` +- **THEN** limits are not exceeded + +#### Scenario: Multi-line script exempt from segment limit + +- **WHEN** command is a 5-line script with one command per line, `max_segments: 3` +- **THEN** the segment limit is not exceeded (multi-line is the compliant form) + +#### Scenario: Nesting depth over threshold in any form + +- **WHEN** command is `echo $(echo $(whoami))` (depth 3) or a multi-line script containing a depth-3 substitution, `max_depth: 2` +- **THEN** limits are exceeded in both cases + +#### Scenario: Meta-command body counts + +- **WHEN** command is `bash -c "a && b && c && d"` (4 segments after recursive parse), `max_segments: 3` +- **THEN** limits are exceeded + +#### Scenario: Simple command passes + +- **WHEN** command is `git status`, `max_segments: 3`, `max_depth: 2` +- **THEN** limits are not exceeded + +### Requirement: Reject ask-resolving complex one-liners with actionable guidance + +When `restructure` is enabled AND limits are exceeded AND the chain resolves to `ask`, the plugin SHALL reject the command by throwing in `tool.execute.before` — nothing SHALL execute and no permission dialog SHALL appear for that call. The error message SHALL include the actual segment count and nesting depth and SHALL instruct the model to re-issue the command as separate bash tool calls or as a multi-line script with one command per line. Chains resolving to `allow`, `deny`, or `null`, and parse-error fail-closed denies, SHALL follow existing flows unchanged. + +#### Scenario: Allowed complex chain passes through — allowed stays allowed + +- **WHEN** command `git status && git log && git diff && git show` exceeds `max_segments: 3` and all segments match `"git *": "allow"` +- **THEN** the chain resolves to `allow` and runs without rejection — no restructuring message + +#### Scenario: Complex ask chain rejected + +- **WHEN** command `git status && rm -rf /tmp/x && echo ok && ls` exceeds `max_segments: 3` and `rm` resolves to `ask` +- **THEN** the plugin throws; the command does not execute; no permission dialog appears for this call; the error text contains "4" and the re-issue instruction + +#### Scenario: Deny flow unchanged + +- **WHEN** command `git push --force && git status && git log && git show` exceeds limits but `git push` resolves to `deny` +- **THEN** the existing deny flow applies (wrap + stored deny) — no restructuring message + +#### Scenario: No-opinion chain unchanged + +- **WHEN** command `a && b && c && d` exceeds limits and no bash rule matches any segment (chain action `null`) +- **THEN** the plugin does not throw — the command proceeds to native opencode handling + +#### Scenario: Parse error unchanged + +- **WHEN** command is `echo "unbalanced` (parse error) regardless of complexity +- **THEN** the fail-closed deny applies — no restructuring message + +#### Scenario: Repeated violation — same rejection + +- **WHEN** the model re-issues a complex single-line chain after a rejection +- **THEN** the same rejection fires again with updated counts (no counter, no escalation) + +#### Scenario: Feature disabled — ask flow unchanged + +- **WHEN** `restructure.enabled` is `false` and a complex ask-resolving chain arrives +- **THEN** the existing ask flow applies (wrap, native dialog) — no rejection + +### Requirement: Restructured commands remain fully verified + +Commands re-issued in a compliant form SHALL go through the normal permission evaluation with no bypass: separate tool calls arrive as single-segment commands; a multi-line script is parsed into per-line segments (newlines are segment separators). + +#### Scenario: Multi-line re-issue checked per line + +- **WHEN** the rejected `a && b && c && d` is re-issued as a 4-line script +- **THEN** `parseChain` yields 4 segments, each evaluated independently against permission rules; allowed lines pass, uncovered lines ask + +#### Scenario: Separate calls checked individually + +- **WHEN** the model re-issues as four single-command tool calls +- **THEN** each call is a single segment, evaluated independently; allowed ones run + +### Requirement: Documentation reflects actual chain behavior and JSONC config + +The README SHALL describe the implemented semantics: fully-allowed chains pass through without interruption; the "multi-segment chains trigger ask (defense-in-depth)" claim SHALL be removed or corrected. The README SHALL document the `opencode-bash-guard.jsonc` file (locations, `restructure` schema, defaults) and include a recommended AGENTS.md snippet as a complementary soft layer. + +#### Scenario: README example table corrected + +- **WHEN** the README documents `git status && git log` with both segments allowed +- **THEN** it states the chain passes through (not "ask") + +#### Scenario: Config file documented + +- **WHEN** the README "Readable commands" section is read +- **THEN** it shows the `opencode-bash-guard.jsonc` example with `restructure` nested fields, both config locations, and an AGENTS.md instruction snippet diff --git a/openspec/changes/chain-restructuring/tasks.md b/openspec/changes/chain-restructuring/tasks.md new file mode 100644 index 0000000..69a131e --- /dev/null +++ b/openspec/changes/chain-restructuring/tasks.md @@ -0,0 +1,50 @@ +## 1. Plugin Config Loader (`src/plugin-config.ts`, new) + +- [ ] 1.1 Add `jsonc-parser` dependency +- [ ] 1.2 Implement file discovery: global (`~/.config/opencode/opencode-bash-guard.jsonc`, honoring `XDG_CONFIG_HOME`) and project (`/.opencode/opencode-bash-guard.jsonc`, root from plugin init `input.directory`) +- [ ] 1.3 Parse JSONC (comments, trailing commas); missing file → skip silently; invalid JSONC → warning naming the file, feature treated as disabled +- [ ] 1.4 Deep-merge global + project (project wins); implement `parsePluginConfig(files): PluginConfig` +- [ ] 1.5 Define `RestructureConfig { enabled: boolean; maxSegments: number; maxDepth: number }` — defaults `enabled: false`, `maxSegments: 3`, `maxDepth: 2`; non-numeric or `< 1` threshold values dropped with warning, default applies +- [ ] 1.6 Wire loader into plugin init in `src/index.ts` +- [ ] 1.7 Unit tests: no files → disabled; project-over-global merge; JSONC comments/trailing commas; invalid file → warning + disabled; invalid thresholds → defaults; explicit values honored + +## 2. Chain Metrics (`src/chain.ts`) + +- [ ] 2.1 Extend `parseChain` to report max substitution nesting depth: track depth while recursively walking `$()`, backticks, and meta-command string args +- [ ] 2.2 Return shape: `{ segments, maxDepth, parseError }` (or parallel accessor) — keep existing call sites compiling +- [ ] 2.3 Unit tests: flat chain depth 0/1, single-level `$()`, triple nesting, `bash -c "..."` depth accounting + +## 3. Enforcement (`src/enforce.ts`) + +- [ ] 3.1 Export rejection-message builder with actual counts: `[opencode-bash-guard] Complex one-liner rejected (N chained commands, nesting depth D). Re-issue as separate bash tool calls, or as a multi-line script with one command per line — each command is then permission-checked individually.` +- [ ] 3.2 Implement complexity check: single-line command (no newline) → segments > maxSegments; any command → maxDepth > maxDepthLimit; multi-line exempt from segment check +- [ ] 3.3 In `beforeExecute`: after `resolveChain`, throw the guidance Error ONLY when action is `ask` AND `restructure.enabled` AND limits exceeded; `allow`/`deny`/`null`/parse-error flows verbatim +- [ ] 3.4 Unit tests: allowed complex chain passes (no throw); complex ask chain throws (message contains counts + instruction); deny/ask-disabled/null/parse-error unchanged; repeated violation re-throws; boundaries (N == max passes, N+1 throws; multi-line with many lines passes segment check) + +## 4. Integration Tests + +- [ ] 4.1 Multi-line re-issue: 4-line script parses into 4 segments, each checked independently +- [ ] 4.2 Separate-calls re-issue: single-segment commands evaluate normally +- [ ] 4.3 Config matrix: disabled → zero throws across all commands; enabled with defaults; enabled with custom thresholds + +## 5. Documentation + +- [ ] 5.1 Fix README stale claims: `git status && git log` row → "passes through"; remove/correct "multi-segment chains trigger ask (defense-in-depth)" +- [ ] 5.2 README new section "Readable commands": `opencode-bash-guard.jsonc` example (both locations, `restructure` nested fields with defaults), strict-greater threshold semantics, what a rejection looks like (tool error, nothing executes, no dialog), AGENTS.md snippet: + + ```markdown + ## Bash command style + - Issue one command per tool call. For sequences, use separate bash calls. + - Never write chained one-liners (`a && b && c`). If rejected, split and retry. + ``` + +- [ ] 5.3 README known limitations: config read once at startup (restart to apply); multi-line scripts with long per-line `&&`-chains pass the segment check (v1 edge); no retry counter; note on `permission.ask` reliability (issue anomalyco/opencode#19469) pending separate verification + +## 6. Verification + +- [ ] 6.1 `npm test` — full suite green including new tests +- [ ] 6.2 `npm run build` — type-check passes +- [ ] 6.3 Manual: complex not-allowed one-liner with `restructure.enabled: true` → tool error with counts, model retries as multi-line → readable ask dialog +- [ ] 6.4 Manual: complex fully-allowed one-liner with feature enabled → runs (allowed stays allowed) +- [ ] 6.5 Manual: no config file → behavior identical to previous release +- [ ] 6.6 Manual: deny/ask commands with feature disabled → unchanged