From 14142e1496f682d01e1e90081c5203f6fb82de96 Mon Sep 17 00:00:00 2001 From: Aaron Stannard Date: Wed, 19 Aug 2026 19:02:49 -0500 Subject: [PATCH 1/2] Sync delta specs for completed OpenSpec changes Apply the delta specs of 16 completed changes to the main specs. Create four new capability specs: daemon-shell-path, shell-policy-evaluator-architecture, skillserver-native-sidecar-sync, and named-model-definitions. Correct three reminder requirements against the merged code: - One-shot success removes the definition and its history. Only a poisoned one-shot is soft-deleted (PR #1821). - No execution capacity cap exists (PR #1839). The nack and ack-skip policy now covers a duplicate active occurrence and a short acknowledgement lease. - Every delivery kind now holds its envelope. ReminderDeliveryResult replaces ReminderDeliveryObserved. --- .../specs/audience-context-filtering/spec.md | 24 +- openspec/specs/daemon-shell-path/spec.md | 117 ++++ .../specs/named-model-definitions/spec.md | 60 ++ openspec/specs/netclaw-cli/spec.md | 382 ++++++----- openspec/specs/netclaw-mcp/spec.md | 238 +++++++ .../specs/netclaw-model-capabilities/spec.md | 47 ++ .../specs/netclaw-model-providers/spec.md | 105 ++- openspec/specs/netclaw-scheduling/spec.md | 592 +++++++++-------- openspec/specs/netclaw-subagents/spec.md | 48 ++ openspec/specs/netclaw-testing/spec.md | 18 + .../specs/reminder-execution-history/spec.md | 25 + openspec/specs/session-cwd/spec.md | 43 ++ .../spec.md | 239 +++++++ .../specs/skill-index-compression/spec.md | 24 +- openspec/specs/skill-tools/spec.md | 52 +- .../skillserver-native-sidecar-sync/spec.md | 127 ++++ openspec/specs/tool-approval-gates/spec.md | 617 ++++++++++++++++-- 17 files changed, 2193 insertions(+), 565 deletions(-) create mode 100644 openspec/specs/daemon-shell-path/spec.md create mode 100644 openspec/specs/named-model-definitions/spec.md create mode 100644 openspec/specs/shell-policy-evaluator-architecture/spec.md create mode 100644 openspec/specs/skillserver-native-sidecar-sync/spec.md diff --git a/openspec/specs/audience-context-filtering/spec.md b/openspec/specs/audience-context-filtering/spec.md index 9bb2b2766..e75ad1b2c 100644 --- a/openspec/specs/audience-context-filtering/spec.md +++ b/openspec/specs/audience-context-filtering/spec.md @@ -15,12 +15,7 @@ fallback. ## Requirements ### Requirement: Context layer audience filtering -The context layer system SHALL accept a `TrustAudience` parameter on -`IContextLayerProvider.GetContextLayer()`. Each context layer implementation -SHALL use the audience to determine what content to return. The -`ContextAssemblyInput` record SHALL include a `TrustAudience Audience` field. -When a feature is disabled deployment-wide, the corresponding context layer -SHALL also return empty even for non-Public audiences. +The context layer system SHALL accept a `TrustAudience` parameter on `IContextLayerProvider.GetContextLayer()`. Each context layer implementation SHALL use the audience to determine what content to return. The `ContextAssemblyInput` record SHALL include a `TrustAudience Audience` field. When a feature is disabled deployment-wide, the corresponding context layer SHALL also return empty even for non-Public audiences. The skill context layer SHALL use separate Team and Personal index values when source permissions differ. #### Scenario: Public audience receives no skill index @@ -47,15 +42,23 @@ SHALL also return empty even for non-Public audiences. - **THEN** `SkillIndexContextLayer.GetContextLayer(Team)` returns empty string - **AND** no skill index appears in the session's system messages -#### Scenario: Team audience receives all allowed context layers +#### Scenario: Team audience receives allowed context layers - **WHEN** a Team-audience session assembles context -- **THEN** all enabled context layers return their full content +- **THEN** all enabled context layers return their allowed content -#### Scenario: Personal audience receives all allowed context layers +#### Scenario: Personal audience receives allowed context layers - **WHEN** a Personal-audience session assembles context -- **THEN** all enabled context layers return their full content +- **THEN** all enabled context layers return their allowed content + +#### Scenario: MCP prompt server differs by audience + +- **GIVEN** Personal can use MCP server `gigatron` +- **AND** Team cannot use MCP server `gigatron` +- **WHEN** both audiences request the skill context layer +- **THEN** the Personal index contains `mcp__gigatron__` prompt skills +- **AND** the Team index does not reveal those skill names ### Requirement: Session block path redaction @@ -150,3 +153,4 @@ fail loudly rather than adopt a default audience. - **WHEN** the pipeline derives the effective audience - **THEN** the derived audience reflects the Personal source audience - **AND** no default-audience value participates in the derivation + diff --git a/openspec/specs/daemon-shell-path/spec.md b/openspec/specs/daemon-shell-path/spec.md new file mode 100644 index 000000000..f7c35394c --- /dev/null +++ b/openspec/specs/daemon-shell-path/spec.md @@ -0,0 +1,117 @@ +# daemon-shell-path Specification + +## Purpose + +Define how the installed Netclaw daemon gets the operator's real `PATH`. A +systemd `--user` service does not inherit the login shell environment. Netclaw +therefore captures the operator `PATH` at install time and delivers it through a +netclaw-owned environment file. Install, `doctor`, and uninstall keep that file +correct. + +## Requirements + +### Requirement: Installed service PATH is captured from the operator environment, not guessed + +When installing the systemd `--user` service, `netclaw daemon install` SHALL provision the +daemon's `PATH` by capturing the operator's real `PATH` from the CLI process's own inherited +environment. It SHALL NOT write a hardcoded/guessed list of directories, and it SHALL NOT execute +a shell or source operator dotfiles to obtain the value. The captured `PATH` SHALL be delivered to +the daemon via a netclaw-owned environment file referenced by the unit's `EnvironmentFile=` +directive, and the generated unit SHALL NOT contain an inline `Environment=PATH=` directive. + +The provisioned `PATH` value SHALL place the daemon's own install directory first, then the +captured operator `PATH`, then a guaranteed system-directory floor +(`/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin`) so the daemon's shell remains functional even when +the installing shell's `PATH` was empty or partial. Entries SHALL be de-duplicated, and empty +`PATH` elements (which POSIX resolves to the current directory) SHALL be dropped. + +#### Scenario: Install captures the caller's PATH into the environment file + +- **WHEN** the operator runs `netclaw daemon install` from a shell whose `PATH` includes + `~/.dotnet` +- **THEN** the netclaw-owned environment file contains a `PATH=` line that includes `~/.dotnet` +- **AND** the file's `PATH` begins with the daemon's install directory +- **AND** no shell process was spawned to read the `PATH` + +#### Scenario: Generated unit wires the environment file and omits inline PATH + +- **WHEN** `netclaw daemon install` writes `~/.config/systemd/user/netclaw.service` +- **THEN** the unit contains an `EnvironmentFile=` directive pointing at the netclaw-owned + environment file +- **AND** the unit does NOT contain an `Environment=PATH=` directive + +### Requirement: `doctor --fix` rehydrates the daemon PATH environment file + +When the installed unit references the daemon PATH environment file, `netclaw doctor --fix` SHALL +rehydrate that file from the current shell's `PATH` when it is missing or does not include the +daemon's install directory. Rehydration SHALL run independently of whether the application config +file (`netclaw.json`) exists. The fix SHALL write files only — creating the parent directory if it +is absent — and SHALL surface an explicit instruction to run `systemctl --user restart netclaw`; it +SHALL NOT restart the daemon implicitly. `doctor --fix` SHALL NOT rewrite the systemd unit: a unit +that does not reference the environment file (a legacy inline-`PATH` unit) is a reinstall case, +routed by the doctor check. + +#### Scenario: Missing environment file is recreated by the fix + +- **WHEN** the systemd unit is installed but the daemon PATH environment file is absent +- **AND** the operator runs `netclaw doctor --fix` +- **THEN** the fix writes the environment file from the current shell's `PATH` +- **AND** the fix output instructs the operator to `systemctl --user restart netclaw` +- **AND** the fix does not restart the daemon + +#### Scenario: Rehydration runs even when the app config file is absent + +- **WHEN** `netclaw.json` does not exist +- **AND** the systemd unit is installed but its PATH environment file is stale or missing +- **AND** the operator runs `netclaw doctor --fix` +- **THEN** the environment-file rehydration fix is still evaluated and applied + +### Requirement: Systemd PATH doctor check validates the environment-file wiring + +`SystemdUnitPathDoctorCheck` SHALL validate that the installed unit references the daemon PATH +environment file via `EnvironmentFile=`, that the referenced file exists, and that the file's +`PATH` includes the daemon's install directory. When the referenced file is missing or omits the +install directory, remediation SHALL point the operator at `netclaw doctor --fix` followed by a +restart. A legacy unit that supplies its `PATH` inline (`Environment=PATH=`) SHALL pass when that +inline `PATH` includes the install directory (with a note to migrate via reinstall), and SHALL warn +with a reinstall remediation otherwise. The check SHALL pass silently when no service is installed +or on non-Linux platforms. + +#### Scenario: Wired, present, and install-dir on PATH passes + +- **WHEN** the unit references the environment file, the file exists, and its `PATH` includes the + install directory +- **THEN** the check passes + +#### Scenario: Functional legacy inline-PATH unit passes with a migration note + +- **WHEN** the installed unit has no `EnvironmentFile=` but supplies an inline `Environment=PATH=` + that includes the install directory +- **THEN** the check passes and notes that re-running `netclaw daemon install` migrates it + +#### Scenario: Unwired or broken legacy unit warns with reinstall remediation + +- **WHEN** the installed unit has no `EnvironmentFile=` and no inline `PATH` that includes the + install directory +- **THEN** the check returns a warning with remediation to reinstall and restart + +#### Scenario: Referenced environment file absent warns + +- **WHEN** the unit references the environment file but the file does not exist on disk +- **THEN** the check returns a warning with remediation to run `netclaw doctor --fix` and restart + +#### Scenario: No service installed skips + +- **WHEN** no netclaw systemd unit file exists +- **THEN** the check passes without warning + +### Requirement: Uninstall removes the daemon PATH environment file + +`netclaw daemon uninstall` SHALL remove the netclaw-owned daemon PATH environment file in addition +to the unit file, leaving no orphaned environment file behind. + +#### Scenario: Uninstall deletes the environment file + +- **WHEN** the operator runs `netclaw daemon uninstall` with an installed service and an existing + daemon PATH environment file +- **THEN** both the unit file and the daemon PATH environment file are deleted diff --git a/openspec/specs/named-model-definitions/spec.md b/openspec/specs/named-model-definitions/spec.md new file mode 100644 index 000000000..caa5ff1d0 --- /dev/null +++ b/openspec/specs/named-model-definitions/spec.md @@ -0,0 +1,60 @@ +# named-model-definitions Specification + +## Purpose + +Define named model definitions that own provider identity, model ID, and +operator capability overrides. Model roles reference a definition by name. A +role switch therefore does not destroy the metadata of the definition it leaves. +This capability also defines legacy configuration acceptance, migration, and +conflict behavior. + +## Requirements + +### Requirement: Model-owned named definitions +The system SHALL store provider identity, model ID, context-window override, modality overrides, and provenance in named model definitions independent of runtime role assignment. Roles SHALL reference definitions by name. + +#### Scenario: Switching away and back preserves overrides +- **GIVEN** definition `vision` has a manual `InputModalities` override +- **WHEN** Main switches from `vision` to another definition and back +- **THEN** the `vision` definition SHALL remain unchanged +- **AND** Main SHALL resolve to its original override + +#### Scenario: Manual absence remains runtime detection +- **GIVEN** an existing definition omits an optional capability property +- **WHEN** the definition is assigned to another role +- **THEN** the property SHALL remain absent +- **AND** no tombstone or discovered replacement SHALL be persisted + +### Requirement: Legacy model configuration compatibility +The system SHALL accept the legacy inline Main/Fallback/Compaction shape without rewriting it during startup and SHALL resolve it to the same runtime model selection. + +#### Scenario: Existing deployment starts after upgrade +- **GIVEN** a valid configuration written by the latest stable Netclaw image +- **WHEN** the upgraded daemon starts +- **THEN** startup SHALL succeed with equivalent model role values and capabilities +- **AND** the configuration file SHALL not be rewritten merely by startup + +#### Scenario: Explicit mutation migrates legacy shape +- **GIVEN** a valid legacy configuration +- **WHEN** an operator performs a model-writing command or runs doctor fix +- **THEN** the system SHALL atomically persist the named shape before completing the mutation +- **AND** the persisted named shape SHALL resolve to the same runtime values + +#### Scenario: Ambiguous shape fails loudly +- **GIVEN** configuration contains both legacy role objects and named role references +- **WHEN** configuration is validated or loaded +- **THEN** the operation SHALL fail with remediation identifying the mixed shape + +### Requirement: Reference integrity +Every persisted role reference SHALL resolve to an existing definition before persistence and startup. + +#### Scenario: Missing definition is rejected +- **WHEN** a role references an unknown definition +- **THEN** validation SHALL fail before runtime client construction +- **AND** no partial configuration write SHALL occur + +#### Scenario: Conflicting legacy duplicates are rejected +- **GIVEN** two legacy roles identify the same provider/model but contain conflicting overrides +- **WHEN** migration is requested +- **THEN** migration SHALL fail with the conflicting roles and fields +- **AND** the legacy file SHALL remain unchanged diff --git a/openspec/specs/netclaw-cli/spec.md b/openspec/specs/netclaw-cli/spec.md index 1320351a0..d980ee98a 100644 --- a/openspec/specs/netclaw-cli/spec.md +++ b/openspec/specs/netclaw-cli/spec.md @@ -91,220 +91,232 @@ longer configured. ### Requirement: Operator CLI for persistent tool approvals -The CLI SHALL provide a `netclaw approvals` command surface for -inspecting, revoking, and adding entries to the persistent approvals -file (`~/.netclaw/config/tool-approvals.json`). The command SHALL -operate on the file directly via `Netclaw.Configuration.ToolApprovalStore` -without requiring the daemon to be running. Bare `netclaw approvals` -(and `netclaw approvals tui`) SHALL launch an interactive Termina TUI -page. Single-shot subcommands SHALL be `list`, `revoke`, `trust-verb`, -and `help`. - -`list` SHALL accept `--audience `, `--tool `, -and `--json`. Without flags it SHALL print every audience and tool group -in a stable order. Each entry SHALL be labeled by its scope: entries -with a non-null `directory` print as ` in `; entries -with `directory: null` print as ` anywhere`. The CLI SHALL NOT -mix verb and directory entries in a single column. - -`revoke ` SHALL remove entries that match ``. The -pattern SHALL accept either of the user-visible forms emitted by -`list`: ` in ` matches a `(verb, directory)` entry -exactly, and ` anywhere` matches a `(verb, null)` entry. -Case-sensitivity SHALL match the daemon's matcher comparer (Ordinal on -POSIX, OrdinalIgnoreCase on Windows). `revoke` SHALL accept `--audience` -and `--tool` to scope the removal. `revoke --tool --all` SHALL -clear every entry for that tool in the targeted audiences. `revoke` of -a pattern that does not match any entry SHALL exit non-zero with a -clear message; the CLI SHALL NOT silently succeed. - -`trust-verb ` SHALL write a new `(verb, null)` entry for the -specified verb chain — the global wildcard. The subcommand SHALL accept -`--audience ` (default `personal`) and -`--tool ` (default `shell_execute`). `trust-verb` SHALL be the -canonical way to pre-approve a verb for unattended/scheduled invocations -where the cwd will vary across firings. If the entry already exists, -`trust-verb` SHALL exit zero with a "no changes" message. - -The CLI SHALL ONLY support adding global wildcards via `trust-verb`. It -SHALL NOT provide a way to add `(verb, directory)` entries from the -CLI; folder-scoped grants SHALL be acquired exclusively through -interactive approval prompts. This is a deliberate friction asymmetry: -prompt-driven grants are the default user path, and the CLI exists to -handle the unattended case and the global-trust case operators -explicitly want. - -When the underlying store has quarantined a malformed v1 file -(`tool-approvals.json.v1.bak` sibling), the CLI SHALL emit a one-line -note before list/revoke output indicating the quarantine and pointing -at the backup file. The CLI SHALL NOT silently swallow the condition. - -Exit codes SHALL be 0 for success and 1 for user errors (bad flag -combos, unknown audience, no match for revoke, `--all` without `--tool`, -etc.). - -#### Scenario: Empty approvals file lists no entries with exit zero - -- **GIVEN** `tool-approvals.json` does not exist or contains an empty - v2 store +The CLI SHALL provide `netclaw approvals` for the persistent approval file. +It SHALL use `ToolApprovalStore` without a daemon connection. Bare +`netclaw approvals` and `netclaw approvals tui` SHALL open the Termina page. +The other commands SHALL be `list`, `revoke`, `trust-verb`, and `help`. + +`list` SHALL accept `--audience`, `--tool`, and `--json`. Text output SHALL use +a stable order. A shell row SHALL show shell, match kind, canonical phrase, +scope, and age. A non-shell row SHALL show its exact phrase, scope, and age. + +The shell scope label SHALL use these forms: + +- ` token-prefix "" anywhere` +- ` token-prefix "" in ` +- ` legacy-exact "" anywhere` +- ` legacy-exact "" in ` + +A non-shell entry SHALL use `NonShell exact "" anywhere` or +`NonShell exact "" in `. The JSON-quoted phrase SHALL keep +scope separator text unambiguous. `revoke` SHALL accept the prior untyped label +when that label selects one entry. +`--json` SHALL emit the exact version-3 entry form and audience and tool keys. + +`revoke ` SHALL accept each text form from `list`. It SHALL compare +shell, match kind, phrase, and scope. It SHALL use native shell case rules. It +SHALL accept the old untyped scope form only when that form selects one entry. +An ambiguous old form SHALL fail with no file change. + +`revoke` SHALL accept `--audience` and `--tool`. The form +`revoke --tool --all` SHALL remove all entries for that tool. No match +SHALL exit with code 1 and a clear message. + +`trust-verb ` SHALL accept `--audience`, `--tool`, and `--shell`. The +default audience SHALL be `personal`. The default tool SHALL be +`shell_execute`. For `shell_execute`, the default shell SHALL be Bash on POSIX +and PowerShell on Windows. `--shell bash|powershell` SHALL select the canonical +ShellSyntaxTree parser. The standalone CLI SHALL try the PowerShell 7 and +Windows PowerShell 5.1 parsers. It SHALL use a valid PowerShell 7 result first. +It SHALL use a valid Windows PowerShell 5.1 result only when the preferred +parser rejects the phrase. The daemon SHALL continue to use its resolved +native dialect for execution-time command facts. `--shell` with another tool +SHALL be a user error. + +For `shell_execute`, `trust-verb` SHALL accept one complete static command +phrase. It SHALL use ShellSyntaxTree canonical verb tokens and write a global +`TokenPrefix` entry. It SHALL reject compound, dynamic, or incomplete input. +It SHALL not use a private command parser or a whitespace split. + +The occurrence SHALL have no parser-classified argument, flag, assignment, +redirect, cwd effect, substitution, or control-flow effect. The input SHALL +equal the canonical token phrase. The CLI SHALL fail instead of a silent phrase +reduction. It SHALL not reinterpret a parser-classified verb token through +executable-private grammar. + +For another tool, `trust-verb` SHALL write the compatible global non-shell +exact entry. It SHALL keep support for an arbitrary `--tool` value. It SHALL +not add shell members. + +The CLI SHALL add only global entries. A folder entry SHALL come from an +interactive approval. An equal entry SHALL exit with code 0 and a no-change +message. + +If the store is unavailable, each command SHALL exit with code 1. It SHALL +show one bounded error and SHALL not change the active file. If `.v2.bak` +exists, text `list` output and `help` SHALL state the manual recovery steps. +The note SHALL tell the operator to stop the daemon and restore the backup for +the current daemon. `list --json` SHALL remain exact JSON with no prose. The +old v1 quarantine note SHALL remain for `.v1.bak`. + +Exit code 0 SHALL mean success. Exit code 1 SHALL mean a user or store error. + +#### Scenario: Empty store lists no entries + +- **GIVEN** the approval file is absent or has an empty version-3 store - **WHEN** the operator runs `netclaw approvals list` - **THEN** the CLI prints `No persistent approvals.` -- **AND** exits with code `0` +- **AND** it exits with code 0 #### Scenario: List filters by audience -- **GIVEN** `tool-approvals.json` contains entries under `personal` - and `team` -- **WHEN** the operator runs `netclaw approvals list --audience personal` -- **THEN** only the `personal` audience entries are printed +- **GIVEN** the store has `personal` and `team` entries +- **WHEN** the operator uses `list --audience personal` +- **THEN** the output has only `personal` entries -#### Scenario: List labels entries by scope +#### Scenario: List labels typed shell phrases -- **GIVEN** `tool-approvals.json` contains - `{"verb":"git remote","directory":"/home/user/repos/foo/"}` and - `{"verb":"freshdesk","directory":null}` under `personal/shell_execute` +- **GIVEN** a Bash token-prefix grant covers `git push` in `/work/repo` +- **AND** a Bash legacy grant covers `git push` everywhere - **WHEN** the operator runs `netclaw approvals list` -- **THEN** the output includes `git remote in /home/user/repos/foo/` -- **AND** the output includes `freshdesk anywhere` +- **THEN** output has `Bash token-prefix "git push" in /work/repo` +- **AND** output has `Bash legacy-exact "git push" anywhere` -#### Scenario: List emits typed JSON +#### Scenario: List JSON uses version 3 forms -- **GIVEN** `tool-approvals.json` contains - `{"version":2,"audiences":{"personal":{"shell_execute":[ - {"verb":"git push","directory":null}]}}}` +- **GIVEN** the store has token-prefix, legacy, and non-shell entries - **WHEN** the operator runs `netclaw approvals list --json` -- **THEN** the output is valid JSON -- **AND** each entry preserves the `verb`/`directory` shape - -#### Scenario: Revoke removes a folder-scoped entry by user-visible form - -- **GIVEN** `tool-approvals.json` contains - `{"verb":"git remote","directory":"/home/user/repos/foo/"}` and - `{"verb":"freshdesk","directory":null}` -- **WHEN** the operator runs - `netclaw approvals revoke "git remote in /home/user/repos/foo/"` -- **THEN** the `git remote` entry is removed -- **AND** the `freshdesk anywhere` entry remains -- **AND** the CLI exits with code `0` - -#### Scenario: Revoke removes a global wildcard by user-visible form - -- **GIVEN** `tool-approvals.json` contains - `{"verb":"freshdesk","directory":null}` -- **WHEN** the operator runs - `netclaw approvals revoke "freshdesk anywhere"` -- **THEN** the entry is removed -- **AND** the CLI exits with code `0` - -#### Scenario: Revoke with no match exits non-zero - -- **GIVEN** `tool-approvals.json` does not contain `git push` -- **WHEN** the operator runs `netclaw approvals revoke "git push anywhere"` -- **THEN** the CLI prints a no-match message -- **AND** exits with code `1` -- **AND** does not modify the file - -#### Scenario: trust-verb writes a global wildcard entry - -- **GIVEN** `tool-approvals.json` does not yet contain `freshdesk` -- **WHEN** the operator runs `netclaw approvals trust-verb freshdesk` -- **THEN** the file gains entry - `{"verb":"freshdesk","directory":null}` under - `personal/shell_execute` -- **AND** the CLI exits with code `0` +- **THEN** output is valid JSON +- **AND** each entry has its exact version-3 form + +#### Scenario: Revoke removes one typed phrase + +- **GIVEN** token-prefix and legacy grants have phrase `git push` +- **WHEN** the operator revokes `Bash token-prefix "git push" anywhere` +- **THEN** the token-prefix grant is absent +- **AND** the legacy grant remains + +#### Scenario: Old ambiguous revoke form fails + +- **GIVEN** two typed entries render the old form `git push anywhere` +- **WHEN** the operator uses that old form with `revoke` +- **THEN** the command exits with code 1 +- **AND** the store does not change + +#### Scenario: Revoke with no match fails + +- **GIVEN** no store entry matches the pattern +- **WHEN** the operator runs `netclaw approvals revoke ` +- **THEN** the command exits with code 1 +- **AND** the store does not change + +#### Scenario: Static Bash phrase creates token prefix + +- **WHEN** the operator trusts `git push` for Bash `shell_execute` +- **THEN** the store adds Bash tokens `git`, `push` +- **AND** the entry has global scope + +#### Scenario: Native Windows default prefers PowerShell 7 grammar + +- **GIVEN** Netclaw runs natively on Windows +- **WHEN** the operator omits `--shell` for `shell_execute` +- **THEN** the CLI uses a valid PowerShell 7 grant result first +- **AND** it uses the Windows PowerShell 5.1 result only when the first result is invalid +- **AND** it stores shell `PowerShell` + +#### Scenario: Compound shell phrase fails + +- **WHEN** the operator trusts `git status; rm file` for `shell_execute` +- **THEN** the command exits with code 1 +- **AND** the store does not change + +#### Scenario: Shell effects fail phrase creation + +- **WHEN** the operator trusts a phrase with a flag, parser-classified + argument, assignment, or redirect +- **THEN** the command exits with code 1 +- **AND** no reduced token-prefix entry is stored + +#### Scenario: Parser-owned phrase keeps every token + +- **WHEN** the operator trusts `git push origin` +- **AND** ShellSyntaxTree returns canonical tokens `git`, `push`, and `origin` +- **THEN** the store adds all three tokens +- **AND** no `git push` entry is added + +#### Scenario: Non-shell tool stays exact + +- **WHEN** the operator trusts `create-page` for a non-shell tool +- **THEN** the store adds a non-shell exact entry +- **AND** the entry has no shell member #### Scenario: trust-verb is idempotent -- **GIVEN** `tool-approvals.json` already contains - `{"verb":"freshdesk","directory":null}` -- **WHEN** the operator runs `netclaw approvals trust-verb freshdesk` -- **THEN** the file is unchanged -- **AND** the CLI prints a "no changes" message -- **AND** exits with code `0` +- **GIVEN** the exact target entry is in the store +- **WHEN** the operator issues the same trust command +- **THEN** the file does not change +- **AND** the command exits with code 0 -#### Scenario: trust-verb honors --audience and --tool +#### Scenario: Store error fails closed -- **WHEN** the operator runs - `netclaw approvals trust-verb freshdesk --audience team --tool shell_execute` -- **THEN** the entry is written under `team/shell_execute` -- **AND** the CLI exits with code `0` +- **GIVEN** the store has an invalid version-3 entry +- **WHEN** the operator runs list, revoke, or trust-verb +- **THEN** the command exits with code 1 +- **AND** the active file stays byte-identical -#### Scenario: Quarantined v1 file surfaces a one-line note +#### Scenario: Backup note states current recovery path -- **GIVEN** `~/.netclaw/config/tool-approvals.json.v1.bak` exists - (the daemon has previously quarantined a v1 file) +- **GIVEN** `.v2.bak` exists - **WHEN** the operator runs `netclaw approvals list` -- **THEN** the CLI emits a one-line note before the listing pointing - at the `.v1.bak` file -- **AND** the listing reflects only v2 entries +- **THEN** the CLI states how to stop the daemon and restore the backup +- **AND** it does not claim that an old binary can read version 3 -#### Scenario: Daemon picks up CLI-applied trust-verb without restart +#### Scenario: Daemon sees a CLI change without restart -- **GIVEN** the daemon is running -- **WHEN** the operator runs `netclaw approvals trust-verb freshdesk` -- **AND** the agent invokes `freshdesk --since=24h` afterwards -- **THEN** the daemon re-loads the file and observes the new entry -- **AND** the call auto-approves with no prompt -- **AND** the daemon was not restarted +- **GIVEN** the daemon is active +- **WHEN** the CLI adds or revokes a grant +- **THEN** the next actor snapshot sees the new version-3 state -#### Scenario: Bare invocation launches the TUI +#### Scenario: Bare command opens the TUI -- **WHEN** the operator runs `netclaw approvals` with no subcommand -- **THEN** the CLI launches the interactive Termina approvals page -- **AND** the page displays entries grouped by audience and tool with - scope labels (` in ` / ` anywhere`) +- **WHEN** the operator runs `netclaw approvals` +- **THEN** the CLI opens the Termina approvals page +- **AND** each row uses the typed phrase and scope label ### Requirement: Approval surfaces show grant creation time -The `netclaw approvals` inspection surfaces SHALL display when each -persisted grant was created. Both the `netclaw approvals list` command -and the interactive `netclaw approvals` Termina TUI list SHALL derive -this from the `ApprovalEntry.createdAt` field. - -Human-readable output (the default `list` rendering and the TUI list -rows) SHALL render the creation time as relative text — for example -`added 3 days ago`. An entry whose `createdAt` is `null` (a grant -written before timestamp tracking) SHALL render a stable placeholder -(`added —`) rather than a fabricated or omitted value. The creation-time -text SHALL NOT be mixed into the scope label column; it is presented as -distinct per-entry metadata. - -`netclaw approvals list --json` SHALL expose the raw `createdAt` value -on each entry — an ISO-8601 string when present, `null` otherwise — so -scripts can compare it against daemon log timestamps. The JSON output -SHALL remain a superset of the previous shape: existing `verb` and -`directory` fields are unchanged. +The approval CLI and TUI SHALL show `createdAt` for each entry. Text output +SHALL use relative text, such as `added 3 days ago`. A null value SHALL show +`added —`. The age SHALL stay separate from the scope label. + +`list --json` SHALL emit the raw `createdAt` member. The member SHALL contain an +ISO-8601 value or JSON null. It SHALL appear with the exact version-3 entry +form. A token-prefix entry does not need a `verb` member. #### Scenario: List shows relative creation time -- **GIVEN** `tool-approvals.json` contains an entry whose `createdAt` is - three days before now +- **GIVEN** an entry has a timestamp from three days before now - **WHEN** the operator runs `netclaw approvals list` -- **THEN** the entry's row includes relative text such as `added 3 days ago` +- **THEN** the row includes `added 3 days ago` -#### Scenario: Entry without a timestamp shows a placeholder +#### Scenario: Null timestamp shows a placeholder -- **GIVEN** `tool-approvals.json` contains an entry with no `createdAt` +- **GIVEN** an entry has null `createdAt` - **WHEN** the operator runs `netclaw approvals list` -- **THEN** the entry's row shows the `added —` placeholder -- **AND** the command exits with code `0` +- **THEN** the row shows `added —` -#### Scenario: JSON output exposes the raw timestamp +#### Scenario: JSON output shows raw time -- **GIVEN** `tool-approvals.json` contains one entry with a `createdAt` - and one without +- **GIVEN** one entry has a timestamp and one has null - **WHEN** the operator runs `netclaw approvals list --json` -- **THEN** the first entry's JSON object includes a `createdAt` - ISO-8601 string -- **AND** the second entry's `createdAt` is `null` +- **THEN** the first entry has an ISO-8601 `createdAt` +- **AND** the second entry has JSON null -#### Scenario: TUI list shows creation time per entry +#### Scenario: TUI shows time for each row -- **GIVEN** the operator launches the interactive `netclaw approvals` TUI -- **AND** `tool-approvals.json` contains at least one timestamped entry -- **WHEN** the approvals list page renders -- **THEN** each row shows the grant's relative creation time alongside - its scope label +- **GIVEN** the store has a timestamped entry +- **WHEN** the approvals page appears +- **THEN** the row shows its relative age and typed scope ### Requirement: CLI derives local control-plane endpoint from daemon bind config @@ -357,3 +369,21 @@ The daemon-host CLI SHALL decide whether to attach a bearer token based on wheth - **AND** daemon config exposure mode is `local` - **WHEN** the CLI builds its daemon connection - **THEN** it does not attach a bearer token by default + + +### Requirement: Named model role management + +Model CLI and TUI operations SHALL assign roles by changing references and SHALL edit model metadata only through the selected definition. + +#### Scenario: Assign existing definition + +- **WHEN** an operator assigns an existing named definition to Main +- **THEN** only the Main role reference SHALL change +- **AND** the definition SHALL keep its stored capability overrides + +#### Scenario: Mutating legacy configuration + +- **GIVEN** the CLI loads a valid legacy model configuration +- **WHEN** a model mutation is requested +- **THEN** the CLI SHALL migrate and validate the canonical shape before persistence +- **AND** failure SHALL leave the original file unchanged diff --git a/openspec/specs/netclaw-mcp/spec.md b/openspec/specs/netclaw-mcp/spec.md index 0cd233365..4d9aefbf0 100644 --- a/openspec/specs/netclaw-mcp/spec.md +++ b/openspec/specs/netclaw-mcp/spec.md @@ -419,3 +419,241 @@ audience, and for granted tool names that do not exist on the server. - **GIVEN** no audience profile has `McpServerToolGrants` entries for `memorizer` - **WHEN** the daemon connects to `memorizer` - **THEN** no tool change detection warnings are logged for that server + +### Requirement: MCP tool and prompt notification compatibility + +The system SHALL listen for tool and prompt list changes on each published MCP client generation. + +For MCP revision 2026-07-28, the system SHALL use one `subscriptions/listen` request. +It SHALL enable only the event types in the matching acknowledgement. + +For older revisions, the system SHALL accept direct list-change notifications only for capabilities that declare `listChanged` support. + +#### Scenario: Modern server accepts both event types + +- **GIVEN** a server negotiates MCP revision 2026-07-28 +- **AND** it acknowledges tool and prompt list changes +- **WHEN** it sends either accepted notification with the matching subscription identifier +- **THEN** the system requests a catalog refresh without waiting for the poll interval + +#### Scenario: Modern server accepts one event type + +- **GIVEN** a server negotiates MCP revision 2026-07-28 +- **AND** it acknowledges only tool list changes +- **WHEN** it sends a prompt list notification +- **THEN** the system does not request a refresh for that notification +- **AND** the existing poll remains active + +#### Scenario: Legacy server declares direct notifications + +- **GIVEN** a server negotiates a revision before 2026-07-28 +- **AND** its tool capability declares `listChanged` +- **WHEN** it sends a direct tool list-change notification +- **THEN** the system requests a catalog refresh without a `subscriptions/listen` request + +#### Scenario: Server declares no notification support + +- **GIVEN** a server does not support modern or legacy catalog notifications +- **WHEN** the connection becomes healthy +- **THEN** the system keeps the existing catalog poll active +- **AND** the connection remains healthy + +### Requirement: Notification refresh preserves atomic catalog generations + +The system SHALL list the complete supported tool and prompt candidate before it publishes a notification refresh. +It SHALL publish one immutable generation only when the complete catalog fingerprint changes. + +It SHALL keep the last good generation after any list failure. +It SHALL retain one active refresh and at most one queued follow-up refresh for each server. + +#### Scenario: Tool notification changes the catalog + +- **GIVEN** a connected server has a published tool and prompt generation +- **WHEN** a tool notification starts a successful refresh with a changed fingerprint +- **THEN** the system publishes one new generation with the complete tool and prompt catalog + +#### Scenario: Duplicate notifications do not create unbounded work + +- **GIVEN** one notification refresh is active +- **WHEN** the server sends repeated tool and prompt notifications +- **THEN** the system queues at most one follow-up refresh +- **AND** it does not run concurrent catalog refreshes for that server + +#### Scenario: Notification refresh finds no change + +- **GIVEN** a connected server sends a supported notification +- **WHEN** the complete catalog fingerprint is unchanged +- **THEN** the system keeps the current generation +- **AND** it resets the poll interval after the successful check + +#### Scenario: Notification refresh fails + +- **GIVEN** a connected server has a last good generation +- **WHEN** a notification refresh cannot list the complete catalog +- **THEN** the system keeps the last good generation +- **AND** a later notification or poll can retry the refresh + +### Requirement: MCP catalog notification lease lifecycle + +Each MCP client candidate SHALL own one notification lease. +The system SHALL install its handlers before client creation and activate refresh work only after publication. + +The system SHALL deactivate and dispose the lease when it replaces or disposes its client. +A stale lease SHALL NOT refresh a later generation. + +#### Scenario: Notification arrives before publication + +- **GIVEN** a candidate client receives a supported notification during initialization +- **WHEN** the system publishes that candidate +- **THEN** its lease processes the queued notification against the published generation + +#### Scenario: Reconnect renews the lease + +- **GIVEN** a server has a published connection and notification lease +- **WHEN** the system publishes a replacement connection +- **THEN** the replacement owns a new notification lease +- **AND** the old lease cannot refresh the replacement generation + +#### Scenario: Shutdown removes notification work + +- **GIVEN** a published connection has an active notification lease +- **WHEN** daemon shutdown disposes the connection +- **THEN** the system stops the lease worker +- **AND** it disposes the client without leaked notification work + +### Requirement: MCP notification failure and repair behavior + +The system SHALL keep a usable MCP connection and the existing poll after notification setup or listener failure. +It SHALL report the compatibility mode and failures through safe structured logs. + +#### Scenario: Modern subscription method is unsupported + +- **GIVEN** a server negotiates MCP revision 2026-07-28 +- **WHEN** `subscriptions/listen` returns an unsupported-method error +- **THEN** the system keeps the connection and catalog available +- **AND** the existing poll remains the repair path +- **AND** the system logs the failure category without raw protocol content + +#### Scenario: Modern acknowledgement times out + +- **GIVEN** a server accepts the listen request but sends no matching acknowledgement +- **WHEN** the 15-second `TimeProvider` timeout expires +- **THEN** the system keeps the connection and catalog available +- **AND** the existing poll remains the repair path + +#### Scenario: Listener closes after publication + +- **GIVEN** a modern notification listener is active +- **WHEN** its request ends unexpectedly +- **THEN** the system disables that notification lease +- **AND** it logs a warning +- **AND** the existing poll remains the repair path + +#### Scenario: Poll repairs a missed notification + +- **GIVEN** a connected server changes its catalog without a usable notification +- **WHEN** the next catalog poll succeeds +- **THEN** the system publishes the repaired catalog through the same generation rules + +### Requirement: MCP prompt discovery and generation ownership + +The system SHALL list prompts when an enabled MCP server declares prompt support. +It SHALL publish prompt descriptors in the same immutable server generation as the discovered tools. + +Each descriptor SHALL use the logical name `mcp____`. +It SHALL retain the server name, prompt name, prompt arguments, and generation. + +#### Scenario: Prompt-capable server connects + +- **GIVEN** an enabled server declares prompt support +- **WHEN** the daemon initializes the server connection +- **THEN** the daemon lists the server prompts +- **AND** it publishes the tools and prompts in one server generation +- **AND** each prompt appears in the skill registry under its canonical logical name + +#### Scenario: Tool-only server connects + +- **GIVEN** an enabled server does not declare prompt support +- **WHEN** the daemon initializes the server connection +- **THEN** the daemon does not call `prompts/list` +- **AND** the server tools remain available + +#### Scenario: Prompt discovery fails during replacement + +- **GIVEN** a healthy published server generation +- **WHEN** a replacement candidate cannot list its declared prompts +- **THEN** the system keeps the prior server generation +- **AND** it keeps the prior MCP prompt skill inventory +- **AND** diagnostics report the replacement failure + +### Requirement: MCP prompt catalog poll + +The existing MCP catalog poll SHALL include prompts for a prompt-capable server. +It SHALL publish one replacement generation when a tool or prompt descriptor changes. + +#### Scenario: Prompt descriptor changes + +- **GIVEN** a connected server changes a prompt description or argument descriptor +- **WHEN** the next catalog poll succeeds +- **THEN** the system publishes a new server generation +- **AND** the skill registry contains the new prompt descriptor + +#### Scenario: Prompt catalog becomes empty + +- **GIVEN** a connected prompt-capable server removes its final prompt +- **WHEN** the next catalog poll succeeds with an empty prompt list +- **THEN** the system removes that server's MCP prompt skills +- **AND** it preserves the server's tools and file skills + +### Requirement: MCP prompt server permission + +The system SHALL use the existing MCP server grant for prompt discovery and prompt use. +It SHALL NOT add a prompt-specific grant category. + +#### Scenario: Audience can use the server + +- **GIVEN** an audience can use MCP server `gigatron` +- **WHEN** the system builds that audience's skill index +- **THEN** allowed `mcp__gigatron__*` prompt skills appear + +#### Scenario: Audience cannot use the server + +- **GIVEN** an audience cannot use MCP server `gigatron` +- **WHEN** the system builds that audience's skill index or handles a prompt load +- **THEN** no `gigatron` prompt descriptor appears +- **AND** the load follows the generic denied result + +#### Scenario: Unknown skill fallback does not reveal remote prompts + +- **GIVEN** the registry contains MCP prompt skills from one or more servers +- **WHEN** a session requests an unknown skill name +- **THEN** the fallback list contains no MCP server or prompt names +- **AND** the audience-filtered skill index remains the discovery source for remote prompts + +### Requirement: MCP prompt load generation and failure behavior + +The system SHALL resolve an MCP prompt through the client generation that supplied its skill descriptor. +It SHALL fail visibly when the descriptor is stale, the server is unavailable, or the result has unsupported content. + +#### Scenario: Current prompt descriptor loads + +- **GIVEN** an MCP prompt skill references the current server generation +- **WHEN** `skill_load` loads the prompt +- **THEN** the system calls `prompts/get` on that generation +- **AND** the result identifies the source server, prompt, and generation +- **AND** the result preserves each prompt message role + +#### Scenario: Stale prompt descriptor fails + +- **GIVEN** an MCP prompt skill references a replaced server generation +- **WHEN** `skill_load` loads the prompt +- **THEN** the system returns an explicit stale-generation error +- **AND** it does not call `prompts/get` on the new generation + +#### Scenario: Unsupported prompt content fails + +- **GIVEN** `prompts/get` returns a content block that this slice cannot render +- **WHEN** the adapter processes the result +- **THEN** it returns an explicit unsupported-content error +- **AND** it does not silently omit the block + diff --git a/openspec/specs/netclaw-model-capabilities/spec.md b/openspec/specs/netclaw-model-capabilities/spec.md index 56ec498cf..140c252b5 100644 --- a/openspec/specs/netclaw-model-capabilities/spec.md +++ b/openspec/specs/netclaw-model-capabilities/spec.md @@ -431,3 +431,50 @@ instead take `ModelCapabilities` as a dependency. - **WHEN** the service is constructed - **THEN** it takes `ModelCapabilities` as a dependency (not `SessionConfig`) - **AND** reads `ModelId`, `InputModalities`, and `OutputModalities` from it + +### Requirement: Complete session input compatibility check + +The session actor SHALL check all active persisted media and all new media against the active model input modalities before each model call. +The check SHALL include recovered history and media that a tool adds during the current turn. +The actor SHALL reject an unsupported or unknown modality before any primary, fallback, or provider client receives a request. +The actor SHALL preserve all original media references and SHALL identify the incompatible modalities in the session error. + +#### Scenario: Recovered image history meets a text-only model + +- **GIVEN** a recovered session contains an image media reference +- **AND** the active model accepts text only +- **WHEN** the user resumes the session +- **THEN** the actor SHALL emit an input compatibility error +- **AND** the error SHALL identify image input as unsupported +- **AND** no primary, fallback, or provider client SHALL receive a request + +#### Scenario: New unsupported media is rejected before turn admission + +- **GIVEN** a new user command contains an image media reference +- **AND** the active model accepts text only +- **WHEN** the actor receives the command +- **THEN** the actor SHALL reject the command before it adds the user message to session state +- **AND** no model client SHALL receive a request + +#### Scenario: Tool-produced media is checked before the next call + +- **GIVEN** the active model call starts with compatible text input +- **AND** a tool result adds media that the active model cannot accept +- **WHEN** the actor prepares the next model call +- **THEN** the actor SHALL fail the current turn with an input compatibility error +- **AND** no later model client SHALL receive the incompatible request + +#### Scenario: Unknown persisted modality fails closed + +- **GIVEN** a session contains a media reference with an unknown modality value +- **WHEN** the actor prepares a model call +- **THEN** the actor SHALL emit an input compatibility error +- **AND** no model client SHALL receive a request + +#### Scenario: Compatible media reaches the model + +- **GIVEN** all session media modalities are accepted by the active model +- **WHEN** the actor prepares a model call +- **THEN** the actor SHALL preserve the media references +- **AND** the model call SHALL proceed through normal routing + diff --git a/openspec/specs/netclaw-model-providers/spec.md b/openspec/specs/netclaw-model-providers/spec.md index fe20f3dc9..d20a7c283 100644 --- a/openspec/specs/netclaw-model-providers/spec.md +++ b/openspec/specs/netclaw-model-providers/spec.md @@ -146,24 +146,31 @@ diagnostics. ### Requirement: Primary and fallback model -The system SHALL support configuring both a primary model and a fallback model. -When the primary model is unavailable due to rate limiting, timeout, or error, -the system SHALL automatically switch to the fallback model. Fallback activation -SHALL be logged for operator visibility. +The system SHALL support configuring primary, fallback, and compaction roles as +references to persistent named model definitions. Changing a role SHALL NOT +change the referenced definition. When the primary model is unavailable due to +rate limiting, timeout, or error, the system SHALL automatically switch to the +fallback model. Fallback activation SHALL be logged for operator visibility. #### Scenario: Primary model succeeds -- **GIVEN** both primary and fallback models are configured +- **GIVEN** both primary and fallback roles reference valid definitions - **WHEN** the primary model responds successfully -- **THEN** the primary model response is used -- **AND** no fallback activation occurs +- **THEN** the primary model response SHALL be used +- **AND** no fallback activation SHALL occur #### Scenario: Automatic fallback on primary failure -- **GIVEN** both primary and fallback models are configured +- **GIVEN** both primary and fallback roles reference valid definitions - **WHEN** the primary model returns a rate limit, timeout, or error response -- **THEN** the system retries the request using the fallback model -- **AND** a log entry records the fallback activation with the failure reason +- **THEN** the system SHALL retry using the fallback definition +- **AND** a log entry SHALL record the fallback activation with the failure reason + +#### Scenario: Role switch preserves model definition + +- **GIVEN** a named model definition contains operator capability overrides +- **WHEN** Main or Fallback is assigned to another definition +- **THEN** the previous definition SHALL remain unchanged and available for reassignment #### Scenario: Fallback model also fails @@ -539,3 +546,81 @@ capability. - **THEN** the No-Op chat client SHALL remain active - **AND** chat turns SHALL continue to return the configuration message until restart + +### Requirement: DeepSeek provider + +The system SHALL support DeepSeek as a selectable provider profile with the type key `deepseek`. The provider SHALL use `Microsoft.Extensions.AI.IChatClient` and the stable DeepSeek OpenAI-compatible API. + +The provider SHALL require an API key. It SHALL send the key with HTTP Bearer authentication and SHALL NOT offer OAuth authentication. + +The default endpoint SHALL be `https://api.deepseek.com/v1`. Chat requests SHALL use `/chat/completions`, and model discovery SHALL use `/models`. + +#### Scenario: Operator adds a DeepSeek provider + +- **WHEN** the operator adds a `deepseek` provider with an API key +- **THEN** Netclaw stores the provider profile in configuration +- **AND** Netclaw stores the API key through the encrypted secrets path +- **AND** runtime resolves the provider through `IChatClient` + +#### Scenario: DeepSeek provider has no API key + +- **GIVEN** a `deepseek` provider has no API key +- **WHEN** configuration validation or a provider probe runs +- **THEN** validation fails with DeepSeek-specific API-key guidance +- **AND** Netclaw does not select a real chat client + +#### Scenario: DeepSeek model discovery + +- **GIVEN** a `deepseek` provider has a valid API key +- **WHEN** model discovery runs +- **THEN** Netclaw calls the configured `/models` endpoint with the exact Bearer token +- **AND** Netclaw returns the model IDs from the live response + +#### Scenario: Current DeepSeek model capabilities + +- **WHEN** discovery returns `deepseek-v4-flash` or `deepseek-v4-pro` +- **THEN** Netclaw assigns a one-million-token context window +- **AND** Netclaw assigns text input and output modalities + +#### Scenario: Unknown DeepSeek model metadata + +- **WHEN** discovery returns an unknown DeepSeek model ID without capability metadata +- **THEN** Netclaw leaves its context window unresolved +- **AND** Netclaw does not invent a context value + +### Requirement: DeepSeek reasoning and tool-loop contract + +The DeepSeek provider SHALL map MEAI reasoning options to DeepSeek request fields. It SHALL preserve DeepSeek reasoning content when an assistant tool call returns to the provider. + +The DeepSeek provider SHALL NOT add DeepSeek fields to generic OpenAI-compatible requests. It SHALL NOT send local-server fields to DeepSeek. + +#### Scenario: Disable DeepSeek reasoning + +- **WHEN** a request sets MEAI reasoning effort to `None` +- **THEN** the DeepSeek request sets `thinking.type` to `disabled` + +#### Scenario: Select DeepSeek reasoning effort + +- **WHEN** a request sets low, medium, or high MEAI reasoning effort +- **THEN** the DeepSeek request sets `thinking.type` to `enabled` +- **AND** the request sets `reasoning_effort` to `high` + +#### Scenario: Select maximum DeepSeek reasoning effort + +- **WHEN** a request sets extra-high MEAI reasoning effort +- **THEN** the DeepSeek request sets `thinking.type` to `enabled` +- **AND** the request sets `reasoning_effort` to `max` + +#### Scenario: Replay reasoning during a tool loop + +- **GIVEN** DeepSeek returns reasoning content and a tool call +- **WHEN** Netclaw sends the tool result in the next request +- **THEN** the assistant history includes the returned `reasoning_content` +- **AND** the assistant history includes the original tool call + +#### Scenario: Generic provider payload remains unchanged + +- **WHEN** Netclaw sends a request through the generic OpenAI-compatible profile +- **THEN** it does not add DeepSeek thinking or reasoning-replay fields +- **AND** it retains existing local-server request fields + diff --git a/openspec/specs/netclaw-scheduling/spec.md b/openspec/specs/netclaw-scheduling/spec.md index a9699f6dd..503f396d8 100644 --- a/openspec/specs/netclaw-scheduling/spec.md +++ b/openspec/specs/netclaw-scheduling/spec.md @@ -332,51 +332,49 @@ when listing tasks. ### Requirement: Failure handling and guardrails -Netclaw's reminder manager SHALL track consecutive execution failures per -reminder via `_failureCounts` and SHALL auto-pause a reminder when the -count reaches an internal `FailurePauseThreshold` constant. A successful -execution SHALL reset the failure count to zero. Paused reminders SHALL -remain persisted with `status: "paused"` and SHALL be visible via -`netclaw reminders list`. +The reminder manager SHALL store consecutive failures in each reminder +definition. A successful execution SHALL reset the count. + +The manager SHALL disable a reminder when the count reaches +`FailurePauseThreshold`. The disabled definition SHALL remain available for +status and diagnosis. `FailurePauseThreshold` is not operator-configurable — it lives as an `internal const` on `ReminderManagerActor`. `Akka.Reminders` applies its own separate retry budget (`MaxDeliveryAttempts`, library default) to -envelope delivery; Netclaw's auto-pause threshold is set strictly below -the library's default so the Netclaw-side pause fires first in practice -and operators see a `paused` reminder in `netclaw reminders list` before -the library would mark an occurrence terminally failed. If either -default changes in a way that breaks this ordering, add back a single -operator knob. - -The reminder manager SHALL allow any number of reminder executions to run -concurrently — there is no execution cap, because each execution already has a -one-hour absolute timeout and Akka.Reminders owns failure retry. The manager -SHALL enforce a per-execution timeout (`ExecutionTimeoutSeconds`, internal -const on `ReminderExecutionActor`). - -#### Scenario: Consecutive failures auto-pause task - -- **GIVEN** a scheduled task has failed N times in a row where N equals - `FailurePauseThreshold` -- **WHEN** the Nth failure is reported to `ReminderManagerActor` -- **THEN** the task status is set to `paused` -- **AND** the Akka timer for the task is cancelled -- **AND** a log event is emitted naming the reminder and the failure count -- **AND** the reminder remains in `tasks.json` with `status: "paused"` +envelope delivery; Netclaw's threshold is set strictly below the library's +default so the Netclaw-side pause fires first in practice and operators see a +disabled reminder in `netclaw reminders list` before the library would mark an +occurrence terminally failed. If either default changes in a way that breaks +this ordering, add back a single operator knob. + +The manager SHALL NOT cap the number of concurrent executions. Capacity was +removed because every execution already has a one-hour absolute limit and +Akka.Reminders owns failure retry, so unbounded scheduling pressure on the LLM +is acceptable. + +Each execution SHALL have a one-hour absolute limit. A known timeout SHALL +count as a failed attempt. + +#### Scenario: Consecutive failures disable a reminder + +- **GIVEN** a reminder has one fewer failure than `FailurePauseThreshold` +- **WHEN** its next execution fails +- **THEN** the manager saves the threshold failure count +- **AND** the manager disables the reminder +- **AND** the definition remains available -#### Scenario: Successful execution resets failure counter +#### Scenario: A successful execution resets the failure count -- **GIVEN** a scheduled task has failed twice -- **WHEN** the next execution succeeds -- **THEN** the internal failure count for that reminder is reset to zero -- **AND** subsequent failures start counting from zero again +- **GIVEN** a reminder has one or more consecutive failures +- **WHEN** its next execution succeeds +- **THEN** the manager saves a zero failure count -#### Scenario: Reminders run concurrently without an execution cap +#### Scenario: Reminder fires while other reminders are executing -- **GIVEN** several reminders are already executing -- **WHEN** another reminder fires -- **THEN** the new reminder starts executing immediately +- **GIVEN** several reminder attempts are active +- **WHEN** another occurrence arrives +- **THEN** the manager starts the new execution immediately - **AND** no occurrence is skipped or deferred for capacity reasons #### Scenario: Execution timeout enforced @@ -449,285 +447,122 @@ inline. If no history exists, the tool SHALL return an empty list. - **THEN** the tool call is rejected by the ACL policy - **AND** the agent receives a permission-denied response -### Requirement: Envelope-ack-gated at-least-once delivery for Mode B - -The `ReminderManagerActor` SHALL NOT eagerly ack the -`Aaron.Akka.Reminders` envelope for reminders with -`Delivery.Kind = CurrentSession` (the canonical term for what this -requirement historically called "Mode B"). It SHALL spawn `ReminderExecutionActor` and pass the -`ReminderEnvelope` to the child. The execution actor SHALL acquire -`IReminderClient` via `ReminderClientExtension.Get(Context.System)` -at startup. - -The execution actor SHALL dispatch -`DeliverTrustedSessionTurn(SessionId, Content, MessageSource)` to the -target channel gateway using `Ask` (Slack via -`SlackGatewayActor`, SignalR/TUI via `SignalRGatewayActor`, selected -by `Delivery.OriginChannelType`) with a timeout of -`ReminderSettings.DefaultAckTimeout`. The gateway's handler SHALL -propagate the message down its existing routing hierarchy via -`Forward` (preserving `Sender`) until it reaches the leaf binding / -session actor, which reads `Sender`, places it on the outgoing -`ChannelInput` as `MessageSource.AckTarget`, and populates -`MessageSource.ReminderId` with the reminder delivery key. -`ChannelPipeline.MapToCommand`'s stream sink SHALL use -`cmd.Source?.AckTarget ?? ActorRefs.NoSender` as the `Tell` sender. -`LlmSessionActor`'s `TryReplyAck()` fires `CommandAck` to that sender, -completing the dispatcher's `Ask`. - -When `Delivery.DeliveryRequired = true`, the execution actor SHALL -also wait for a `ReminderDeliveryObserved(reminderId, channelType)` -signal emitted by `ChannelPipeline`'s outbound stage when the -session's assistant reply whose source turn carries a matching -`SourceReminderId` flows out through the channel's subscriber sink. -The execution actor SHALL NOT call `AckAsync(envelope)` until both -`CommandAck` and `ReminderDeliveryObserved` are received for the -reminder. The outbound wait SHALL use a dedicated timeout -(`DeliveryObservedTimeout`, internal const on -`ReminderExecutionActor`) strictly greater than -`DefaultAckTimeout`. - -When `Delivery.DeliveryRequired = false`, `CommandAck` alone SHALL -satisfy the acknowledgment; the outbound signal wait SHALL be skipped. - -On successful ack conditions, the execution actor SHALL call -`await _client.AckAsync(envelope)`, inspect the -`ReminderAckResponse.ResponseCode`, log on non-`Success`, and tell -`Context.Parent` a `ReminderExecutionCompleted(success=true)`. On -Ask-timeout, `CommandNack`, gateway/transport exception, OR -delivery-observed timeout with `DeliveryRequired = true`, the -execution actor SHALL NOT call `AckAsync`; it SHALL tell the parent a -`ReminderExecutionCompleted(success=false)` with a descriptive error -message. The un-acked envelope SHALL be redelivered by -`Aaron.Akka.Reminders` per its built-in `AckTimeout` and -`MaxDeliveryAttempts` defaults. - -For `Delivery.Kind ∈ {Channel, None}`, the manager SHALL continue to -call `_client.AckAsync(envelope)` eagerly after spawning the execution -actor; delivery-success tracking for those kinds flows through -`ExecutionOutputAccumulator` / `ReminderExecutionCompleted` / -`FailurePauseThreshold` as today. - -Redelivery SHALL be best-effort deduped: the target session dedup -pre-checks the reminder's `(reminderId, fireTimestampMs)` pair against -its in-memory `ProcessedReminderIds` set and SHALL reply `CommandAck` -without processing a duplicate when the dedup check hits. - -#### Scenario: CurrentSession envelope held until outbound delivery observed - -- **GIVEN** a `CurrentSession` reminder with `DeliveryRequired = true` - fires -- **WHEN** the execution child `Ask`s the target channel - gateway with a `DeliverTrustedSessionTurn` and the session's - `TryReplyAck()` replies `CommandAck` -- **THEN** the execution child does NOT yet call - `_client.AckAsync(envelope)` -- **WHEN** the session completes its turn and the assistant reply - carrying the matching `SourceReminderId` flows out through the - channel pipeline's outbound stage -- **THEN** `ChannelPipeline` emits a - `ReminderDeliveryObserved(reminderId, channelType)` signal addressed - to the execution actor -- **AND** the execution actor calls `await _client.AckAsync(envelope)` - exactly once -- **AND** the execution actor tells `Context.Parent` a - `ReminderExecutionCompleted(success=true)` - -#### Scenario: CurrentSession outbound delivery timeout fails loud - -- **GIVEN** a `CurrentSession` reminder with `DeliveryRequired = true` - fires and `CommandAck` was received from the session -- **WHEN** `DeliveryObservedTimeout` elapses without a - `ReminderDeliveryObserved` signal -- **THEN** the execution actor does NOT call `_client.AckAsync(envelope)` -- **AND** the execution actor tells `Context.Parent` a - `ReminderExecutionCompleted(success=false)` with a - "delivery not observed" error -- **AND** `OperationalAlert.ReminderExecutionFailed` is emitted -- **AND** `Aaron.Akka.Reminders` redelivers the envelope on next fire +### Requirement: Envelope-ack-gated at-least-once delivery + +The reminder manager SHALL retain each Akka.Reminders envelope until the attempt +has a known outcome. This rule SHALL apply to every delivery kind, so `Channel` +and `None` reminders no longer get an eager acknowledgement. + +The execution actor SHALL report its outcome to the manager. It SHALL wait for +`ReminderExecutionAccepted` before it stops. + +The manager SHALL acknowledge only a successful execution with all required +delivery evidence. It SHALL negatively acknowledge a known failure. + +A `CurrentSession` reminder SHALL still use the origin gateway and +`Ask`. Required delivery SHALL also wait for +`ReminderDeliveryResult`, which reports success and failure explicitly. + +The target session SHALL keep its best-effort reminder key check. The key SHALL +use the stable occurrence due time. + +#### Scenario: CurrentSession requires observed delivery + +- **GIVEN** a `CurrentSession` reminder has `DeliveryRequired = true` +- **WHEN** the target session returns `CommandAck` +- **THEN** the execution remains incomplete +- **WHEN** a matching successful `ReminderDeliveryResult` arrives +- **THEN** the child reports success to the manager +- **AND** the manager acknowledges the occurrence + +#### Scenario: CurrentSession delivery fails + +- **GIVEN** a `CurrentSession` reminder awaits required delivery +- **WHEN** the gateway rejects the turn or delivery fails +- **THEN** the child reports a descriptive failure +- **AND** the manager sends a negative acknowledgement #### Scenario: CurrentSession with DeliveryRequired=false acks on CommandAck alone -- **GIVEN** a `CurrentSession` reminder with `DeliveryRequired = false` - fires +- **GIVEN** a `CurrentSession` reminder with `DeliveryRequired = false` fires - **WHEN** `CommandAck` is received from the session -- **THEN** the execution actor immediately calls - `await _client.AckAsync(envelope)` -- **AND** tells `Context.Parent` a - `ReminderExecutionCompleted(success=true)` -- **AND** no `ReminderDeliveryObserved` signal wait is attempted - -#### Scenario: Session Ask-timeout triggers Akka.Reminders redelivery - -- **GIVEN** a `CurrentSession` reminder fires and the target channel - gateway has been dispatched a `DeliverTrustedSessionTurn` -- **AND** the pipeline or session fails to reply `CommandAck` within - `ReminderSettings.DefaultAckTimeout` -- **WHEN** the execution actor's `Ask` times out -- **THEN** the execution actor does NOT call `_client.AckAsync(envelope)` -- **AND** the execution actor tells `Context.Parent` a - `ReminderExecutionCompleted(success=false)` with a timeout error -- **AND** `Aaron.Akka.Reminders` marks the envelope as ack-timed-out - and redelivers it per its built-in `MaxDeliveryAttempts` default - -#### Scenario: Channel kind keeps eager envelope ack - -- **GIVEN** a reminder with `Delivery.Kind = Channel` fires -- **WHEN** `ReminderManagerActor.HandleReminderFiredAsync` runs -- **THEN** the manager calls `_client.AckAsync(envelope)` after - spawning the execution actor -- **AND** execution-success/failure tracking flows through - `ReminderExecutionCompleted` and - `OperationalAlert.ReminderExecutionFailed` +- **THEN** the child reports success to the manager +- **AND** no `ReminderDeliveryResult` wait is attempted + +#### Scenario: Channel execution fails + +- **GIVEN** a `Channel` reminder starts an isolated execution +- **WHEN** the execution or notification fails +- **THEN** the manager does not acknowledge success +- **AND** the manager sends a negative acknowledgement + +#### Scenario: None delivery succeeds + +- **GIVEN** a reminder uses `Delivery.Kind = None` +- **WHEN** its execution completes successfully +- **THEN** the manager acknowledges the occurrence + +#### Scenario: The child reports success before it stops + +- **GIVEN** an execution child reports success +- **WHEN** the manager saves local state and acknowledges the occurrence +- **THEN** the manager sends `ReminderExecutionAccepted` +- **AND** the child stops after that message #### Scenario: Redelivered CurrentSession reminder is deduped on the target session -- **GIVEN** a `CurrentSession` reminder was previously processed by - the session (evidenced by a `TurnRecorded` event whose - `SourceReminderId` matches the reminder's - `{reminderId}:{fireTimestampMs}` and is present in - `ProcessedReminderIds`) -- **WHEN** Akka.Reminders redelivers the same envelope after a - transient failure -- **THEN** the session dedup pre-check fires in - `HandleIncomingUserMessage` and `TryReplyAck()` returns `CommandAck` - without re-processing the turn -- **AND** `ReminderDeliveryObserved` fires because the prior turn's - outbound reply replay produces an observable delivery signal OR - the execution actor treats the dedup-ack path as observed for - acking purposes (implementation detail documented in design.md) -- **AND** the execution actor calls `_client.AckAsync(envelope)` once, - closing out the redelivery loop +- **GIVEN** a `CurrentSession` reminder was previously processed by the session, + evidenced by a `TurnRecorded` event whose `SourceReminderId` matches the + reminder's `{reminderId}:{fireTimestampMs}` key and is present in + `ProcessedReminderIds` +- **WHEN** Akka.Reminders redelivers the same envelope after a transient failure +- **THEN** the session dedup pre-check fires and `TryReplyAck()` returns + `CommandAck` without re-processing the turn +- **AND** the manager settles the occurrence once, closing the redelivery loop ### Requirement: Reminder delivery guarantees -The Mode B reminder delivery pipeline SHALL provide at-least-once -guarantees from the Akka.Reminders envelope down to the target session's -in-memory `CommandAck` boundary, with an explicitly accepted gap between -session-ack and turn-persist that is subsumed by future work. - -**Guaranteed windows** (at-least-once, dedup-safe or redelivery-safe): - -1. Crash before the channel gateway receives - `DeliverTrustedSessionTurn`: envelope un-acked, Akka.Reminders - redelivers on next fire. -2. Crash between the gateway's `OfferAsync` and the pipeline stream - stage processing the `ChannelInput`: the Ask temp actor never - receives a reply, execution actor's `Ask` times out without calling - `AckAsync`, envelope un-acked, Akka.Reminders redelivers. -3. Crash after session received the message (in-memory state updated) - but before execution actor calls `_client.AckAsync(envelope)`: the - envelope is still un-acked, Akka.Reminders redelivers. On - redelivery, if `TurnRecorded` already persisted, the session's - `ProcessedReminderIds` dedup catches it (best-effort); if not, the - redelivery is processed as a fresh turn (desired retry). -4. Ack message lost in flight between execution actor and the - Akka.Reminders scheduler proxy: Akka.Reminders redelivers on - `AckTimeout`, session dedup likely catches the duplicate. - -**Explicitly NOT guaranteed (accepted tradeoffs)**: - -- **Crash after `_client.AckAsync(envelope)` succeeds but before the - session's LLM turn completes and `TurnRecorded` is persisted.** In - this window the envelope has been acknowledged from Akka.Reminders' - perspective (the scheduler will not redeliver it) but the session - only reached in-memory state and did not write a durable record. On - restart, the reminder is lost. This window spans the entire LLM turn - execution, potentially minutes for tool-heavy reasoning. **This is - the identical failure mode every regular `SendUserMessage` has today** - — Mode B reminders do not introduce a new failure class. Closing - this gap requires a durable ingress queue on `LlmSessionActor`, which - is session-wide work deferred to the drain-on-shutdown follow-up - (issues #403, #419). - -- **Duplicate reminder processing across snapshot recovery boundaries.** - If `LlmSessionActor` is recovered from a snapshot rather than - replaying the full journal, the `ProcessedReminderIds` dedup set - starts empty. A redelivery of a pre-snapshot reminder would then be - processed as a fresh turn. In practice this requires the reminder to - still be within Akka.Reminders' `MaxDeliveryWindow` after a snapshot - has been taken — a narrow timing window. **Accepted tradeoff**: the - LLM itself typically recognizes a duplicate prompt in its recent - context and responds appropriately. Persisting the dedup set to - snapshot was not worth the complexity. - -Operators who need stronger guarantees should track the -drain-on-shutdown follow-up. - -#### Scenario: Crash before gateway offer is safe - -- **GIVEN** a Mode B reminder fires -- **WHEN** the daemon crashes before the channel gateway's - `DeliverTrustedSessionTurn` handler completes its `OfferAsync` -- **THEN** the envelope is un-acked -- **AND** on daemon restart, Akka.Reminders redelivers the envelope -- **AND** the reminder is processed normally - -#### Scenario: Crash between gateway offer and stream stage is safe - -- **GIVEN** a Mode B reminder fires and the channel gateway has - successfully offered a `ChannelInput` to the pipeline queue -- **WHEN** the daemon crashes before the pipeline stream stage processes - the `ChannelInput` and reaches the session actor -- **THEN** the execution actor's `Ask` times out -- **AND** `_client.AckAsync(envelope)` is not called -- **AND** the envelope is un-acked -- **AND** on daemon restart, Akka.Reminders redelivers and the reminder - is processed normally - -#### Scenario: Crash between session in-memory receipt and AckAsync is safe - -- **GIVEN** the session's `HandleIncomingUserMessage` has updated - in-memory state and fired `TryReplyAck()`, but the `CommandAck` has - not yet been processed by the execution actor's Ask -- **WHEN** the daemon crashes before `_client.AckAsync(envelope)` is - called -- **THEN** the envelope is un-acked -- **AND** on daemon restart, Akka.Reminders redelivers -- **AND** if `TurnRecorded` was already persisted by the session before - the crash, the dedup pre-check catches the redelivery (best-effort) -- **AND** if `TurnRecorded` was NOT yet persisted, the redelivered - reminder is processed as a fresh turn (desired retry) - -#### Scenario: Crash after AckAsync but before TurnRecorded loses the reminder (accepted gap) - -- **GIVEN** the execution actor has called - `_client.AckAsync(envelope)` successfully and received a - `ReminderAckResponse(Success)` -- **AND** the session has begun processing the turn but has not yet - persisted `TurnRecorded` -- **WHEN** the daemon crashes -- **THEN** the envelope is acked from Akka.Reminders' perspective and - is NOT redelivered on restart -- **AND** the session recovery replays its journal but finds no - `TurnRecorded` for this reminder -- **AND** the reminder turn is lost -- **AND** this outcome is documented as an explicit accepted tradeoff, - identical to the failure mode every regular `SendUserMessage` has - today, subsumed by the drain-on-shutdown follow-up (issues #403, #419) +The reminder pipeline SHALL provide at-least-once attempt delivery until the +manager confirms execution and required delivery success. + +A crash before acknowledgement SHALL leave the occurrence eligible for retry. A +crash after acknowledgement SHALL not lose successful work. + +The stable occurrence identity and the session reminder key SHALL reduce +duplicate work. Netclaw SHALL not claim exactly-once delivery. + +#### Scenario: The daemon stops during execution + +- **GIVEN** a reminder attempt has not reached manager acknowledgement +- **WHEN** the daemon stops +- **THEN** the acknowledgement lease expires +- **AND** Akka.Reminders can retry the occurrence + +#### Scenario: The daemon stops after acknowledgement + +- **GIVEN** execution and required delivery succeeded +- **AND** the manager acknowledged the occurrence +- **WHEN** the daemon stops before one-shot terminal state is saved +- **THEN** durable occurrence status remains `Delivered` +- **AND** reconciliation repairs the one-shot terminal state #### Scenario: Duplicate across snapshot recovery is accepted -- **GIVEN** a Mode B reminder was processed and `TurnRecorded` - persisted +- **GIVEN** a `CurrentSession` reminder was processed and `TurnRecorded` persisted - **AND** a subsequent `SessionSnapshot` was taken -- **AND** the session later recovers from that snapshot (journal - replay skips events before the snapshot) -- **AND** a redelivery of the original reminder arrives via - Akka.Reminders (the envelope was within `MaxDeliveryWindow`) +- **AND** the session later recovers from that snapshot, so journal replay skips + events before the snapshot +- **AND** a redelivery of the original reminder arrives via Akka.Reminders - **WHEN** the dedup pre-check runs -- **THEN** the set is empty (not populated from the snapshot) and the - redelivery is processed as a fresh turn -- **AND** the LLM may observe the duplicate in its transcript context - and respond appropriately -- **AND** this outcome is documented as an explicit accepted tradeoff +- **THEN** the set is empty and the redelivery is processed as a fresh turn +- **AND** this outcome is an explicit accepted tradeoff #### Scenario: Delivery guarantees documented in reminder-set confirmation -- **GIVEN** a Mode B reminder is successfully set +- **GIVEN** a `CurrentSession` reminder is successfully set - **WHEN** the tool returns its success message -- **THEN** the message conveys that the reminder will fire and deliver - a new turn to the originating session +- **THEN** the message conveys that the reminder will fire and deliver a new turn + to the originating session ### Requirement: Recurring reminder expiration @@ -1022,3 +857,166 @@ error enumerating the registered transports. - **THEN** the tool returns an error naming `"discord"` as unknown and listing `["slack"]` as the registered transports - **AND** no reminder is persisted + +### Requirement: Execution outcome controls occurrence acknowledgement + +Netclaw SHALL pass the Akka.Reminders envelope to every reminder execution. Netclaw SHALL acknowledge an occurrence only after successful execution and required delivery. + +Netclaw SHALL send a negative acknowledgement after a known execution or delivery failure. The negative acknowledgement SHALL use the library retry budget. + +The reminder manager SHALL accept the execution result before the child stops. DeathWatch SHALL report failure only before result acceptance. + +#### Scenario: Channel execution fails before delivery + +- **GIVEN** an enabled channel reminder occurrence is awaiting acknowledgement +- **WHEN** its session fails before required delivery succeeds +- **THEN** Netclaw sends a negative acknowledgement with the failure reason +- **AND** Netclaw does not send a successful acknowledgement +- **AND** Akka.Reminders persists the next attempt or a terminal state + +#### Scenario: Execution and required delivery succeed + +- **GIVEN** an enabled reminder occurrence is awaiting acknowledgement +- **WHEN** execution and required delivery succeed +- **THEN** Netclaw acknowledges the exact occurrence +- **AND** Akka.Reminders records `Delivered` + +### Requirement: Reminder-level poison state is durable + +Netclaw SHALL persist a consecutive execution failure count in the reminder definition. Each failed attempt SHALL increment the count, and a successful attempt SHALL reset it. + +Netclaw SHALL disable the complete reminder when the count reaches `FailurePauseThreshold`. This count SHALL remain separate from the Akka.Reminders per-occurrence attempt count. + +#### Scenario: Restart preserves the poison count + +- **GIVEN** a reminder has three consecutive failed attempts +- **WHEN** the daemon restarts +- **THEN** reminder status reports three consecutive failures +- **AND** the next failed attempt increments the count to four + +#### Scenario: Success resets the poison count + +- **GIVEN** a reminder has one or more consecutive failed attempts +- **WHEN** a later attempt succeeds +- **THEN** Netclaw persists a zero consecutive failure count + +#### Scenario: Fifth failure disables the complete reminder + +- **GIVEN** a reminder has four consecutive failed attempts +- **WHEN** the next attempt fails +- **THEN** Netclaw disables the reminder +- **AND** Netclaw records a failed terminal outcome +- **AND** Netclaw cancels future occurrences for the complete reminder + +### Requirement: One-shot reminders have one terminal settlement + +Netclaw SHALL settle each one-shot reminder exactly once. + +After a successful execution, Netclaw SHALL remove the one-shot definition and its execution history. + +When a one-shot reaches `FailurePauseThreshold`, Netclaw SHALL retain the definition, disable it, and record the `Failed` terminal outcome. Only an explicit delete command SHALL remove that retained definition and its history. + +Below that threshold, Netclaw SHALL keep a failed one-shot enabled so Akka.Reminders can retry it. + +#### Scenario: Successful one-shot is removed + +- **GIVEN** a one-shot reminder succeeds +- **WHEN** Netclaw completes its acknowledgement +- **THEN** Netclaw deletes the definition and its history file +- **AND** reconciliation removes any residual `Completed` one-shot + +#### Scenario: Failed one-shot remains enabled for retry + +- **GIVEN** a one-shot attempt fails below the poison threshold +- **WHEN** Akka.Reminders schedules another attempt +- **THEN** Netclaw keeps the definition enabled +- **AND** reminder status shows the durable attempt state + +#### Scenario: Poisoned one-shot remains inspectable + +- **GIVEN** a one-shot reaches `FailurePauseThreshold` +- **WHEN** Netclaw settles the final failed attempt +- **THEN** Netclaw disables the definition with outcome `Failed` +- **AND** an all-reminders query returns the definition + +#### Scenario: Reconciliation uses durable occurrence state + +- **GIVEN** a one-shot has a past fire time +- **WHEN** reconciliation finds no active schedule +- **THEN** reconciliation reads the durable occurrence state +- **AND** reconciliation selects restoration, a terminal soft delete, or removal of a delivered one-shot + +### Requirement: Reminder attempts have bounded acknowledgement leases + +Netclaw SHALL use a one-hour absolute execution limit and a 70-minute Akka.Reminders acknowledgment timeout. It SHALL retain the 20-minute inactivity limit. + +#### Scenario: Valid long execution completes within the lease + +- **GIVEN** a reminder execution produces activity and completes within one hour +- **WHEN** required delivery succeeds +- **THEN** Netclaw acknowledges the occurrence before its 70-minute deadline + +#### Scenario: Execution reaches the absolute limit + +- **GIVEN** a reminder execution remains active for one hour +- **WHEN** the absolute limit expires +- **THEN** Netclaw stops the attempt +- **AND** Netclaw sends a negative acknowledgement + +#### Scenario: The remaining lease cannot contain an attempt + +- **GIVEN** an occurrence has less than the maximum attempt duration plus the settlement margin remaining +- **WHEN** Netclaw considers the occurrence for execution +- **THEN** Netclaw does not start the execution +- **AND** Netclaw settles the occurrence by its one-shot or reminder-series blocked-occurrence policy + +### Requirement: Blocked occurrence settlement remains bounded + +An occurrence SHALL be blocked when another execution is already active for the same reminder, or when its remaining acknowledgement lease is shorter than the absolute execution limit plus the settlement margin. + +Netclaw SHALL NOT retain a blocked Akka.Reminders envelope in an in-memory catch-up queue. + +Netclaw SHALL negatively acknowledge a blocked one-shot occurrence. Netclaw SHALL acknowledge and skip a blocked reminder-series occurrence. + +Netclaw SHALL ignore an exact duplicate of the active occurrence. The active execution SHALL remain the sole settlement owner. + +#### Scenario: One-shot occurrence is blocked + +- **GIVEN** a one-shot occurrence cannot start because another execution is active or its remaining lease is too short +- **WHEN** the manager handles the occurrence +- **THEN** the manager sends a negative acknowledgement +- **AND** Akka.Reminders owns the retry delay + +#### Scenario: Reminder-series occurrence is blocked + +- **GIVEN** a reminder-series occurrence cannot start for the same reason +- **WHEN** the manager handles the occurrence +- **THEN** the manager acknowledges the occurrence without execution +- **AND** Netclaw does not retain the occurrence for catch-up work + +#### Scenario: Exact active occurrence arrives again + +- **GIVEN** an occurrence already has an active execution +- **WHEN** the same key, due time, and acknowledgement deadline arrive again +- **THEN** Netclaw does not start or settle the duplicate envelope +- **AND** the active execution remains the sole settlement owner + +### Requirement: Settlement write order supports recovery + +Netclaw SHALL save a failed run and its poison count before it sends a negative acknowledgement. Netclaw SHALL not advance Akka state after a local save failure. + +Netclaw SHALL save a successful run and reset the poison count before it sends an acknowledgement. Reconciliation SHALL repair one-shot terminal state after a post-acknowledgement process failure. + +#### Scenario: Local failure state cannot be saved + +- **GIVEN** an execution attempt fails +- **WHEN** Netclaw cannot save its poison state +- **THEN** Netclaw does not send a negative acknowledgement +- **AND** the Akka.Reminders acknowledgement timeout remains the recovery path + +#### Scenario: Process stops after successful acknowledgement + +- **GIVEN** Netclaw acknowledges a successful one-shot +- **WHEN** the process stops before it saves the terminal outcome +- **THEN** reconciliation reads the durable delivered state +- **AND** reconciliation records the completed removal diff --git a/openspec/specs/netclaw-subagents/spec.md b/openspec/specs/netclaw-subagents/spec.md index aabf44103..7ebaa1d69 100644 --- a/openspec/specs/netclaw-subagents/spec.md +++ b/openspec/specs/netclaw-subagents/spec.md @@ -459,3 +459,51 @@ Each subagent SHALL own an ephemeral working context initialized by forking a re - **WHEN** the parent handles the failure result - **THEN** the outcome contains no mergeable working-context delta - **AND** no child file metadata is merged into parent durable working context + +### Requirement: Managed server-feed sub-agent discovery + +The system SHALL load sub-agent definitions from user-authored top-level files under `~/.netclaw/agents/*.md` and from managed server-feed files under `~/.netclaw/agents/.server-feeds//*.md`. User-authored top-level sub-agents SHALL take precedence over managed server-feed sub-agents with the same logical name. Shadowed managed sub-agents SHALL NOT be exposed through sub-agent discovery, `spawn_agent`, or routed skill execution. + +#### Scenario: Managed sub-agent is loaded when no local conflict exists + +- **GIVEN** `~/.netclaw/agents/.server-feeds/team/code-reviewer.md` declares `name: code-reviewer` +- **AND** no top-level local sub-agent declares `name: code-reviewer` +- **WHEN** the sub-agent loader refreshes definitions +- **THEN** `code-reviewer` is registered as an available sub-agent according to its frontmatter visibility + +#### Scenario: Local sub-agent shadows managed sub-agent + +- **GIVEN** `~/.netclaw/agents/code-reviewer.md` declares `name: code-reviewer` +- **AND** `~/.netclaw/agents/.server-feeds/team/code-reviewer.md` also declares `name: code-reviewer` +- **WHEN** the sub-agent loader refreshes definitions +- **THEN** the top-level local `code-reviewer` definition is registered +- **AND** the managed feed `code-reviewer` definition is skipped +- **AND** NetClaw emits a diagnostic identifying the shadowed managed definition + +#### Scenario: Shadowed managed sub-agent cannot be spawned by routed skill + +- **GIVEN** a top-level local sub-agent shadows a managed server-feed sub-agent with the same name +- **WHEN** a skill routes execution through `metadata.subagent` using that name +- **THEN** routed execution resolves to the registered local sub-agent definition +- **AND** the shadowed managed definition is not used + +#### Scenario: Managed feed conflicts are deterministic + +- **GIVEN** two configured server feeds both provide a managed sub-agent named `reviewer` +- **WHEN** the sub-agent loader refreshes definitions +- **THEN** NetClaw registers only one `reviewer` definition using deterministic configured feed order +- **AND** skips later managed duplicates with diagnostics + +#### Scenario: Managed file changes refresh the registry + +- **GIVEN** a managed sub-agent file changes under `~/.netclaw/agents/.server-feeds/team/` +- **WHEN** the sub-agent loader checks for changes +- **THEN** the loader detects the managed file change +- **AND** refreshes the sub-agent registry snapshot + +#### Scenario: Missing managed namespace does not block local loading + +- **GIVEN** `~/.netclaw/agents/.server-feeds/` does not exist +- **AND** top-level local sub-agent files exist under `~/.netclaw/agents/` +- **WHEN** the sub-agent loader refreshes definitions +- **THEN** local sub-agent loading continues without requiring the managed namespace to exist diff --git a/openspec/specs/netclaw-testing/spec.md b/openspec/specs/netclaw-testing/spec.md index 8c493a0c5..7530de532 100644 --- a/openspec/specs/netclaw-testing/spec.md +++ b/openspec/specs/netclaw-testing/spec.md @@ -89,3 +89,21 @@ The test suite SHALL prove that admitted authority is required, parallel calls d - **WHEN** the earlier result completes after the later turn becomes active - **THEN** the earlier result is discarded without sleeps - **AND** only the correlated result can affect the active prompt + +### Requirement: Container upgrade compatibility proof + +The smoke suite SHALL verify upgrade from the latest stable Netclaw container to a locally built image using only an isolated temporary configuration volume. + +#### Scenario: Stable-to-local upgrade + +- **GIVEN** the latest stable image has written or consumed a legacy config in a disposable volume +- **WHEN** a uniquely tagged local image starts against the same volume +- **THEN** the new image SHALL become healthy without modifying the file on startup +- **AND** an explicit migration SHALL preserve effective role and capability values +- **AND** switching away from and back to a definition SHALL preserve its overrides + +#### Scenario: Production state isolation + +- **WHEN** the upgrade smoke runs +- **THEN** it SHALL use a newly created absolute temporary directory or uniquely named test volume +- **AND** it SHALL NOT mount or inspect the default or operator-provided Netclaw home diff --git a/openspec/specs/reminder-execution-history/spec.md b/openspec/specs/reminder-execution-history/spec.md index 66d5a50ed..47d2f95e5 100644 --- a/openspec/specs/reminder-execution-history/spec.md +++ b/openspec/specs/reminder-execution-history/spec.md @@ -110,3 +110,28 @@ persisted trust context. audience and boundary - **WHEN** the reminder store deserializes it - **THEN** the audience and boundary are read verbatim with no error logged + +### Requirement: Soft deletion retains reminder history + +Netclaw SHALL retain execution history when it soft-deletes a one-shot that reached its poison threshold. Only an explicit delete command SHALL remove that history file. + +A successful one-shot is not soft-deleted: Netclaw removes its definition and its history file together, so no orphaned history remains. + +#### Scenario: Completed one-shot removes its history with its definition + +- **GIVEN** a one-shot has a successful execution record +- **WHEN** Netclaw settles it as complete +- **THEN** Netclaw deletes the definition and the history file together + +#### Scenario: Failed one-shot retains history + +- **GIVEN** a one-shot reaches its poison threshold +- **WHEN** Netclaw disables it with outcome `Failed` +- **THEN** all failure records remain available through reminder history + +#### Scenario: Execution actor stops before it reports an outcome + +- **GIVEN** a reminder execution actor stops before manager acceptance +- **WHEN** DeathWatch reports the stop +- **THEN** the manager appends a failed execution record +- **AND** the failure record identifies the unexpected stop diff --git a/openspec/specs/session-cwd/spec.md b/openspec/specs/session-cwd/spec.md index d1a1d4dea..b35ad0d84 100644 --- a/openspec/specs/session-cwd/spec.md +++ b/openspec/specs/session-cwd/spec.md @@ -305,3 +305,46 @@ echoing the authored path. - **THEN** the tool returns an error without the authored path - **AND** the project scope remains unchanged - **AND** project instructions are not loaded from that path + +### Requirement: Subagent context announces private session scratch + +Before the first model call, the system SHALL include the exact bound `session_dir` in Personal and Team subagent working context and SHALL identify it as private scratch for disposable artifacts. The guidance SHALL preserve an explicitly required platform temporary path. Public subagent context SHALL NOT include the private session path. + +The context SHALL be derived from the child run's existing bound session scope. It SHALL NOT add a public protocol field, persist the path as agent identity, create a second scratch directory, or change shell authorization. + +#### Scenario: Personal child receives exact scratch path + +- **GIVEN** a Personal subagent has bound session directory `/home/user/.netclaw/sessions/example` +- **WHEN** Netclaw assembles its initial model context +- **THEN** the context contains `session_dir: /home/user/.netclaw/sessions/example` +- **AND** it identifies `session_dir` as the location for disposable artifacts +- **AND** it does not imply that the directory grants shell authority + +#### Scenario: Team child receives exact scratch path + +- **GIVEN** a Team subagent has a valid bound session directory +- **WHEN** Netclaw assembles its initial model context +- **THEN** the context contains that exact directory as private scratch +- **AND** existing Team tool and shell policy remains unchanged + +#### Scenario: Public child retains path redaction + +- **GIVEN** a Public subagent has an internal bound session directory +- **WHEN** Netclaw assembles its initial model context +- **THEN** the context does not contain that directory +- **AND** no scratch guidance discloses another private filesystem path + +#### Scenario: Explicit platform temporary requirement is preserved + +- **GIVEN** a Personal or Team subagent receives scratch guidance +- **WHEN** its task explicitly requires `/tmp` or the native Windows temporary directory +- **THEN** the guidance tells the child to preserve that requirement +- **AND** Netclaw does not rewrite the path or grant authority to it + +#### Scenario: Project declaration does not replace session scratch + +- **GIVEN** a child has received its initial session scratch context +- **WHEN** it later calls `set_working_directory` successfully +- **THEN** its project scope and project instructions update through the existing contract +- **AND** its bound `session_dir` remains unchanged + diff --git a/openspec/specs/shell-policy-evaluator-architecture/spec.md b/openspec/specs/shell-policy-evaluator-architecture/spec.md new file mode 100644 index 000000000..7ae879ccf --- /dev/null +++ b/openspec/specs/shell-policy-evaluator-architecture/spec.md @@ -0,0 +1,239 @@ +# shell-policy-evaluator-architecture Specification + +## Purpose + +Define the internal structure of the shell authorization evaluator. One +call-local state holds candidates, coverage, grant evidence, and trace facts. +The policy phases run in one fixed order and fail closed. This capability keeps +the evaluator auditable and holds its observable decisions constant across +refactor work. + +## Requirements + +### Requirement: Shell policy uses one explicit evaluation state + +The system SHALL use one call-local shell policy state for projected candidates, coverage, grant evidence, and trace facts. + +The state SHALL preserve candidate identity and order for the complete authorization call. No state instance SHALL cross an actor, persistence, or session boundary. + +#### Scenario: Partial coverage composes in one state + +- **WHEN** a call has candidates covered by a session grant, reviewed-safe policy, and one-time authority +- **THEN** one evaluation state SHALL record each distinct coverage source +- **AND** the final call SHALL allow only after every candidate has coverage + +#### Scenario: Candidate identity cannot change + +- **WHEN** any policy phase observes a candidate ID or fact that differs from projection +- **THEN** policy SHALL deny with `internal_policy_failure` +- **AND** no later phase SHALL apply authority + +#### Scenario: Allowed analysis reaches execution + +- **WHEN** shell policy allows a stream or non-stream execution +- **THEN** the executor SHALL receive the exact analysis that policy authorized +- **AND** no analysis SHALL pass through a context cache + +#### Scenario: Preflight allows without asynchronous completion + +- **WHEN** preflight allows a parsed shell call through Auto mode or another terminal rule +- **THEN** its terminal result SHALL carry the exact authorized analysis +- **AND** execution SHALL not parse the command again + +#### Scenario: Authorization does not execute + +- **WHEN** a caller requests authorization without execution +- **THEN** policy SHALL return the current decision +- **AND** policy SHALL retain no analysis for a later call + +### Requirement: Shell policy phases have one fixed order + +The system SHALL execute synchronous preflight and asynchronous completion phases in the documented order. A terminal decision SHALL stop all later policy phases. + +#### Scenario: Protected path precedes grant and safe policy + +- **WHEN** a candidate references a protected real or fallback path +- **THEN** policy SHALL deny before actor grant coverage or reviewed-safe coverage applies + +#### Scenario: Persistent-store failure follows available authority + +- **WHEN** one-time or session authority covers every candidate and persistent state is unavailable +- **THEN** policy SHALL preserve the current allow result +- **AND** policy SHALL deny when an uncovered candidate still depends on persistent state + +#### Scenario: Invalid call-local invariant fails closed + +- **WHEN** evaluation observes an invalid enum, changed candidate, duplicate coverage, or impossible terminal decision +- **THEN** policy SHALL deny with `internal_policy_failure` +- **AND** policy SHALL not open an approval prompt + +### Requirement: Actor grant evidence has one validation boundary + +The system SHALL validate the complete actor result before any actor grant enters candidate coverage. + +Validation SHALL cover store status, candidate count, IDs, facts, grant source, scope, timestamps, near misses, and unavailable-store restrictions. + +#### Scenario: Valid mixed actor evidence + +- **WHEN** the actor returns canonical session and persistent evidence for distinct candidates +- **THEN** policy SHALL apply each coverage source to its exact candidate +- **AND** policy SHALL retain the current approval match order + +#### Scenario: Malformed actor evidence + +- **WHEN** actor evidence has a duplicate ID, mismatched phrase, impossible scope, invalid enum, or inconsistent store state +- **THEN** policy SHALL deny with `internal_policy_failure` +- **AND** no actor grant SHALL enter coverage + +### Requirement: Syntax facts and policy authority remain separate + +ShellSyntaxTree and the shell matcher SHALL provide syntax, occurrence, value, redirect, directory, and candidate facts. They SHALL NOT decide trust, audience, grant authority, or prompt options. + +Netclaw policy SHALL consume those facts without an executable-private command parser. Unknown policy-relevant facts SHALL retain their current strict outcome. + +#### Scenario: Parser facts reach path policy once + +- **WHEN** projection contains exact or finite filesystem facts +- **THEN** policy SHALL evaluate each fact through the current path rules +- **AND** later phases SHALL reuse the projected result without command-text scans + +#### Scenario: Path facts retain their policy meaning + +- **WHEN** projection captures a filesystem fact +- **THEN** it SHALL retain candidate identity, value origin, domain kind, redirect mode, and resolution base +- **AND** intent and fallback resolutions SHALL remain distinct +- **AND** facts from different candidates or bases SHALL NOT be flattened together + +#### Scenario: Unknown and invalid path values remain distinct + +- **WHEN** a policy-relevant path domain is unknown or unsupported +- **THEN** reviewed-safe coverage SHALL remain unavailable +- **AND** the unknown domain SHALL NOT become a protected-path match +- **WHEN** an exact or finite causal value cannot resolve against an intent or fallback scope +- **THEN** the causal protected-path phase SHALL retain its current deny outcome + +#### Scenario: Redirect facts preserve their exact boundary + +- **WHEN** projection captures a file redirect +- **THEN** it SHALL retain the redirect mode, completeness, domain kind, and resolution base +- **AND** causal reviewed-safe coverage SHALL still accept only an exact input redirect +- **AND** file-writing redirects SHALL remain ineligible for reviewed-safe coverage + +#### Scenario: Execution rechecks protected paths + +- **WHEN** authorization reuses projected path results +- **THEN** shell execution SHALL still re-evaluate current denied paths and symlink state before native process start + +#### Scenario: Executable-private argument remains outside policy + +- **WHEN** safety would require private grammar for one executable +- **THEN** Netclaw SHALL retain the current prompt or deny outcome +- **AND** production policy SHALL not add a command-name branch + +### Requirement: Prompt and one-time authority share one candidate context + +The system SHALL derive one prompt context from the current uncovered candidate set. Exact one-time authority and user prompts SHALL use that same context. + +#### Scenario: Safe and granted candidates leave the prompt + +- **WHEN** some candidates already have reviewed-safe or stored-grant coverage +- **THEN** the one-time key and prompt SHALL contain only uncovered candidates +- **AND** candidate order SHALL remain stable + +#### Scenario: Causal policy retains full context + +- **WHEN** causal intent requires prerequisite and consumer candidates as one unit +- **THEN** the one-time key and prompt SHALL retain the complete causal context + +#### Scenario: Exact retry precedes candidate coverage + +- **WHEN** syntax or causal eligibility would prompt before candidate coverage +- **AND** the exact one-time key matches the same complete approval context +- **THEN** policy SHALL allow with `OneTimeApproval` +- **AND** the trace SHALL contain only its completion row +- **AND** no other allow reason SHALL bypass candidate coverage + +### Requirement: Coverage and trace facts remain atomic + +Each coverage change SHALL add its bounded trace fact through the same state operation. Terminal completion SHALL add exactly one completion row. + +#### Scenario: Coverage trace parity + +- **WHEN** a candidate gains session, persistent, reviewed-safe, or one-time coverage +- **THEN** its trace row SHALL report the same coverage source and policy reason + +#### Scenario: Trace data remains redacted + +- **WHEN** any policy phase emits trace evidence +- **THEN** trace data SHALL exclude raw commands, arguments, paths, prompts, session values, and secrets + +### Requirement: Refactor preserves observable policy behavior + +The refactor SHALL preserve all current decisions, deny reasons, allow reasons, corrections, approval options, candidate order, actor request count, grant matches, and trace rows. + +#### Scenario: Exact fixture equivalence + +- **WHEN** the D-case, adversarial, and live regression fixtures execute after each slice +- **THEN** every expected outcome and ordered trace SHALL remain unchanged + +#### Scenario: Full matrix equivalence + +- **WHEN** the complete Bash, PowerShell 7, and Windows PowerShell 5.1 policy matrix executes +- **THEN** every current snapshot and expected result SHALL remain unchanged + +#### Scenario: Channel and headless neutrality + +- **WHEN** the same shell facts arrive from Slack, another interactive channel, a reminder, webhook, or subagent +- **THEN** current audience and interactive-capability rules SHALL remain unchanged + +### Requirement: Refactor reduces policy complexity + +The completed change SHALL reduce original-file lines and control-flow below their frozen baselines. The complete footprint SHALL reduce lines and control-flow below the post-corpus implementation. + +Complete-footprint control-flow SHALL also remain below its frozen baseline. The final evidence SHALL report method complexity, coverage risk, and any residual line delta above that baseline. + +It SHALL not add a public API, durable schema, command parser, or duplicate policy scan. + +#### Scenario: Final complexity audit + +- **WHEN** all refactor slices are complete +- **THEN** the task evidence SHALL report before and after production line and control-flow counts +- **AND** the after counts SHALL be lower than 5,136 lines and 373 control-flow lines +- **AND** the complete footprint SHALL be lower than 10,085 lines and 663 control-flow lines +- **AND** complete-footprint control-flow SHALL be lower than the frozen 635-line baseline +- **AND** an added production file SHALL contribute zero lines to the baseline count +- **AND** each residual production layer above the frozen line baseline SHALL map to a tested security distinction, exact behavior contract, or recorded compatibility obligation +- **AND** the evidence SHALL report method complexity, coverage, and CRAP risk with a versioned command + +#### Scenario: Safe reduction boundary + +- **WHEN** another deletion would erase a tested path, evidence, authority, trace, or compatibility distinction +- **THEN** the task evidence SHALL identify that distinction and its owning tests or follow-up +- **AND** the implementation SHALL NOT compress or merge the distinction only to satisfy a line target + +#### Scenario: Public and durable compatibility + +- **WHEN** the final API and persistence audits run +- **THEN** public APIs, approval entries, actor events, snapshots, session history, and configuration SHALL remain compatible + +#### Scenario: Public compatibility service remains bounded + +- **WHEN** a shell caller uses the public compatibility service +- **THEN** one internal adapter SHALL preserve exact candidate facts +- **AND** new typed policy code SHALL not call the aggregate compatibility methods directly + +### Requirement: Internal failures remain fail-closed + +The system SHALL map unexpected internal faults to `internal_policy_failure`. Caller cancellation SHALL still propagate without conversion to a policy result. + +#### Scenario: Evaluation throws an internal exception + +- **WHEN** a policy phase throws outside caller cancellation +- **THEN** the call SHALL deny with `internal_policy_failure` +- **AND** the failure SHALL create no approval authority + +#### Scenario: Caller cancels evaluation + +- **WHEN** the caller cancellation token is canceled +- **THEN** evaluation SHALL propagate cancellation +- **AND** policy SHALL not emit a false allow, prompt, or deny result diff --git a/openspec/specs/skill-index-compression/spec.md b/openspec/specs/skill-index-compression/spec.md index 5f13f1265..8ebbe8067 100644 --- a/openspec/specs/skill-index-compression/spec.md +++ b/openspec/specs/skill-index-compression/spec.md @@ -32,20 +32,27 @@ The system SHALL generate a compressed skill index using a pipe-delimited format - **THEN** skills are grouped by their `Category` property - **AND** root-level skills appear under the `user` category -### Requirement: All skills visible in index +### Requirement: All authorized model-invocable skills visible in index -The system SHALL include all registered skills in the index regardless of physical origin. The only exclusion is skills with `disable-model-invocation: true`. +The system SHALL include every authorized model-invocable skill in the index regardless of source. It SHALL exclude skills with `DisableModelInvocation` and MCP prompt skills whose server is not allowed for the audience. -#### Scenario: All logical skills visible without origins +#### Scenario: All authorized logical skills visible without physical origins -- **GIVEN** accepted skills from system, native, server-feed, and external sources -- **WHEN** the index is generated +- **GIVEN** accepted skills from system, native, server-feed, external, and MCP prompt sources +- **WHEN** the index is generated for an authorized audience - **THEN** every model-invocable skill appears by logical name -- **AND** source names and physical paths are not required to use the skill +- **AND** source paths are not required to use the skill -#### Scenario: Skill without allowed-tools is always visible +#### Scenario: MCP prompt signature appears -- **GIVEN** a skill has no `allowed-tools` declared in frontmatter +- **GIVEN** an allowed MCP prompt has one required and one optional argument +- **WHEN** the index is generated +- **THEN** the prompt skill appears under its canonical logical name +- **AND** its compact argument hint distinguishes required and optional values + +#### Scenario: Skill without allowed-tools is visible + +- **GIVEN** an authorized skill has no `allowed-tools` metadata - **WHEN** the index is generated - **THEN** the skill appears in the index @@ -61,3 +68,4 @@ LLM does not see them in the skill list. - **WHEN** the compressed index is generated - **THEN** the skill does not appear in the index - **AND** the skill remains available via slash-command dispatch + diff --git a/openspec/specs/skill-tools/spec.md b/openspec/specs/skill-tools/spec.md index 65f74f0ba..e859285a3 100644 --- a/openspec/specs/skill-tools/spec.md +++ b/openspec/specs/skill-tools/spec.md @@ -154,15 +154,22 @@ refreshing the index from a partial set. ### Requirement: Logical model-facing skill access -For non-Public audiences with the skills subsystem enabled, normal model-initiated skill access SHALL use the registered logical skill name rather than a physical storage path. Inline skills SHALL return their instruction body through `skill_load`; skills declaring valid `metadata.subagent` routing SHALL execute through `skill_load` with a non-empty task; listed resources SHALL be read through `skill_read_resource` using the logical skill name and a safe relative resource path. +For non-Public audiences with the skills subsystem enabled, normal model-initiated skill access SHALL use the registered logical skill name rather than a physical storage path. File-backed inline skills SHALL return their instruction body through `skill_load`. MCP prompt skills SHALL render through `prompts/get` on their recorded server generation. Skills declaring valid `metadata.subagent` routing SHALL execute through `skill_load` with a non-empty task. Listed file resources SHALL be read through `skill_read_resource` using the logical skill name and a safe relative resource path. -#### Scenario: Inline skill loads by logical name +#### Scenario: File-backed inline skill loads by logical name -- **GIVEN** an inline skill accepted from any configured source +- **GIVEN** an inline file skill accepted from any configured file source - **WHEN** the model calls `skill_load` with its logical name -- **THEN** the runtime reads the registered `SkillEntry.FilePath` +- **THEN** the runtime reads the file source path - **AND** returns the skill instructions without requiring the model to know the physical origin +#### Scenario: MCP prompt skill loads by logical name + +- **GIVEN** an MCP prompt skill in the registered skill snapshot +- **WHEN** the model calls `skill_load` with its logical name and valid arguments +- **THEN** the runtime renders the prompt through its MCP source +- **AND** returns the attributed prompt instructions without a physical path + #### Scenario: Routed skill activates by logical name - **GIVEN** a skill with valid `metadata.subagent` @@ -172,9 +179,9 @@ For non-Public audiences with the skills subsystem enabled, normal model-initiat #### Scenario: Skill resource reads by logical name -- **GIVEN** a registered skill exposes `references/guide.md` +- **GIVEN** a registered file skill exposes `references/guide.md` - **WHEN** the model calls `skill_read_resource` with the logical skill name and `references/guide.md` -- **THEN** the runtime resolves the path beneath the registered `SkillEntry.SkillDirectory` +- **THEN** the runtime resolves the path beneath the registered file source directory - **AND** applies existing path traversal and audience protections #### Scenario: Explicit physical inspection remains available @@ -214,3 +221,36 @@ Every in-process skill inventory refresh SHALL resolve the current enabled nativ - **WHEN** the refreshed inventory replaces the previous inventory - **THEN** each reader observes either the complete previous snapshot or the complete new snapshot - **AND** no reader observes the registry between clear and repopulation + +### Requirement: skill_load MCP prompt arguments + +`skill_load` SHALL accept an optional string argument map for an MCP prompt skill. +It SHALL validate the map against the published prompt descriptor before `prompts/get`. + +#### Scenario: Required arguments pass unchanged + +- **GIVEN** an MCP prompt requires argument `property` +- **WHEN** the model loads the skill with `property: petabridge-com` +- **THEN** the adapter passes that value to `prompts/get` unchanged + +#### Scenario: Required argument is absent + +- **GIVEN** an MCP prompt requires argument `property` +- **WHEN** the model loads the skill without that key +- **THEN** `skill_load` returns a clear missing-argument error +- **AND** it does not call `prompts/get` + +#### Scenario: Unknown argument is present + +- **GIVEN** an MCP prompt declares no argument named `tenant` +- **WHEN** the model loads the skill with a `tenant` key +- **THEN** `skill_load` returns a clear unknown-argument error +- **AND** it does not call `prompts/get` + +#### Scenario: File skill receives prompt arguments + +- **GIVEN** a file-backed skill +- **WHEN** the model passes a non-empty prompt argument map to `skill_load` +- **THEN** the tool returns a clear source-mismatch error +- **AND** it does not load the file + diff --git a/openspec/specs/skillserver-native-sidecar-sync/spec.md b/openspec/specs/skillserver-native-sidecar-sync/spec.md new file mode 100644 index 000000000..d0a64d492 --- /dev/null +++ b/openspec/specs/skillserver-native-sidecar-sync/spec.md @@ -0,0 +1,127 @@ +# skillserver-native-sidecar-sync Specification + +## Purpose + +Define how Netclaw consumes a SkillServer feed's optional native +`/manifest.json` sidecar. The Cloudflare Agent Skills RFC index stays the +primary skill source. The sidecar adds native-only resources, such as sub-agent +definitions, into a feed-owned managed namespace. Sidecar failure is +non-destructive. + +## Requirements + +### Requirement: RFC skill sync remains primary with optional native sidecar + +For each enabled SkillServer feed, the system SHALL keep using the Cloudflare Agent Skills RFC index as the primary skill sync source. After a successful RFC index fetch for a feed, including a successful empty index, the system MAY feature-detect that feed's native `/manifest.json` sidecar for native-only resources. Native sidecar absence or failure SHALL NOT cause RFC skill sync for that feed to fail. + +#### Scenario: Feed without native sidecar still syncs skills + +- **GIVEN** an enabled server feed exposes a valid RFC skill index +- **AND** the feed does not expose `/manifest.json` +- **WHEN** server feed sync runs +- **THEN** NetClaw syncs skills from the RFC index using existing behavior +- **AND** logs or records that native sidecar sync was unavailable +- **AND** leaves existing managed sub-agent files for that feed unchanged + +#### Scenario: RFC fetch failure skips native sidecar sync + +- **GIVEN** an enabled server feed times out or fails while fetching the RFC skill index +- **WHEN** server feed sync runs +- **THEN** NetClaw does not attempt native sidecar sync for that feed +- **AND** keeps existing on-disk skills and managed sub-agents for that feed + +#### Scenario: Empty RFC index can still use native sidecar + +- **GIVEN** an enabled server feed returns a successful RFC index with zero skills +- **AND** the feed exposes a valid native sidecar with sub-agents +- **WHEN** server feed sync runs +- **THEN** NetClaw does not create or prune RFC skills for that empty index beyond existing safe behavior +- **AND** may sync native sub-agents from the sidecar + +### Requirement: Native sub-agent artifacts sync to a managed namespace + +When a feed's native sidecar advertises sub-agent resources, the system SHALL traverse native sub-agent versions, select the `agent-md` artifact, download it, verify its SHA-256 digest, and write it only to a NetClaw-owned managed path under `~/.netclaw/agents/.server-feeds//.md`. Server-provided manifest metadata SHALL NOT control local filesystem paths. + +#### Scenario: Verified sub-agent artifact is written atomically + +- **GIVEN** a native sidecar advertises sub-agent `code-reviewer` with an `agent-md` artifact and expected SHA-256 digest +- **AND** the downloaded artifact content hashes to the expected digest +- **AND** the artifact frontmatter declares `name: code-reviewer` +- **WHEN** native sidecar sync processes the sub-agent +- **THEN** NetClaw writes the file to `~/.netclaw/agents/.server-feeds//code-reviewer.md` +- **AND** replaces any previous managed file atomically +- **AND** records sync state for the managed sub-agent + +#### Scenario: Server path metadata is ignored + +- **GIVEN** a native sidecar artifact includes metadata that resembles an absolute path or relative traversal path +- **WHEN** native sidecar sync processes the artifact +- **THEN** NetClaw ignores that metadata for local storage +- **AND** derives the managed target path only from the configured feed name and validated sub-agent name + +#### Scenario: Digest mismatch keeps previous managed file + +- **GIVEN** a native sidecar advertises sub-agent `code-reviewer` +- **AND** a previous managed file already exists for `code-reviewer` +- **WHEN** the downloaded artifact hash does not match the expected SHA-256 digest +- **THEN** NetClaw rejects the downloaded artifact +- **AND** keeps the previous managed file unchanged +- **AND** treats the feed's native sidecar sync as partial for pruning purposes + +#### Scenario: Artifact name mismatch is rejected + +- **GIVEN** a native sidecar advertises sub-agent `code-reviewer` +- **AND** the downloaded artifact frontmatter declares `name: other-agent` +- **WHEN** native sidecar sync processes the artifact +- **THEN** NetClaw rejects the downloaded artifact +- **AND** does not replace the managed `code-reviewer.md` file + +### Requirement: Native sidecar failure preserves managed sub-agents + +The system SHALL treat native sidecar failures as non-destructive. If native manifest fetch, traversal, artifact download, digest verification, parsing, validation, or managed write fails for a feed, NetClaw SHALL keep existing managed sub-agent files for that feed and SHALL NOT prune removed sub-agents during that sync attempt. + +#### Scenario: Malformed native manifest does not prune managed sub-agents + +- **GIVEN** managed sub-agent files already exist for a feed +- **WHEN** `/manifest.json` is present but malformed or unsupported +- **THEN** NetClaw logs the native sidecar failure +- **AND** leaves all managed sub-agent files for that feed unchanged +- **AND** does not prune managed sub-agent sync state for that feed + +#### Scenario: One failed artifact prevents pruning + +- **GIVEN** a native sidecar advertises sub-agents `alpha` and `beta` +- **AND** `alpha` downloads and verifies successfully +- **AND** `beta` fails download or verification +- **WHEN** native sidecar sync completes for the feed +- **THEN** NetClaw may keep the successfully synced `alpha` managed file +- **AND** keeps any previous managed `beta` file unchanged +- **AND** skips pruning for that feed because the sync was partial + +### Requirement: Managed sub-agent pruning is successful-sync only + +After a native sidecar sync for a feed completes successfully for all advertised sub-agents, the system SHALL remove only managed sub-agent files and sync-state entries for that same feed that are no longer advertised by the sidecar. The system SHALL NOT remove user-authored sub-agents or managed sub-agents belonging to other feeds. + +#### Scenario: Removed managed sub-agent is pruned after successful sync + +- **GIVEN** the managed namespace for feed `team` contains `old-agent.md` from a previous successful sync +- **AND** the feed's current native sidecar successfully syncs all advertised sub-agents +- **AND** the sidecar no longer advertises `old-agent` +- **WHEN** native sidecar sync completes +- **THEN** NetClaw removes `~/.netclaw/agents/.server-feeds/team/old-agent.md` +- **AND** removes `old-agent` from that feed's managed sub-agent sync state + +#### Scenario: User-authored local sub-agent is never pruned + +- **GIVEN** `~/.netclaw/agents/code-reviewer.md` exists as a user-authored local sub-agent +- **AND** a native sidecar sync for feed `team` completes successfully +- **WHEN** NetClaw prunes removed managed sub-agents for feed `team` +- **THEN** `~/.netclaw/agents/code-reviewer.md` remains unchanged +- **AND** pruning is limited to `~/.netclaw/agents/.server-feeds/team/` + +#### Scenario: Other feed managed sub-agent is never pruned + +- **GIVEN** `~/.netclaw/agents/.server-feeds/team-a/reviewer.md` exists +- **AND** `~/.netclaw/agents/.server-feeds/team-b/reviewer.md` exists +- **WHEN** native sidecar sync for feed `team-a` completes and prunes removed entries +- **THEN** NetClaw does not remove or modify files under `~/.netclaw/agents/.server-feeds/team-b/` diff --git a/openspec/specs/tool-approval-gates/spec.md b/openspec/specs/tool-approval-gates/spec.md index b81c13965..854da6eba 100644 --- a/openspec/specs/tool-approval-gates/spec.md +++ b/openspec/specs/tool-approval-gates/spec.md @@ -821,14 +821,12 @@ text in single-line code fences, and dumping a multi-line quoted blob verbatim corrupts the prompt layout. When the parser cannot decompose the command, line breaks SHALL be flattened to spaces. -Commands containing heredocs or subshell groupings SHALL NOT be -display-reconstructed: the parser drops heredoc bodies from the tree -(only the `<}` +- **AND** the entry has no `verb` member + +#### Scenario: Legacy shell grant has one form + +- **WHEN** Netclaw stores a global Bash legacy phrase `git push` +- **THEN** its entry equals + `{"shell":"Bash","match":"LegacyExact","verb":"git push","directory":null,"createdAt":}` +- **AND** the entry has no `verbTokens` member + +#### Scenario: Non-shell entry keeps its form + +- **WHEN** Netclaw stores a non-shell approval +- **THEN** the entry contains `verb`, `directory`, and `createdAt` +- **AND** the entry has no shell phrase member + +#### Scenario: Duplicate member fails closed + +- **GIVEN** a version-3 entry has two `match` members +- **WHEN** the daemon loads the store +- **THEN** the persistent store status is unavailable +- **AND** no entry from the file can authorize + +#### Scenario: Unknown audience fails closed + +- **GIVEN** a version-3 store has audience key `guest` +- **WHEN** the daemon loads the store +- **THEN** the persistent store status is unavailable + +#### Scenario: Spoof character fails closed + +- **GIVEN** a tool key, verb, token, or directory has a bidi control +- **WHEN** the daemon loads the store +- **THEN** the persistent store status is unavailable +- **AND** no entry from the file can authorize + +#### Scenario: Empty token array fails closed + +- **GIVEN** a token-prefix entry has an empty `verbTokens` array +- **WHEN** the daemon loads the store +- **THEN** the persistent store status is unavailable +- **AND** no entry from the file can authorize + +### Requirement: Exact-authority version 2 migration + +The system SHALL get the canonical native shell from its caller. It SHALL not +guess a shell. On the first valid version-2 load, it SHALL check the whole file +before a file-system change. + +The system SHALL convert each valid `shell_execute` entry to `LegacyExact` for +that shell. It SHALL keep the version-2 `verb` text exactly. It SHALL keep +`createdAt`, audience, and tool. It SHALL NOT add token-prefix authority. + +For a non-null v2 directory, conversion SHALL use `Path.GetFullPath`. It SHALL +preserve a canonical filesystem root, such as `/` or `C:\`. For another path, +it SHALL remove end separators as the current matcher does. The result SHALL be +nonempty and absolute. Conversion SHALL preserve significant path whitespace. +It SHALL never trim a path or map a non-null directory to global null. A null +v2 directory SHALL remain global null. + +The system SHALL keep a valid non-shell entry without shell members. It SHALL +omit a control phrase or a shell phrase with no safe representation. It SHALL +emit one bounded diagnostic count for all such omissions. + +A version-2 verb with whitespace at its start or end has no version-3 form. +The system SHALL omit it. It SHALL not trim it into new authority. + +Each store access SHALL use one exclusive cross-process lock. The lock SHALL +cover read, check, backup, write, replace, and cache update. It SHALL use a +bounded wait. A timeout SHALL make the store unavailable. + +The system SHALL reject a symbolic link at the active, lock, backup, or +temporary path. It SHALL create each new sibling file with exclusive access. +It SHALL compare the active source bytes again before replace. + +The system SHALL copy the source bytes to `.v2.bak` before replace. It SHALL +flush the temporary version-3 file. It SHALL then replace the active file on +the same file system. It SHALL not replace a prior backup with different bytes. + +A backup error SHALL leave the source in place. A replace error SHALL keep the +source and completed backup. Each error SHALL make the store unavailable for +that load. A later load MAY try again. + +#### Scenario: Plain version-2 shell phrase stays exact + +- **GIVEN** a version-2 Bash entry has `verb` equal to `git push` +- **WHEN** conversion succeeds +- **THEN** the version-3 entry uses `LegacyExact` +- **AND** it matches only `git push` +- **AND** it does not match `git push upstream` + +#### Scenario: Folder and time survive conversion + +- **GIVEN** a version-2 shell entry has an absolute directory and timestamp +- **WHEN** conversion succeeds +- **THEN** the legacy entry has the same normalized directory +- **AND** it has the same timestamp + +#### Scenario: Backup keeps the source bytes + +- **GIVEN** a valid version-2 approval file +- **WHEN** conversion succeeds +- **THEN** the `.v2.bak` bytes equal the original bytes +- **AND** the active file is valid version 3 + +#### Scenario: Different backup stops conversion + +- **GIVEN** `.v2.bak` exists with different bytes +- **WHEN** the system tries to convert version 2 +- **THEN** the store status is unavailable +- **AND** neither file changes + +#### Scenario: Other process changes the source + +- **GIVEN** a writer that does not use the lock changes the active source +- **WHEN** the source comparison runs before replace +- **THEN** replace does not occur +- **AND** the store status is unavailable + +#### Scenario: Bad version-2 file gives no authority + +- **GIVEN** one version-2 entry is structurally bad +- **WHEN** the daemon loads the store +- **THEN** conversion does not replace the source +- **AND** no entry from the file can authorize + +#### Scenario: Control phrase is not legacy authority + +- **GIVEN** a valid version-2 entry has a control in its phrase +- **WHEN** conversion succeeds for the rest of the file +- **THEN** that entry is absent from version 3 +- **AND** it is not `LegacyExact` + +#### Scenario: Padded phrase does not gain authority + +- **GIVEN** a version-2 entry has verb text ` git push` +- **WHEN** conversion succeeds for the rest of the file +- **THEN** that entry is absent from version 3 +- **AND** no `git push` authority is created + +#### Scenario: Path space does not widen folder authority + +- **GIVEN** a POSIX v2 directory is `/work ` with a final space +- **WHEN** conversion succeeds +- **THEN** the version-3 directory remains `/work ` +- **AND** it does not become `/work` + +#### Scenario: Empty directory does not become global + +- **GIVEN** a v2 entry has non-null empty directory text +- **WHEN** conversion succeeds for the rest of the file +- **THEN** that entry is absent from version 3 +- **AND** no global grant is created + +#### Scenario: POSIX root keeps root scope + +- **GIVEN** a v2 directory is `/` +- **WHEN** conversion succeeds +- **THEN** the version-3 directory is `/` +- **AND** it is not empty or global null + +#### Scenario: Windows drive root keeps root scope + +- **GIVEN** a v2 directory is `C:\` +- **WHEN** conversion succeeds on Windows +- **THEN** the version-3 directory is `C:\` +- **AND** it is not `C:` or global null + +### Requirement: Canonical trust-verb phrase creation + +For `shell_execute`, `trust-verb` SHALL use the selected ShellSyntaxTree parser. +It SHALL create `TokenPrefix` from one complete static command phrase. It SHALL +use the canonical verb tokens from the parser. It SHALL reject dynamic, +compound, or incomplete shell input. + +The one occurrence SHALL have no parser-classified argument, flag, assignment, +redirect, cwd effect, substitution, or control-flow effect. The input text +SHALL equal the canonical token phrase with one space between tokens. The CLI +SHALL not reduce extra authored text to a broader stored phrase. Netclaw SHALL +not reinterpret a parser-classified verb token through executable-private +grammar. + +For any other tool, `trust-verb` SHALL keep the compatible non-shell exact +entry. It SHALL support the current arbitrary `--tool` value. It SHALL not add +shell members to that entry. + +For an abstract PowerShell request, the parser SHALL try PowerShell 7 and +Windows PowerShell 5.1. It SHALL use a valid PowerShell 7 result first. It SHALL +use a valid Windows PowerShell 5.1 result only when the preferred result is +invalid. A resolved runtime environment SHALL use only its selected dialect. + +#### Scenario: Static shell phrase creates tokens + +- **WHEN** an operator trusts `git push` for `shell_execute` under Bash +- **THEN** the new entry has Bash token prefix `git`, `push` + +#### Scenario: PowerShell 7 valid result has preference + +- **GIVEN** PowerShell 7 accepts the exact canonical phrase +- **WHEN** an operator trusts the phrase under abstract PowerShell +- **THEN** the new entry uses the PowerShell 7 canonical tokens + +#### Scenario: Windows PowerShell result provides a fallback + +- **GIVEN** PowerShell 7 rejects the exact phrase +- **AND** Windows PowerShell 5.1 accepts the exact canonical phrase +- **WHEN** an operator trusts the phrase under abstract PowerShell +- **THEN** the new entry uses the Windows PowerShell 5.1 canonical tokens + +#### Scenario: Compound shell phrase is rejected + +- **WHEN** an operator trusts `git status; rm file` for `shell_execute` +- **THEN** the command exits with a user error +- **AND** the approval store does not change + +#### Scenario: Flag is not reduced to a phrase + +- **WHEN** an operator trusts `git push --force` for `shell_execute` +- **THEN** the command exits with a user error +- **AND** no `git push` grant is stored + +#### Scenario: Parser-owned phrase keeps every token + +- **WHEN** an operator trusts `git push origin` for `shell_execute` +- **AND** ShellSyntaxTree returns canonical tokens `git`, `push`, and `origin` +- **THEN** the stored token prefix has all three tokens +- **AND** no broader `git push` grant is stored + +#### Scenario: Redirect is not reduced to a phrase + +- **WHEN** an operator trusts `git push >out` for `shell_execute` +- **THEN** the command exits with a user error +- **AND** the approval store does not change + +#### Scenario: Assignment is not reduced to a phrase + +- **WHEN** an operator trusts `MODE=safe git push` for `shell_execute` +- **THEN** the command exits with a user error +- **AND** the approval store does not change + +#### Scenario: Non-shell tool stays exact + +- **WHEN** an operator trusts `create-page` for a non-shell tool +- **THEN** the new entry uses the non-shell exact form +- **AND** the entry has no `shell` member + +### Requirement: Version 3 recovery boundary + +The system SHALL treat an absent approval file as a ready empty store. It SHALL +treat malformed JSON and a partly bad version-3 file as unavailable. It SHALL +also reject a bad enum, bad token array, or future schema version. + +A future-version file SHALL stay byte-identical. The system SHALL NOT +quarantine it. The daemon and CLI SHALL not provide an automatic downgrade. + +The operator SHALL stop the daemon before manual recovery. The operator can +restore `.v2.bak` as the active file. The current daemon can convert it again. +A version-2 binary is outside this compatibility promise. + +#### Scenario: Absent store is ready and empty + +- **GIVEN** the approval file does not exist +- **WHEN** the daemon requests a persistent snapshot +- **THEN** the store status is ready +- **AND** the snapshot has no entries + +#### Scenario: Future store stays untouched + +- **GIVEN** the approval file declares a version greater than 3 +- **WHEN** the daemon or CLI tries to load it +- **THEN** the store status is unavailable +- **AND** the file stays byte-identical + +#### Scenario: Operator restores the backup + +- **GIVEN** version-3 conversion completed and `.v2.bak` exists +- **WHEN** an operator stops the daemon and restores the backup +- **THEN** the current daemon can convert that version-2 file again + +### Requirement: Bounded Bash stdin data has a constrained receiver grammar + +Netclaw SHALL treat Bash heredoc and here-string data as resolved only when all +required receiver and data facts are complete. The initial receiver grammar +SHALL accept only argument-free `cat`. It SHALL require a complete literal +heredoc or an exact or finite here-string target. + +The grammar SHALL use the heredoc expansion mode and authored body provenance. +It SHALL use `RedirectAnalysis.Target` for here strings. It SHALL reject +expanding heredocs, unknown domains, incomplete redirects, path-relevant +redirects, non-stdin source descriptors, authored arguments, receiver wrappers, +and every other receiver. A complete direct shell dispatch MAY expose its inner +receiver through Netclaw's established recursive analysis. + +Netclaw SHALL evaluate every other redirect on the occurrence independently. +Stored approval SHALL NOT bypass an unresolved stdin redirect. + +#### Scenario: Exact here string to cat can use the trusted scope + +- **GIVEN** an argument-free `cat` command in a trusted project directory +- **WHEN** its complete here string has exact data +- **THEN** the stdin redirect does not require a separate approval +- **AND** the normal safe-verb and path rules decide the command + +#### Scenario: Literal heredoc to cat can use the trusted scope + +- **GIVEN** an argument-free `cat` command in a trusted project directory +- **WHEN** its complete literal heredoc has complete authored body provenance +- **THEN** the stdin redirect does not require a separate approval +- **AND** the normal safe-verb and path rules decide the command + +#### Scenario: Unknown here-string data stays strict + +- **GIVEN** `cat <<< "$value"` in a trusted project directory +- **WHEN** the parser cannot prove the data value +- **THEN** Netclaw requires one-shot approval or deny +- **AND** Netclaw offers no persistent approval candidate + +#### Scenario: Interpreter stdin stays strict + +- **GIVEN** an interpreter receives a complete literal heredoc or here string +- **WHEN** Netclaw evaluates the redirect +- **THEN** Netclaw requires one-shot approval or deny +- **AND** an existing interpreter grant does not bypass the stdin decision + +### Requirement: Bash command-resolution mutation stays strict + +Netclaw SHALL use the pinned ShellSyntaxTree result as the structural authority. +An unparseable command-resolution mutation or reserved execution form SHALL +produce no persistent approval candidate. + +This rule SHALL cover unsupported `exec`, mutating `hash`, alias changes, +shell-option changes, builtin-enable changes, `time`, negation, coprocesses, and +current-shell brace groups. + +#### Scenario: Command-resolution mutation cannot reuse a grant + +- **GIVEN** a command changes command resolution before another occurrence +- **AND** stored grants cover each visible command name +- **WHEN** ShellSyntaxTree marks the full command unparseable +- **THEN** Netclaw requires one-shot approval or deny +- **AND** Netclaw offers no persistent approval candidate + +#### Scenario: Reserved execution form cannot flatten into a safe command + +- **GIVEN** an unsupported reserved execution form contains a safe verb +- **WHEN** ShellSyntaxTree marks the full command unparseable +- **THEN** Netclaw does not authorize the visible safe verb +- **AND** Netclaw offers no persistent approval candidate + From d201525c7cfe943465d20f495046cf8f2a4bac81 Mon Sep 17 00:00:00 2001 From: Aaron Stannard Date: Wed, 19 Aug 2026 19:02:53 -0500 Subject: [PATCH 2/2] Archive 16 completed OpenSpec changes Move each completed change to openspec/changes/archive/2026-08-19-/. Their code is merged on dev. Also tick task 5.3 of consolidate-binding-actor-engines. PR #2005 merged that work. Leave eval-run checkboxes unticked. An evals-only gap does not block the archive. --- .../2026-08-19-add-deepseek-provider}/.openspec.yaml | 0 .../2026-08-19-add-deepseek-provider}/design.md | 0 .../2026-08-19-add-deepseek-provider}/proposal.md | 0 .../specs/netclaw-model-providers/spec.md | 0 .../2026-08-19-add-deepseek-provider}/tasks.md | 0 .../2026-08-19-add-mcp-prompt-skills}/.openspec.yaml | 0 .../2026-08-19-add-mcp-prompt-skills}/design.md | 0 .../2026-08-19-add-mcp-prompt-skills}/proposal.md | 0 .../specs/audience-context-filtering/spec.md | 0 .../2026-08-19-add-mcp-prompt-skills}/specs/netclaw-mcp/spec.md | 0 .../specs/skill-index-compression/spec.md | 0 .../2026-08-19-add-mcp-prompt-skills}/specs/skill-tools/spec.md | 0 .../2026-08-19-add-mcp-prompt-skills}/tasks.md | 0 .../2026-08-19-add-timestamped-webhook-hmac}/.openspec.yaml | 0 .../2026-08-19-add-timestamped-webhook-hmac}/design.md | 0 .../2026-08-19-add-timestamped-webhook-hmac}/proposal.md | 0 .../specs/inbound-webhooks/spec.md | 0 .../2026-08-19-add-timestamped-webhook-hmac}/tasks.md | 0 .../2026-08-19-adopt-shellsyntax-alpha1}/.openspec.yaml | 0 .../2026-08-19-adopt-shellsyntax-alpha1}/design.md | 0 .../2026-08-19-adopt-shellsyntax-alpha1}/proposal.md | 0 .../specs/tool-approval-gates/spec.md | 0 .../2026-08-19-adopt-shellsyntax-alpha1}/tasks.md | 0 .../.openspec.yaml | 0 .../2026-08-19-consolidate-binding-actor-engines}/design.md | 0 .../2026-08-19-consolidate-binding-actor-engines}/proposal.md | 0 .../specs/channel-binding-parity/spec.md | 0 .../2026-08-19-consolidate-binding-actor-engines}/tasks.md | 2 +- .../.openspec.yaml | 0 .../design.md | 0 .../proposal.md | 0 .../specs/tool-approval-gates/spec.md | 0 .../2026-08-19-extend-live-approval-regression-corpus}/tasks.md | 0 .../.openspec.yaml | 0 .../2026-08-19-guide-subagents-to-session-scratch}/design.md | 0 .../2026-08-19-guide-subagents-to-session-scratch}/proposal.md | 0 .../specs/session-cwd/spec.md | 0 .../specs/tool-approval-gates/spec.md | 0 .../2026-08-19-guide-subagents-to-session-scratch}/tasks.md | 0 .../2026-08-19-lock-shell-approval-store-v3}/.openspec.yaml | 0 .../2026-08-19-lock-shell-approval-store-v3}/design.md | 0 .../2026-08-19-lock-shell-approval-store-v3}/proposal.md | 0 .../specs/netclaw-cli/spec.md | 0 .../specs/tool-approval-gates/spec.md | 0 .../2026-08-19-lock-shell-approval-store-v3}/tasks.md | 0 .../.openspec.yaml | 0 .../design.md | 0 .../proposal.md | 0 .../specs/named-model-definitions/spec.md | 0 .../specs/netclaw-cli/spec.md | 0 .../specs/netclaw-model-providers/spec.md | 0 .../specs/netclaw-testing/spec.md | 0 .../tasks.md | 0 .../.openspec.yaml | 0 .../2026-08-19-reject-incompatible-session-history}/design.md | 0 .../2026-08-19-reject-incompatible-session-history}/proposal.md | 0 .../specs/netclaw-model-capabilities/spec.md | 0 .../2026-08-19-reject-incompatible-session-history}/tasks.md | 0 .../2026-08-19-reliable-one-shot-reminder-retry}/.openspec.yaml | 0 .../2026-08-19-reliable-one-shot-reminder-retry}/design.md | 0 .../2026-08-19-reliable-one-shot-reminder-retry}/proposal.md | 0 .../specs/netclaw-scheduling/spec.md | 0 .../specs/reminder-execution-history/spec.md | 0 .../2026-08-19-reliable-one-shot-reminder-retry}/tasks.md | 0 .../2026-08-19-simplify-shell-policy-evaluator}/.openspec.yaml | 0 .../2026-08-19-simplify-shell-policy-evaluator}/design.md | 0 .../evidence/refactor-baseline.md | 0 .../evidence/refactor-reduction-revision.md | 0 .../2026-08-19-simplify-shell-policy-evaluator}/proposal.md | 0 .../specs/shell-policy-evaluator-architecture/spec.md | 0 .../2026-08-19-simplify-shell-policy-evaluator}/tasks.md | 0 .../2026-08-19-skillserver-native-sidecar-sync}/.openspec.yaml | 0 .../2026-08-19-skillserver-native-sidecar-sync}/design.md | 0 .../2026-08-19-skillserver-native-sidecar-sync}/proposal.md | 0 .../specs/netclaw-subagents/spec.md | 0 .../specs/skillserver-native-sidecar-sync/spec.md | 0 .../2026-08-19-skillserver-native-sidecar-sync}/tasks.md | 0 .../.openspec.yaml | 0 .../2026-08-19-subscribe-mcp-catalog-notifications}/design.md | 0 .../2026-08-19-subscribe-mcp-catalog-notifications}/proposal.md | 0 .../specs/netclaw-mcp/spec.md | 0 .../2026-08-19-subscribe-mcp-catalog-notifications}/tasks.md | 0 .../2026-08-19-systemd-daemon-path-capture}/.openspec.yaml | 0 .../2026-08-19-systemd-daemon-path-capture}/design.md | 0 .../2026-08-19-systemd-daemon-path-capture}/proposal.md | 0 .../specs/daemon-shell-path/spec.md | 0 .../2026-08-19-systemd-daemon-path-capture}/tasks.md | 0 .../2026-08-19-webhook-route-actor-ownership}/.openspec.yaml | 0 .../2026-08-19-webhook-route-actor-ownership}/design.md | 0 .../2026-08-19-webhook-route-actor-ownership}/proposal.md | 0 .../specs/webhook-route-authority/spec.md | 0 .../2026-08-19-webhook-route-actor-ownership}/tasks.md | 0 92 files changed, 1 insertion(+), 1 deletion(-) rename openspec/changes/{add-deepseek-provider => archive/2026-08-19-add-deepseek-provider}/.openspec.yaml (100%) rename openspec/changes/{add-deepseek-provider => archive/2026-08-19-add-deepseek-provider}/design.md (100%) rename openspec/changes/{add-deepseek-provider => archive/2026-08-19-add-deepseek-provider}/proposal.md (100%) rename openspec/changes/{add-deepseek-provider => archive/2026-08-19-add-deepseek-provider}/specs/netclaw-model-providers/spec.md (100%) rename openspec/changes/{add-deepseek-provider => archive/2026-08-19-add-deepseek-provider}/tasks.md (100%) rename openspec/changes/{add-mcp-prompt-skills => archive/2026-08-19-add-mcp-prompt-skills}/.openspec.yaml (100%) rename openspec/changes/{add-mcp-prompt-skills => archive/2026-08-19-add-mcp-prompt-skills}/design.md (100%) rename openspec/changes/{add-mcp-prompt-skills => archive/2026-08-19-add-mcp-prompt-skills}/proposal.md (100%) rename openspec/changes/{add-mcp-prompt-skills => archive/2026-08-19-add-mcp-prompt-skills}/specs/audience-context-filtering/spec.md (100%) rename openspec/changes/{add-mcp-prompt-skills => archive/2026-08-19-add-mcp-prompt-skills}/specs/netclaw-mcp/spec.md (100%) rename openspec/changes/{add-mcp-prompt-skills => archive/2026-08-19-add-mcp-prompt-skills}/specs/skill-index-compression/spec.md (100%) rename openspec/changes/{add-mcp-prompt-skills => archive/2026-08-19-add-mcp-prompt-skills}/specs/skill-tools/spec.md (100%) rename openspec/changes/{add-mcp-prompt-skills => archive/2026-08-19-add-mcp-prompt-skills}/tasks.md (100%) rename openspec/changes/{add-timestamped-webhook-hmac => archive/2026-08-19-add-timestamped-webhook-hmac}/.openspec.yaml (100%) rename openspec/changes/{add-timestamped-webhook-hmac => archive/2026-08-19-add-timestamped-webhook-hmac}/design.md (100%) rename openspec/changes/{add-timestamped-webhook-hmac => archive/2026-08-19-add-timestamped-webhook-hmac}/proposal.md (100%) rename openspec/changes/{add-timestamped-webhook-hmac => archive/2026-08-19-add-timestamped-webhook-hmac}/specs/inbound-webhooks/spec.md (100%) rename openspec/changes/{add-timestamped-webhook-hmac => archive/2026-08-19-add-timestamped-webhook-hmac}/tasks.md (100%) rename openspec/changes/{adopt-shellsyntax-alpha1 => archive/2026-08-19-adopt-shellsyntax-alpha1}/.openspec.yaml (100%) rename openspec/changes/{adopt-shellsyntax-alpha1 => archive/2026-08-19-adopt-shellsyntax-alpha1}/design.md (100%) rename openspec/changes/{adopt-shellsyntax-alpha1 => archive/2026-08-19-adopt-shellsyntax-alpha1}/proposal.md (100%) rename openspec/changes/{adopt-shellsyntax-alpha1 => archive/2026-08-19-adopt-shellsyntax-alpha1}/specs/tool-approval-gates/spec.md (100%) rename openspec/changes/{adopt-shellsyntax-alpha1 => archive/2026-08-19-adopt-shellsyntax-alpha1}/tasks.md (100%) rename openspec/changes/{consolidate-binding-actor-engines => archive/2026-08-19-consolidate-binding-actor-engines}/.openspec.yaml (100%) rename openspec/changes/{consolidate-binding-actor-engines => archive/2026-08-19-consolidate-binding-actor-engines}/design.md (100%) rename openspec/changes/{consolidate-binding-actor-engines => archive/2026-08-19-consolidate-binding-actor-engines}/proposal.md (100%) rename openspec/changes/{consolidate-binding-actor-engines => archive/2026-08-19-consolidate-binding-actor-engines}/specs/channel-binding-parity/spec.md (100%) rename openspec/changes/{consolidate-binding-actor-engines => archive/2026-08-19-consolidate-binding-actor-engines}/tasks.md (98%) rename openspec/changes/{extend-live-approval-regression-corpus => archive/2026-08-19-extend-live-approval-regression-corpus}/.openspec.yaml (100%) rename openspec/changes/{extend-live-approval-regression-corpus => archive/2026-08-19-extend-live-approval-regression-corpus}/design.md (100%) rename openspec/changes/{extend-live-approval-regression-corpus => archive/2026-08-19-extend-live-approval-regression-corpus}/proposal.md (100%) rename openspec/changes/{extend-live-approval-regression-corpus => archive/2026-08-19-extend-live-approval-regression-corpus}/specs/tool-approval-gates/spec.md (100%) rename openspec/changes/{extend-live-approval-regression-corpus => archive/2026-08-19-extend-live-approval-regression-corpus}/tasks.md (100%) rename openspec/changes/{guide-subagents-to-session-scratch => archive/2026-08-19-guide-subagents-to-session-scratch}/.openspec.yaml (100%) rename openspec/changes/{guide-subagents-to-session-scratch => archive/2026-08-19-guide-subagents-to-session-scratch}/design.md (100%) rename openspec/changes/{guide-subagents-to-session-scratch => archive/2026-08-19-guide-subagents-to-session-scratch}/proposal.md (100%) rename openspec/changes/{guide-subagents-to-session-scratch => archive/2026-08-19-guide-subagents-to-session-scratch}/specs/session-cwd/spec.md (100%) rename openspec/changes/{guide-subagents-to-session-scratch => archive/2026-08-19-guide-subagents-to-session-scratch}/specs/tool-approval-gates/spec.md (100%) rename openspec/changes/{guide-subagents-to-session-scratch => archive/2026-08-19-guide-subagents-to-session-scratch}/tasks.md (100%) rename openspec/changes/{lock-shell-approval-store-v3 => archive/2026-08-19-lock-shell-approval-store-v3}/.openspec.yaml (100%) rename openspec/changes/{lock-shell-approval-store-v3 => archive/2026-08-19-lock-shell-approval-store-v3}/design.md (100%) rename openspec/changes/{lock-shell-approval-store-v3 => archive/2026-08-19-lock-shell-approval-store-v3}/proposal.md (100%) rename openspec/changes/{lock-shell-approval-store-v3 => archive/2026-08-19-lock-shell-approval-store-v3}/specs/netclaw-cli/spec.md (100%) rename openspec/changes/{lock-shell-approval-store-v3 => archive/2026-08-19-lock-shell-approval-store-v3}/specs/tool-approval-gates/spec.md (100%) rename openspec/changes/{lock-shell-approval-store-v3 => archive/2026-08-19-lock-shell-approval-store-v3}/tasks.md (100%) rename openspec/changes/{preserve-model-definitions-across-role-switches => archive/2026-08-19-preserve-model-definitions-across-role-switches}/.openspec.yaml (100%) rename openspec/changes/{preserve-model-definitions-across-role-switches => archive/2026-08-19-preserve-model-definitions-across-role-switches}/design.md (100%) rename openspec/changes/{preserve-model-definitions-across-role-switches => archive/2026-08-19-preserve-model-definitions-across-role-switches}/proposal.md (100%) rename openspec/changes/{preserve-model-definitions-across-role-switches => archive/2026-08-19-preserve-model-definitions-across-role-switches}/specs/named-model-definitions/spec.md (100%) rename openspec/changes/{preserve-model-definitions-across-role-switches => archive/2026-08-19-preserve-model-definitions-across-role-switches}/specs/netclaw-cli/spec.md (100%) rename openspec/changes/{preserve-model-definitions-across-role-switches => archive/2026-08-19-preserve-model-definitions-across-role-switches}/specs/netclaw-model-providers/spec.md (100%) rename openspec/changes/{preserve-model-definitions-across-role-switches => archive/2026-08-19-preserve-model-definitions-across-role-switches}/specs/netclaw-testing/spec.md (100%) rename openspec/changes/{preserve-model-definitions-across-role-switches => archive/2026-08-19-preserve-model-definitions-across-role-switches}/tasks.md (100%) rename openspec/changes/{reject-incompatible-session-history => archive/2026-08-19-reject-incompatible-session-history}/.openspec.yaml (100%) rename openspec/changes/{reject-incompatible-session-history => archive/2026-08-19-reject-incompatible-session-history}/design.md (100%) rename openspec/changes/{reject-incompatible-session-history => archive/2026-08-19-reject-incompatible-session-history}/proposal.md (100%) rename openspec/changes/{reject-incompatible-session-history => archive/2026-08-19-reject-incompatible-session-history}/specs/netclaw-model-capabilities/spec.md (100%) rename openspec/changes/{reject-incompatible-session-history => archive/2026-08-19-reject-incompatible-session-history}/tasks.md (100%) rename openspec/changes/{reliable-one-shot-reminder-retry => archive/2026-08-19-reliable-one-shot-reminder-retry}/.openspec.yaml (100%) rename openspec/changes/{reliable-one-shot-reminder-retry => archive/2026-08-19-reliable-one-shot-reminder-retry}/design.md (100%) rename openspec/changes/{reliable-one-shot-reminder-retry => archive/2026-08-19-reliable-one-shot-reminder-retry}/proposal.md (100%) rename openspec/changes/{reliable-one-shot-reminder-retry => archive/2026-08-19-reliable-one-shot-reminder-retry}/specs/netclaw-scheduling/spec.md (100%) rename openspec/changes/{reliable-one-shot-reminder-retry => archive/2026-08-19-reliable-one-shot-reminder-retry}/specs/reminder-execution-history/spec.md (100%) rename openspec/changes/{reliable-one-shot-reminder-retry => archive/2026-08-19-reliable-one-shot-reminder-retry}/tasks.md (100%) rename openspec/changes/{simplify-shell-policy-evaluator => archive/2026-08-19-simplify-shell-policy-evaluator}/.openspec.yaml (100%) rename openspec/changes/{simplify-shell-policy-evaluator => archive/2026-08-19-simplify-shell-policy-evaluator}/design.md (100%) rename openspec/changes/{simplify-shell-policy-evaluator => archive/2026-08-19-simplify-shell-policy-evaluator}/evidence/refactor-baseline.md (100%) rename openspec/changes/{simplify-shell-policy-evaluator => archive/2026-08-19-simplify-shell-policy-evaluator}/evidence/refactor-reduction-revision.md (100%) rename openspec/changes/{simplify-shell-policy-evaluator => archive/2026-08-19-simplify-shell-policy-evaluator}/proposal.md (100%) rename openspec/changes/{simplify-shell-policy-evaluator => archive/2026-08-19-simplify-shell-policy-evaluator}/specs/shell-policy-evaluator-architecture/spec.md (100%) rename openspec/changes/{simplify-shell-policy-evaluator => archive/2026-08-19-simplify-shell-policy-evaluator}/tasks.md (100%) rename openspec/changes/{skillserver-native-sidecar-sync => archive/2026-08-19-skillserver-native-sidecar-sync}/.openspec.yaml (100%) rename openspec/changes/{skillserver-native-sidecar-sync => archive/2026-08-19-skillserver-native-sidecar-sync}/design.md (100%) rename openspec/changes/{skillserver-native-sidecar-sync => archive/2026-08-19-skillserver-native-sidecar-sync}/proposal.md (100%) rename openspec/changes/{skillserver-native-sidecar-sync => archive/2026-08-19-skillserver-native-sidecar-sync}/specs/netclaw-subagents/spec.md (100%) rename openspec/changes/{skillserver-native-sidecar-sync => archive/2026-08-19-skillserver-native-sidecar-sync}/specs/skillserver-native-sidecar-sync/spec.md (100%) rename openspec/changes/{skillserver-native-sidecar-sync => archive/2026-08-19-skillserver-native-sidecar-sync}/tasks.md (100%) rename openspec/changes/{subscribe-mcp-catalog-notifications => archive/2026-08-19-subscribe-mcp-catalog-notifications}/.openspec.yaml (100%) rename openspec/changes/{subscribe-mcp-catalog-notifications => archive/2026-08-19-subscribe-mcp-catalog-notifications}/design.md (100%) rename openspec/changes/{subscribe-mcp-catalog-notifications => archive/2026-08-19-subscribe-mcp-catalog-notifications}/proposal.md (100%) rename openspec/changes/{subscribe-mcp-catalog-notifications => archive/2026-08-19-subscribe-mcp-catalog-notifications}/specs/netclaw-mcp/spec.md (100%) rename openspec/changes/{subscribe-mcp-catalog-notifications => archive/2026-08-19-subscribe-mcp-catalog-notifications}/tasks.md (100%) rename openspec/changes/{systemd-daemon-path-capture => archive/2026-08-19-systemd-daemon-path-capture}/.openspec.yaml (100%) rename openspec/changes/{systemd-daemon-path-capture => archive/2026-08-19-systemd-daemon-path-capture}/design.md (100%) rename openspec/changes/{systemd-daemon-path-capture => archive/2026-08-19-systemd-daemon-path-capture}/proposal.md (100%) rename openspec/changes/{systemd-daemon-path-capture => archive/2026-08-19-systemd-daemon-path-capture}/specs/daemon-shell-path/spec.md (100%) rename openspec/changes/{systemd-daemon-path-capture => archive/2026-08-19-systemd-daemon-path-capture}/tasks.md (100%) rename openspec/changes/{webhook-route-actor-ownership => archive/2026-08-19-webhook-route-actor-ownership}/.openspec.yaml (100%) rename openspec/changes/{webhook-route-actor-ownership => archive/2026-08-19-webhook-route-actor-ownership}/design.md (100%) rename openspec/changes/{webhook-route-actor-ownership => archive/2026-08-19-webhook-route-actor-ownership}/proposal.md (100%) rename openspec/changes/{webhook-route-actor-ownership => archive/2026-08-19-webhook-route-actor-ownership}/specs/webhook-route-authority/spec.md (100%) rename openspec/changes/{webhook-route-actor-ownership => archive/2026-08-19-webhook-route-actor-ownership}/tasks.md (100%) diff --git a/openspec/changes/add-deepseek-provider/.openspec.yaml b/openspec/changes/archive/2026-08-19-add-deepseek-provider/.openspec.yaml similarity index 100% rename from openspec/changes/add-deepseek-provider/.openspec.yaml rename to openspec/changes/archive/2026-08-19-add-deepseek-provider/.openspec.yaml diff --git a/openspec/changes/add-deepseek-provider/design.md b/openspec/changes/archive/2026-08-19-add-deepseek-provider/design.md similarity index 100% rename from openspec/changes/add-deepseek-provider/design.md rename to openspec/changes/archive/2026-08-19-add-deepseek-provider/design.md diff --git a/openspec/changes/add-deepseek-provider/proposal.md b/openspec/changes/archive/2026-08-19-add-deepseek-provider/proposal.md similarity index 100% rename from openspec/changes/add-deepseek-provider/proposal.md rename to openspec/changes/archive/2026-08-19-add-deepseek-provider/proposal.md diff --git a/openspec/changes/add-deepseek-provider/specs/netclaw-model-providers/spec.md b/openspec/changes/archive/2026-08-19-add-deepseek-provider/specs/netclaw-model-providers/spec.md similarity index 100% rename from openspec/changes/add-deepseek-provider/specs/netclaw-model-providers/spec.md rename to openspec/changes/archive/2026-08-19-add-deepseek-provider/specs/netclaw-model-providers/spec.md diff --git a/openspec/changes/add-deepseek-provider/tasks.md b/openspec/changes/archive/2026-08-19-add-deepseek-provider/tasks.md similarity index 100% rename from openspec/changes/add-deepseek-provider/tasks.md rename to openspec/changes/archive/2026-08-19-add-deepseek-provider/tasks.md diff --git a/openspec/changes/add-mcp-prompt-skills/.openspec.yaml b/openspec/changes/archive/2026-08-19-add-mcp-prompt-skills/.openspec.yaml similarity index 100% rename from openspec/changes/add-mcp-prompt-skills/.openspec.yaml rename to openspec/changes/archive/2026-08-19-add-mcp-prompt-skills/.openspec.yaml diff --git a/openspec/changes/add-mcp-prompt-skills/design.md b/openspec/changes/archive/2026-08-19-add-mcp-prompt-skills/design.md similarity index 100% rename from openspec/changes/add-mcp-prompt-skills/design.md rename to openspec/changes/archive/2026-08-19-add-mcp-prompt-skills/design.md diff --git a/openspec/changes/add-mcp-prompt-skills/proposal.md b/openspec/changes/archive/2026-08-19-add-mcp-prompt-skills/proposal.md similarity index 100% rename from openspec/changes/add-mcp-prompt-skills/proposal.md rename to openspec/changes/archive/2026-08-19-add-mcp-prompt-skills/proposal.md diff --git a/openspec/changes/add-mcp-prompt-skills/specs/audience-context-filtering/spec.md b/openspec/changes/archive/2026-08-19-add-mcp-prompt-skills/specs/audience-context-filtering/spec.md similarity index 100% rename from openspec/changes/add-mcp-prompt-skills/specs/audience-context-filtering/spec.md rename to openspec/changes/archive/2026-08-19-add-mcp-prompt-skills/specs/audience-context-filtering/spec.md diff --git a/openspec/changes/add-mcp-prompt-skills/specs/netclaw-mcp/spec.md b/openspec/changes/archive/2026-08-19-add-mcp-prompt-skills/specs/netclaw-mcp/spec.md similarity index 100% rename from openspec/changes/add-mcp-prompt-skills/specs/netclaw-mcp/spec.md rename to openspec/changes/archive/2026-08-19-add-mcp-prompt-skills/specs/netclaw-mcp/spec.md diff --git a/openspec/changes/add-mcp-prompt-skills/specs/skill-index-compression/spec.md b/openspec/changes/archive/2026-08-19-add-mcp-prompt-skills/specs/skill-index-compression/spec.md similarity index 100% rename from openspec/changes/add-mcp-prompt-skills/specs/skill-index-compression/spec.md rename to openspec/changes/archive/2026-08-19-add-mcp-prompt-skills/specs/skill-index-compression/spec.md diff --git a/openspec/changes/add-mcp-prompt-skills/specs/skill-tools/spec.md b/openspec/changes/archive/2026-08-19-add-mcp-prompt-skills/specs/skill-tools/spec.md similarity index 100% rename from openspec/changes/add-mcp-prompt-skills/specs/skill-tools/spec.md rename to openspec/changes/archive/2026-08-19-add-mcp-prompt-skills/specs/skill-tools/spec.md diff --git a/openspec/changes/add-mcp-prompt-skills/tasks.md b/openspec/changes/archive/2026-08-19-add-mcp-prompt-skills/tasks.md similarity index 100% rename from openspec/changes/add-mcp-prompt-skills/tasks.md rename to openspec/changes/archive/2026-08-19-add-mcp-prompt-skills/tasks.md diff --git a/openspec/changes/add-timestamped-webhook-hmac/.openspec.yaml b/openspec/changes/archive/2026-08-19-add-timestamped-webhook-hmac/.openspec.yaml similarity index 100% rename from openspec/changes/add-timestamped-webhook-hmac/.openspec.yaml rename to openspec/changes/archive/2026-08-19-add-timestamped-webhook-hmac/.openspec.yaml diff --git a/openspec/changes/add-timestamped-webhook-hmac/design.md b/openspec/changes/archive/2026-08-19-add-timestamped-webhook-hmac/design.md similarity index 100% rename from openspec/changes/add-timestamped-webhook-hmac/design.md rename to openspec/changes/archive/2026-08-19-add-timestamped-webhook-hmac/design.md diff --git a/openspec/changes/add-timestamped-webhook-hmac/proposal.md b/openspec/changes/archive/2026-08-19-add-timestamped-webhook-hmac/proposal.md similarity index 100% rename from openspec/changes/add-timestamped-webhook-hmac/proposal.md rename to openspec/changes/archive/2026-08-19-add-timestamped-webhook-hmac/proposal.md diff --git a/openspec/changes/add-timestamped-webhook-hmac/specs/inbound-webhooks/spec.md b/openspec/changes/archive/2026-08-19-add-timestamped-webhook-hmac/specs/inbound-webhooks/spec.md similarity index 100% rename from openspec/changes/add-timestamped-webhook-hmac/specs/inbound-webhooks/spec.md rename to openspec/changes/archive/2026-08-19-add-timestamped-webhook-hmac/specs/inbound-webhooks/spec.md diff --git a/openspec/changes/add-timestamped-webhook-hmac/tasks.md b/openspec/changes/archive/2026-08-19-add-timestamped-webhook-hmac/tasks.md similarity index 100% rename from openspec/changes/add-timestamped-webhook-hmac/tasks.md rename to openspec/changes/archive/2026-08-19-add-timestamped-webhook-hmac/tasks.md diff --git a/openspec/changes/adopt-shellsyntax-alpha1/.openspec.yaml b/openspec/changes/archive/2026-08-19-adopt-shellsyntax-alpha1/.openspec.yaml similarity index 100% rename from openspec/changes/adopt-shellsyntax-alpha1/.openspec.yaml rename to openspec/changes/archive/2026-08-19-adopt-shellsyntax-alpha1/.openspec.yaml diff --git a/openspec/changes/adopt-shellsyntax-alpha1/design.md b/openspec/changes/archive/2026-08-19-adopt-shellsyntax-alpha1/design.md similarity index 100% rename from openspec/changes/adopt-shellsyntax-alpha1/design.md rename to openspec/changes/archive/2026-08-19-adopt-shellsyntax-alpha1/design.md diff --git a/openspec/changes/adopt-shellsyntax-alpha1/proposal.md b/openspec/changes/archive/2026-08-19-adopt-shellsyntax-alpha1/proposal.md similarity index 100% rename from openspec/changes/adopt-shellsyntax-alpha1/proposal.md rename to openspec/changes/archive/2026-08-19-adopt-shellsyntax-alpha1/proposal.md diff --git a/openspec/changes/adopt-shellsyntax-alpha1/specs/tool-approval-gates/spec.md b/openspec/changes/archive/2026-08-19-adopt-shellsyntax-alpha1/specs/tool-approval-gates/spec.md similarity index 100% rename from openspec/changes/adopt-shellsyntax-alpha1/specs/tool-approval-gates/spec.md rename to openspec/changes/archive/2026-08-19-adopt-shellsyntax-alpha1/specs/tool-approval-gates/spec.md diff --git a/openspec/changes/adopt-shellsyntax-alpha1/tasks.md b/openspec/changes/archive/2026-08-19-adopt-shellsyntax-alpha1/tasks.md similarity index 100% rename from openspec/changes/adopt-shellsyntax-alpha1/tasks.md rename to openspec/changes/archive/2026-08-19-adopt-shellsyntax-alpha1/tasks.md diff --git a/openspec/changes/consolidate-binding-actor-engines/.openspec.yaml b/openspec/changes/archive/2026-08-19-consolidate-binding-actor-engines/.openspec.yaml similarity index 100% rename from openspec/changes/consolidate-binding-actor-engines/.openspec.yaml rename to openspec/changes/archive/2026-08-19-consolidate-binding-actor-engines/.openspec.yaml diff --git a/openspec/changes/consolidate-binding-actor-engines/design.md b/openspec/changes/archive/2026-08-19-consolidate-binding-actor-engines/design.md similarity index 100% rename from openspec/changes/consolidate-binding-actor-engines/design.md rename to openspec/changes/archive/2026-08-19-consolidate-binding-actor-engines/design.md diff --git a/openspec/changes/consolidate-binding-actor-engines/proposal.md b/openspec/changes/archive/2026-08-19-consolidate-binding-actor-engines/proposal.md similarity index 100% rename from openspec/changes/consolidate-binding-actor-engines/proposal.md rename to openspec/changes/archive/2026-08-19-consolidate-binding-actor-engines/proposal.md diff --git a/openspec/changes/consolidate-binding-actor-engines/specs/channel-binding-parity/spec.md b/openspec/changes/archive/2026-08-19-consolidate-binding-actor-engines/specs/channel-binding-parity/spec.md similarity index 100% rename from openspec/changes/consolidate-binding-actor-engines/specs/channel-binding-parity/spec.md rename to openspec/changes/archive/2026-08-19-consolidate-binding-actor-engines/specs/channel-binding-parity/spec.md diff --git a/openspec/changes/consolidate-binding-actor-engines/tasks.md b/openspec/changes/archive/2026-08-19-consolidate-binding-actor-engines/tasks.md similarity index 98% rename from openspec/changes/consolidate-binding-actor-engines/tasks.md rename to openspec/changes/archive/2026-08-19-consolidate-binding-actor-engines/tasks.md index c47888954..13887462e 100644 --- a/openspec/changes/consolidate-binding-actor-engines/tasks.md +++ b/openspec/changes/archive/2026-08-19-consolidate-binding-actor-engines/tasks.md @@ -30,4 +30,4 @@ Working branch: `refactor/binding-engine`, stacked on `refactor/delivery-failure - [x] 5.1 Full solution build with zero warnings; full `Netclaw.Actors.Tests`, `Netclaw.Daemon.Tests` channel suites; slopwatch and header gates - [x] 5.2 Line-count accounting for the PR description (target ~1,200-1,500 lines removed) and parity-spec sync via `/opsx-sync` -- [ ] 5.3 Submit `refactor/binding-engine` as PR 4 via `gh stack submit`; PR body documents any differences surfaced by the stop rule +- [x] 5.3 Submit `refactor/binding-engine` as PR 4 via `gh stack submit`; PR body documents any differences surfaced by the stop rule diff --git a/openspec/changes/extend-live-approval-regression-corpus/.openspec.yaml b/openspec/changes/archive/2026-08-19-extend-live-approval-regression-corpus/.openspec.yaml similarity index 100% rename from openspec/changes/extend-live-approval-regression-corpus/.openspec.yaml rename to openspec/changes/archive/2026-08-19-extend-live-approval-regression-corpus/.openspec.yaml diff --git a/openspec/changes/extend-live-approval-regression-corpus/design.md b/openspec/changes/archive/2026-08-19-extend-live-approval-regression-corpus/design.md similarity index 100% rename from openspec/changes/extend-live-approval-regression-corpus/design.md rename to openspec/changes/archive/2026-08-19-extend-live-approval-regression-corpus/design.md diff --git a/openspec/changes/extend-live-approval-regression-corpus/proposal.md b/openspec/changes/archive/2026-08-19-extend-live-approval-regression-corpus/proposal.md similarity index 100% rename from openspec/changes/extend-live-approval-regression-corpus/proposal.md rename to openspec/changes/archive/2026-08-19-extend-live-approval-regression-corpus/proposal.md diff --git a/openspec/changes/extend-live-approval-regression-corpus/specs/tool-approval-gates/spec.md b/openspec/changes/archive/2026-08-19-extend-live-approval-regression-corpus/specs/tool-approval-gates/spec.md similarity index 100% rename from openspec/changes/extend-live-approval-regression-corpus/specs/tool-approval-gates/spec.md rename to openspec/changes/archive/2026-08-19-extend-live-approval-regression-corpus/specs/tool-approval-gates/spec.md diff --git a/openspec/changes/extend-live-approval-regression-corpus/tasks.md b/openspec/changes/archive/2026-08-19-extend-live-approval-regression-corpus/tasks.md similarity index 100% rename from openspec/changes/extend-live-approval-regression-corpus/tasks.md rename to openspec/changes/archive/2026-08-19-extend-live-approval-regression-corpus/tasks.md diff --git a/openspec/changes/guide-subagents-to-session-scratch/.openspec.yaml b/openspec/changes/archive/2026-08-19-guide-subagents-to-session-scratch/.openspec.yaml similarity index 100% rename from openspec/changes/guide-subagents-to-session-scratch/.openspec.yaml rename to openspec/changes/archive/2026-08-19-guide-subagents-to-session-scratch/.openspec.yaml diff --git a/openspec/changes/guide-subagents-to-session-scratch/design.md b/openspec/changes/archive/2026-08-19-guide-subagents-to-session-scratch/design.md similarity index 100% rename from openspec/changes/guide-subagents-to-session-scratch/design.md rename to openspec/changes/archive/2026-08-19-guide-subagents-to-session-scratch/design.md diff --git a/openspec/changes/guide-subagents-to-session-scratch/proposal.md b/openspec/changes/archive/2026-08-19-guide-subagents-to-session-scratch/proposal.md similarity index 100% rename from openspec/changes/guide-subagents-to-session-scratch/proposal.md rename to openspec/changes/archive/2026-08-19-guide-subagents-to-session-scratch/proposal.md diff --git a/openspec/changes/guide-subagents-to-session-scratch/specs/session-cwd/spec.md b/openspec/changes/archive/2026-08-19-guide-subagents-to-session-scratch/specs/session-cwd/spec.md similarity index 100% rename from openspec/changes/guide-subagents-to-session-scratch/specs/session-cwd/spec.md rename to openspec/changes/archive/2026-08-19-guide-subagents-to-session-scratch/specs/session-cwd/spec.md diff --git a/openspec/changes/guide-subagents-to-session-scratch/specs/tool-approval-gates/spec.md b/openspec/changes/archive/2026-08-19-guide-subagents-to-session-scratch/specs/tool-approval-gates/spec.md similarity index 100% rename from openspec/changes/guide-subagents-to-session-scratch/specs/tool-approval-gates/spec.md rename to openspec/changes/archive/2026-08-19-guide-subagents-to-session-scratch/specs/tool-approval-gates/spec.md diff --git a/openspec/changes/guide-subagents-to-session-scratch/tasks.md b/openspec/changes/archive/2026-08-19-guide-subagents-to-session-scratch/tasks.md similarity index 100% rename from openspec/changes/guide-subagents-to-session-scratch/tasks.md rename to openspec/changes/archive/2026-08-19-guide-subagents-to-session-scratch/tasks.md diff --git a/openspec/changes/lock-shell-approval-store-v3/.openspec.yaml b/openspec/changes/archive/2026-08-19-lock-shell-approval-store-v3/.openspec.yaml similarity index 100% rename from openspec/changes/lock-shell-approval-store-v3/.openspec.yaml rename to openspec/changes/archive/2026-08-19-lock-shell-approval-store-v3/.openspec.yaml diff --git a/openspec/changes/lock-shell-approval-store-v3/design.md b/openspec/changes/archive/2026-08-19-lock-shell-approval-store-v3/design.md similarity index 100% rename from openspec/changes/lock-shell-approval-store-v3/design.md rename to openspec/changes/archive/2026-08-19-lock-shell-approval-store-v3/design.md diff --git a/openspec/changes/lock-shell-approval-store-v3/proposal.md b/openspec/changes/archive/2026-08-19-lock-shell-approval-store-v3/proposal.md similarity index 100% rename from openspec/changes/lock-shell-approval-store-v3/proposal.md rename to openspec/changes/archive/2026-08-19-lock-shell-approval-store-v3/proposal.md diff --git a/openspec/changes/lock-shell-approval-store-v3/specs/netclaw-cli/spec.md b/openspec/changes/archive/2026-08-19-lock-shell-approval-store-v3/specs/netclaw-cli/spec.md similarity index 100% rename from openspec/changes/lock-shell-approval-store-v3/specs/netclaw-cli/spec.md rename to openspec/changes/archive/2026-08-19-lock-shell-approval-store-v3/specs/netclaw-cli/spec.md diff --git a/openspec/changes/lock-shell-approval-store-v3/specs/tool-approval-gates/spec.md b/openspec/changes/archive/2026-08-19-lock-shell-approval-store-v3/specs/tool-approval-gates/spec.md similarity index 100% rename from openspec/changes/lock-shell-approval-store-v3/specs/tool-approval-gates/spec.md rename to openspec/changes/archive/2026-08-19-lock-shell-approval-store-v3/specs/tool-approval-gates/spec.md diff --git a/openspec/changes/lock-shell-approval-store-v3/tasks.md b/openspec/changes/archive/2026-08-19-lock-shell-approval-store-v3/tasks.md similarity index 100% rename from openspec/changes/lock-shell-approval-store-v3/tasks.md rename to openspec/changes/archive/2026-08-19-lock-shell-approval-store-v3/tasks.md diff --git a/openspec/changes/preserve-model-definitions-across-role-switches/.openspec.yaml b/openspec/changes/archive/2026-08-19-preserve-model-definitions-across-role-switches/.openspec.yaml similarity index 100% rename from openspec/changes/preserve-model-definitions-across-role-switches/.openspec.yaml rename to openspec/changes/archive/2026-08-19-preserve-model-definitions-across-role-switches/.openspec.yaml diff --git a/openspec/changes/preserve-model-definitions-across-role-switches/design.md b/openspec/changes/archive/2026-08-19-preserve-model-definitions-across-role-switches/design.md similarity index 100% rename from openspec/changes/preserve-model-definitions-across-role-switches/design.md rename to openspec/changes/archive/2026-08-19-preserve-model-definitions-across-role-switches/design.md diff --git a/openspec/changes/preserve-model-definitions-across-role-switches/proposal.md b/openspec/changes/archive/2026-08-19-preserve-model-definitions-across-role-switches/proposal.md similarity index 100% rename from openspec/changes/preserve-model-definitions-across-role-switches/proposal.md rename to openspec/changes/archive/2026-08-19-preserve-model-definitions-across-role-switches/proposal.md diff --git a/openspec/changes/preserve-model-definitions-across-role-switches/specs/named-model-definitions/spec.md b/openspec/changes/archive/2026-08-19-preserve-model-definitions-across-role-switches/specs/named-model-definitions/spec.md similarity index 100% rename from openspec/changes/preserve-model-definitions-across-role-switches/specs/named-model-definitions/spec.md rename to openspec/changes/archive/2026-08-19-preserve-model-definitions-across-role-switches/specs/named-model-definitions/spec.md diff --git a/openspec/changes/preserve-model-definitions-across-role-switches/specs/netclaw-cli/spec.md b/openspec/changes/archive/2026-08-19-preserve-model-definitions-across-role-switches/specs/netclaw-cli/spec.md similarity index 100% rename from openspec/changes/preserve-model-definitions-across-role-switches/specs/netclaw-cli/spec.md rename to openspec/changes/archive/2026-08-19-preserve-model-definitions-across-role-switches/specs/netclaw-cli/spec.md diff --git a/openspec/changes/preserve-model-definitions-across-role-switches/specs/netclaw-model-providers/spec.md b/openspec/changes/archive/2026-08-19-preserve-model-definitions-across-role-switches/specs/netclaw-model-providers/spec.md similarity index 100% rename from openspec/changes/preserve-model-definitions-across-role-switches/specs/netclaw-model-providers/spec.md rename to openspec/changes/archive/2026-08-19-preserve-model-definitions-across-role-switches/specs/netclaw-model-providers/spec.md diff --git a/openspec/changes/preserve-model-definitions-across-role-switches/specs/netclaw-testing/spec.md b/openspec/changes/archive/2026-08-19-preserve-model-definitions-across-role-switches/specs/netclaw-testing/spec.md similarity index 100% rename from openspec/changes/preserve-model-definitions-across-role-switches/specs/netclaw-testing/spec.md rename to openspec/changes/archive/2026-08-19-preserve-model-definitions-across-role-switches/specs/netclaw-testing/spec.md diff --git a/openspec/changes/preserve-model-definitions-across-role-switches/tasks.md b/openspec/changes/archive/2026-08-19-preserve-model-definitions-across-role-switches/tasks.md similarity index 100% rename from openspec/changes/preserve-model-definitions-across-role-switches/tasks.md rename to openspec/changes/archive/2026-08-19-preserve-model-definitions-across-role-switches/tasks.md diff --git a/openspec/changes/reject-incompatible-session-history/.openspec.yaml b/openspec/changes/archive/2026-08-19-reject-incompatible-session-history/.openspec.yaml similarity index 100% rename from openspec/changes/reject-incompatible-session-history/.openspec.yaml rename to openspec/changes/archive/2026-08-19-reject-incompatible-session-history/.openspec.yaml diff --git a/openspec/changes/reject-incompatible-session-history/design.md b/openspec/changes/archive/2026-08-19-reject-incompatible-session-history/design.md similarity index 100% rename from openspec/changes/reject-incompatible-session-history/design.md rename to openspec/changes/archive/2026-08-19-reject-incompatible-session-history/design.md diff --git a/openspec/changes/reject-incompatible-session-history/proposal.md b/openspec/changes/archive/2026-08-19-reject-incompatible-session-history/proposal.md similarity index 100% rename from openspec/changes/reject-incompatible-session-history/proposal.md rename to openspec/changes/archive/2026-08-19-reject-incompatible-session-history/proposal.md diff --git a/openspec/changes/reject-incompatible-session-history/specs/netclaw-model-capabilities/spec.md b/openspec/changes/archive/2026-08-19-reject-incompatible-session-history/specs/netclaw-model-capabilities/spec.md similarity index 100% rename from openspec/changes/reject-incompatible-session-history/specs/netclaw-model-capabilities/spec.md rename to openspec/changes/archive/2026-08-19-reject-incompatible-session-history/specs/netclaw-model-capabilities/spec.md diff --git a/openspec/changes/reject-incompatible-session-history/tasks.md b/openspec/changes/archive/2026-08-19-reject-incompatible-session-history/tasks.md similarity index 100% rename from openspec/changes/reject-incompatible-session-history/tasks.md rename to openspec/changes/archive/2026-08-19-reject-incompatible-session-history/tasks.md diff --git a/openspec/changes/reliable-one-shot-reminder-retry/.openspec.yaml b/openspec/changes/archive/2026-08-19-reliable-one-shot-reminder-retry/.openspec.yaml similarity index 100% rename from openspec/changes/reliable-one-shot-reminder-retry/.openspec.yaml rename to openspec/changes/archive/2026-08-19-reliable-one-shot-reminder-retry/.openspec.yaml diff --git a/openspec/changes/reliable-one-shot-reminder-retry/design.md b/openspec/changes/archive/2026-08-19-reliable-one-shot-reminder-retry/design.md similarity index 100% rename from openspec/changes/reliable-one-shot-reminder-retry/design.md rename to openspec/changes/archive/2026-08-19-reliable-one-shot-reminder-retry/design.md diff --git a/openspec/changes/reliable-one-shot-reminder-retry/proposal.md b/openspec/changes/archive/2026-08-19-reliable-one-shot-reminder-retry/proposal.md similarity index 100% rename from openspec/changes/reliable-one-shot-reminder-retry/proposal.md rename to openspec/changes/archive/2026-08-19-reliable-one-shot-reminder-retry/proposal.md diff --git a/openspec/changes/reliable-one-shot-reminder-retry/specs/netclaw-scheduling/spec.md b/openspec/changes/archive/2026-08-19-reliable-one-shot-reminder-retry/specs/netclaw-scheduling/spec.md similarity index 100% rename from openspec/changes/reliable-one-shot-reminder-retry/specs/netclaw-scheduling/spec.md rename to openspec/changes/archive/2026-08-19-reliable-one-shot-reminder-retry/specs/netclaw-scheduling/spec.md diff --git a/openspec/changes/reliable-one-shot-reminder-retry/specs/reminder-execution-history/spec.md b/openspec/changes/archive/2026-08-19-reliable-one-shot-reminder-retry/specs/reminder-execution-history/spec.md similarity index 100% rename from openspec/changes/reliable-one-shot-reminder-retry/specs/reminder-execution-history/spec.md rename to openspec/changes/archive/2026-08-19-reliable-one-shot-reminder-retry/specs/reminder-execution-history/spec.md diff --git a/openspec/changes/reliable-one-shot-reminder-retry/tasks.md b/openspec/changes/archive/2026-08-19-reliable-one-shot-reminder-retry/tasks.md similarity index 100% rename from openspec/changes/reliable-one-shot-reminder-retry/tasks.md rename to openspec/changes/archive/2026-08-19-reliable-one-shot-reminder-retry/tasks.md diff --git a/openspec/changes/simplify-shell-policy-evaluator/.openspec.yaml b/openspec/changes/archive/2026-08-19-simplify-shell-policy-evaluator/.openspec.yaml similarity index 100% rename from openspec/changes/simplify-shell-policy-evaluator/.openspec.yaml rename to openspec/changes/archive/2026-08-19-simplify-shell-policy-evaluator/.openspec.yaml diff --git a/openspec/changes/simplify-shell-policy-evaluator/design.md b/openspec/changes/archive/2026-08-19-simplify-shell-policy-evaluator/design.md similarity index 100% rename from openspec/changes/simplify-shell-policy-evaluator/design.md rename to openspec/changes/archive/2026-08-19-simplify-shell-policy-evaluator/design.md diff --git a/openspec/changes/simplify-shell-policy-evaluator/evidence/refactor-baseline.md b/openspec/changes/archive/2026-08-19-simplify-shell-policy-evaluator/evidence/refactor-baseline.md similarity index 100% rename from openspec/changes/simplify-shell-policy-evaluator/evidence/refactor-baseline.md rename to openspec/changes/archive/2026-08-19-simplify-shell-policy-evaluator/evidence/refactor-baseline.md diff --git a/openspec/changes/simplify-shell-policy-evaluator/evidence/refactor-reduction-revision.md b/openspec/changes/archive/2026-08-19-simplify-shell-policy-evaluator/evidence/refactor-reduction-revision.md similarity index 100% rename from openspec/changes/simplify-shell-policy-evaluator/evidence/refactor-reduction-revision.md rename to openspec/changes/archive/2026-08-19-simplify-shell-policy-evaluator/evidence/refactor-reduction-revision.md diff --git a/openspec/changes/simplify-shell-policy-evaluator/proposal.md b/openspec/changes/archive/2026-08-19-simplify-shell-policy-evaluator/proposal.md similarity index 100% rename from openspec/changes/simplify-shell-policy-evaluator/proposal.md rename to openspec/changes/archive/2026-08-19-simplify-shell-policy-evaluator/proposal.md diff --git a/openspec/changes/simplify-shell-policy-evaluator/specs/shell-policy-evaluator-architecture/spec.md b/openspec/changes/archive/2026-08-19-simplify-shell-policy-evaluator/specs/shell-policy-evaluator-architecture/spec.md similarity index 100% rename from openspec/changes/simplify-shell-policy-evaluator/specs/shell-policy-evaluator-architecture/spec.md rename to openspec/changes/archive/2026-08-19-simplify-shell-policy-evaluator/specs/shell-policy-evaluator-architecture/spec.md diff --git a/openspec/changes/simplify-shell-policy-evaluator/tasks.md b/openspec/changes/archive/2026-08-19-simplify-shell-policy-evaluator/tasks.md similarity index 100% rename from openspec/changes/simplify-shell-policy-evaluator/tasks.md rename to openspec/changes/archive/2026-08-19-simplify-shell-policy-evaluator/tasks.md diff --git a/openspec/changes/skillserver-native-sidecar-sync/.openspec.yaml b/openspec/changes/archive/2026-08-19-skillserver-native-sidecar-sync/.openspec.yaml similarity index 100% rename from openspec/changes/skillserver-native-sidecar-sync/.openspec.yaml rename to openspec/changes/archive/2026-08-19-skillserver-native-sidecar-sync/.openspec.yaml diff --git a/openspec/changes/skillserver-native-sidecar-sync/design.md b/openspec/changes/archive/2026-08-19-skillserver-native-sidecar-sync/design.md similarity index 100% rename from openspec/changes/skillserver-native-sidecar-sync/design.md rename to openspec/changes/archive/2026-08-19-skillserver-native-sidecar-sync/design.md diff --git a/openspec/changes/skillserver-native-sidecar-sync/proposal.md b/openspec/changes/archive/2026-08-19-skillserver-native-sidecar-sync/proposal.md similarity index 100% rename from openspec/changes/skillserver-native-sidecar-sync/proposal.md rename to openspec/changes/archive/2026-08-19-skillserver-native-sidecar-sync/proposal.md diff --git a/openspec/changes/skillserver-native-sidecar-sync/specs/netclaw-subagents/spec.md b/openspec/changes/archive/2026-08-19-skillserver-native-sidecar-sync/specs/netclaw-subagents/spec.md similarity index 100% rename from openspec/changes/skillserver-native-sidecar-sync/specs/netclaw-subagents/spec.md rename to openspec/changes/archive/2026-08-19-skillserver-native-sidecar-sync/specs/netclaw-subagents/spec.md diff --git a/openspec/changes/skillserver-native-sidecar-sync/specs/skillserver-native-sidecar-sync/spec.md b/openspec/changes/archive/2026-08-19-skillserver-native-sidecar-sync/specs/skillserver-native-sidecar-sync/spec.md similarity index 100% rename from openspec/changes/skillserver-native-sidecar-sync/specs/skillserver-native-sidecar-sync/spec.md rename to openspec/changes/archive/2026-08-19-skillserver-native-sidecar-sync/specs/skillserver-native-sidecar-sync/spec.md diff --git a/openspec/changes/skillserver-native-sidecar-sync/tasks.md b/openspec/changes/archive/2026-08-19-skillserver-native-sidecar-sync/tasks.md similarity index 100% rename from openspec/changes/skillserver-native-sidecar-sync/tasks.md rename to openspec/changes/archive/2026-08-19-skillserver-native-sidecar-sync/tasks.md diff --git a/openspec/changes/subscribe-mcp-catalog-notifications/.openspec.yaml b/openspec/changes/archive/2026-08-19-subscribe-mcp-catalog-notifications/.openspec.yaml similarity index 100% rename from openspec/changes/subscribe-mcp-catalog-notifications/.openspec.yaml rename to openspec/changes/archive/2026-08-19-subscribe-mcp-catalog-notifications/.openspec.yaml diff --git a/openspec/changes/subscribe-mcp-catalog-notifications/design.md b/openspec/changes/archive/2026-08-19-subscribe-mcp-catalog-notifications/design.md similarity index 100% rename from openspec/changes/subscribe-mcp-catalog-notifications/design.md rename to openspec/changes/archive/2026-08-19-subscribe-mcp-catalog-notifications/design.md diff --git a/openspec/changes/subscribe-mcp-catalog-notifications/proposal.md b/openspec/changes/archive/2026-08-19-subscribe-mcp-catalog-notifications/proposal.md similarity index 100% rename from openspec/changes/subscribe-mcp-catalog-notifications/proposal.md rename to openspec/changes/archive/2026-08-19-subscribe-mcp-catalog-notifications/proposal.md diff --git a/openspec/changes/subscribe-mcp-catalog-notifications/specs/netclaw-mcp/spec.md b/openspec/changes/archive/2026-08-19-subscribe-mcp-catalog-notifications/specs/netclaw-mcp/spec.md similarity index 100% rename from openspec/changes/subscribe-mcp-catalog-notifications/specs/netclaw-mcp/spec.md rename to openspec/changes/archive/2026-08-19-subscribe-mcp-catalog-notifications/specs/netclaw-mcp/spec.md diff --git a/openspec/changes/subscribe-mcp-catalog-notifications/tasks.md b/openspec/changes/archive/2026-08-19-subscribe-mcp-catalog-notifications/tasks.md similarity index 100% rename from openspec/changes/subscribe-mcp-catalog-notifications/tasks.md rename to openspec/changes/archive/2026-08-19-subscribe-mcp-catalog-notifications/tasks.md diff --git a/openspec/changes/systemd-daemon-path-capture/.openspec.yaml b/openspec/changes/archive/2026-08-19-systemd-daemon-path-capture/.openspec.yaml similarity index 100% rename from openspec/changes/systemd-daemon-path-capture/.openspec.yaml rename to openspec/changes/archive/2026-08-19-systemd-daemon-path-capture/.openspec.yaml diff --git a/openspec/changes/systemd-daemon-path-capture/design.md b/openspec/changes/archive/2026-08-19-systemd-daemon-path-capture/design.md similarity index 100% rename from openspec/changes/systemd-daemon-path-capture/design.md rename to openspec/changes/archive/2026-08-19-systemd-daemon-path-capture/design.md diff --git a/openspec/changes/systemd-daemon-path-capture/proposal.md b/openspec/changes/archive/2026-08-19-systemd-daemon-path-capture/proposal.md similarity index 100% rename from openspec/changes/systemd-daemon-path-capture/proposal.md rename to openspec/changes/archive/2026-08-19-systemd-daemon-path-capture/proposal.md diff --git a/openspec/changes/systemd-daemon-path-capture/specs/daemon-shell-path/spec.md b/openspec/changes/archive/2026-08-19-systemd-daemon-path-capture/specs/daemon-shell-path/spec.md similarity index 100% rename from openspec/changes/systemd-daemon-path-capture/specs/daemon-shell-path/spec.md rename to openspec/changes/archive/2026-08-19-systemd-daemon-path-capture/specs/daemon-shell-path/spec.md diff --git a/openspec/changes/systemd-daemon-path-capture/tasks.md b/openspec/changes/archive/2026-08-19-systemd-daemon-path-capture/tasks.md similarity index 100% rename from openspec/changes/systemd-daemon-path-capture/tasks.md rename to openspec/changes/archive/2026-08-19-systemd-daemon-path-capture/tasks.md diff --git a/openspec/changes/webhook-route-actor-ownership/.openspec.yaml b/openspec/changes/archive/2026-08-19-webhook-route-actor-ownership/.openspec.yaml similarity index 100% rename from openspec/changes/webhook-route-actor-ownership/.openspec.yaml rename to openspec/changes/archive/2026-08-19-webhook-route-actor-ownership/.openspec.yaml diff --git a/openspec/changes/webhook-route-actor-ownership/design.md b/openspec/changes/archive/2026-08-19-webhook-route-actor-ownership/design.md similarity index 100% rename from openspec/changes/webhook-route-actor-ownership/design.md rename to openspec/changes/archive/2026-08-19-webhook-route-actor-ownership/design.md diff --git a/openspec/changes/webhook-route-actor-ownership/proposal.md b/openspec/changes/archive/2026-08-19-webhook-route-actor-ownership/proposal.md similarity index 100% rename from openspec/changes/webhook-route-actor-ownership/proposal.md rename to openspec/changes/archive/2026-08-19-webhook-route-actor-ownership/proposal.md diff --git a/openspec/changes/webhook-route-actor-ownership/specs/webhook-route-authority/spec.md b/openspec/changes/archive/2026-08-19-webhook-route-actor-ownership/specs/webhook-route-authority/spec.md similarity index 100% rename from openspec/changes/webhook-route-actor-ownership/specs/webhook-route-authority/spec.md rename to openspec/changes/archive/2026-08-19-webhook-route-actor-ownership/specs/webhook-route-authority/spec.md diff --git a/openspec/changes/webhook-route-actor-ownership/tasks.md b/openspec/changes/archive/2026-08-19-webhook-route-actor-ownership/tasks.md similarity index 100% rename from openspec/changes/webhook-route-actor-ownership/tasks.md rename to openspec/changes/archive/2026-08-19-webhook-route-actor-ownership/tasks.md