* feat(#1451): wire gsd capability install/update/remove/list/disable/enable CLI
ADR-1244 D5/D6: the management command was built as a library (capability-lifecycle.cjs
install/upgrade/remove + capability-ledger.cjs) across Phases 3-5 but never wired to a
user-facing command — gsd-tools.cjs 'capability' only handled state/set. This adds the
six subcommands, dispatching to the existing lifecycle/ledger:
- install <spec> [--integrity] [--scope global|project] [--yes] [--shared-file <rel>]…
- update [<id>|--all] [--scope] [--yes] [--shared-file] (re-resolves recorded source)
- remove <id> [--purge-data] [--scope] (first-party rejected)
- list [--json] (first-party + overlay, both scopes, JSON array)
- disable|enable <id> (activation-state alias of capability set --off/--on)
Scope→runtimeDir mapping matches capability-loader exactly (global=$GSD_HOME||home,
project=project root; caps at <root>/.gsd/capabilities/<id>, ledger at <root>/.gsd-capabilities.json).
Consent is non-interactive: --yes grants; without it an executable install aborts after
printing the disclosure and writes nothing. Best-effort reconcile before each mutation.
Tests: tests/capability-cli.test.cjs (20 behavioral, real resolver via local specs,
GSD_HOME-sandboxed) — install consent/block/usage matrix, list, update round-trip,
remove round-trip + first-party guard, disable/enable, unknown subcommand.
Docs: docs/reference/gsd-capability-command.md reconciled to the real surface
(ledger paths, --shared-file, consent model, disable mechanism, outdated marked planned);
docs/COMMANDS.md gains the gsd capability entry.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#1451): resolve adversarial-review findings + root-cause the --raw silent-output bug
Adversarial-review (Codex) fixes:
- capReadStrict passes a malformed strict_known_registries value THROUGH so the trust gate
fail-closes on it (was silently downgrading to permissive)
- installCapability/upgradeCapability gain an expectedId guard + first-party-id rejection
(capability-lifecycle.cts): an overlay can't shadow a first-party id, and 'update <id>' can't
act on a different id if the recorded source was retargeted
- capability update: prints the consent disclosure, exits non-zero on --all partial failure,
no longer masks the resolved id
- capability remove: ledger-first ordering so an overlay is removable even if it shadows a
first-party name; first-party guard only fires for ids not in the ledger
- gsd-capability-command.md: disable/enable doc corrected (registry-known ids; overlay toggle
not yet wired through this path)
Silent-output bug (root cause, not waved off as pre-existing):
- captureStdoutSyncWrites buffered fd-1 output and DISCARDED it on the throw path — any --raw
command that emitted a result/error envelope then threw (to set a non-zero exit) lost ALL of
stdout. Now it flushes the captured buffer before re-throwing (exit code preserved).
- cmdCapabilitySet threw via process.exit() (bypassing the capture wrapper entirely); now throws
ExitError so the wrapper flushes — matches the repo's no-process-exit architecture.
- Regression test: capability disable <unknown> --raw must emit the JSON error envelope on stdout.
Verified: capability suite 165/165, @file/json-errors/phase 183/183, lint clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#1451): address adversarial-review R2 — shared-file confinement, MCP no-clobber, config fail-closed
- confinedSharedFile(): realpath-confine every shared-config write/strip to the scope root (mirrors
safeRmUnder), so a --shared-file whose parent is a symlink escaping the scope can't write outside it.
- mcpServers shared edits: never overwrite an UNOWNED entry — a name collision with the user's (or
another capability's) server is skipped, so install/remove can't silently clobber user MCP config
(hooks already append; the map-keyed mcpServers path was the gap).
- capReadStrict: a PRESENT-but-unparseable .planning/config.json now fails CLOSED (lockdown) instead
of silently downgrading the strict_known_registries policy to permissive.
- Tests: symlink-escape shared-file writes nothing outside scope; colliding user mcpServers entry
preserved; unparseable config blocks an external install. capability suite 83/83, lint clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#1451): address code-review — aborted-status robustness + coverage + project-scoped strict doc
- install/update: handle an 'aborted' result independently of the requiresConsent flag so it can never
fall through to the generic 'blocked: unknown reason' arm (aborted always means consent-needed per
the lifecycle contract; latent today, hardened for future status additions).
- Clarify capResolveScope comment (project scope === already-resolved cwd) and document that
strict_known_registries is a PROJECT-scoped policy (read regardless of --scope; no machine-wide
allowlist) in gsd-capability-command.md.
- Tests: update --all over an empty ledger returns an empty result set (exit 0); a flag value that
looks like another flag (--integrity --scope) is rejected, not swallowed. CLI suite 33/33, lint clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(#1451): FEATURES.md entry #147 + Added/Fixed changesets for the capability CLI
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore(#1451): backfill changeset PR number → #1457
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>