The descriptor-driven install path (resolveRuntimeArtifactLayout) installed no
agents for copilot/antigravity/cursor/windsurf/augment/trae/codebuddy/cline —
their per-runtime agent conversion ran only via the legacy bin/install.js loop.
This wires each runtime's agent converter into the descriptor so the new path
applies per-runtime conversion (follow-up to #1099; ADR-1235 cutover).
- capabilities/<rt>/capability.json: declare an `agents` kind with the runtime's
converter (global+local; cline global-only). Regenerated capability-registry.cjs.
- convertedAgentsKind threads install scope -> isGlobal so the scope-aware
copilot/antigravity converters choose global vs workspace-relative paths; the
six single-arg converters ignore the extra arg. stageAgentsForRuntimeWithConverter
passes isGlobal to the converter.
- Tests: feat-1173 gains a real-registry block asserting each runtime's descriptor
applies the correct converter (== conv(src, isGlobal), != raw copy) with scope
threading (fails-first on pristine next). The ADR-857 equivalence golden +
per-runtime kind-count assertions now include the agents kind, each annotated as
an intentional #1173 change.
Scope: this wires the per-runtime CONVERTER. The remaining byte-parity behaviors of
the legacy loop (copilot `.agent.md` rename, cross-cutting path/attribution rewrites,
config-reading) stay with the legacy loop -- which runs after installRuntimeArtifacts
and is authoritative for the real install -- and are tracked by ADR-1235's later
cutover steps. No user-facing change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(#1521): resolve own runtime + worktrees-off for all non-Claude installs
Generalizes the Codex-only #1515/#1519 fix to every non-Claude runtime, and
wires it into the real install path (where it was previously dead-on-arrival).
Root causes:
1. The runtime-default stamping lived only in `_applyRuntimeRewrites`, but the
installer emits `gsd-core/workflows/*.md` via `copyWithPathReplacement`, which
never calls it — so a real `--codex`/`--cursor`/etc. install emitted
`--default claude` and worktrees-on. RUNTIME mis-resolved to claude and the
workflow ran executors unisolated against the main checkout. (#1515/#1519 were
also dead-on-arrival in real installs; this repairs them.)
2. Only `case 'codex'` was stamped; every other non-Claude runtime kept the
Claude default.
Fix:
- New `_stampNonClaudeRuntimeDefaults(content, runtime)` (single shared helper)
stamps `--default <runtime>` + `use_worktrees=false` for every `runtime !=
claude`; called from both `_applyRuntimeRewrites` and, crucially,
`copyWithPathReplacement` in bin/install.js (the real workflow emit path).
- Generalize the fail-closed worktree guard `= codex` -> `!= claude` in
execute-phase/quick/diagnose-issues (worktree isolation is Claude-Code-only).
- Flip manager/autonomous inline-vs-background gating to `codex -> background,
everything-else -> inline` (research: only Codex can background-nest the
pipeline's subagents; all others run inline, which they support).
Worktree-capability determination is research-backed (official docs for all 14
non-Claude runtimes: none honor GSD's isolation="worktree" mechanism, only Codex
background-nests). New end-to-end real-install test asserts the EMITTED workflow
is stamped — the regression guard that would have caught the dead-on-arrival bug.
Closes#1521
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013vX5eUtWa2wsZEyeMf5i3r
* chore(#1521): backfill changeset PR number (#1537)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013vX5eUtWa2wsZEyeMf5i3r
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(#1515): make Codex installs resolve their own runtime and fail closed on worktrees
A Codex install with a runtime-neutral .planning/config.json resolved
RUNTIME=claude and enabled git worktree isolation, which Codex's
spawn_agent cannot honor. Two root causes:
1. Workflows read `config-get runtime` / `config-get workflow.use_worktrees`
without `--raw`, so config-get's JSON-quoted output ("codex") was captured
verbatim into the bash var and broke every `[ "$RUNTIME" = ... ]` check —
the Codex fail-closed guard was dead even when runtime:codex was explicit,
and Claude's own worktree degrade-check was dead too. Add `--raw` to those
reads across execute-phase, autonomous, manager, diagnose-issues, quick.
2. The conversion engine emitted `--default claude` for every runtime. Stamp
the codex-emitted workflows to `--default codex` (runtime) and
`--default false` (use_worktrees) in _applyRuntimeRewrites case 'codex', so
a neutral config on a Codex install resolves runtime=codex / worktrees off.
Also extend the Codex fail-closed worktree guard to quick.md and
diagnose-issues.md (they spawned isolation="worktree" with no runtime guard).
Regression test asserts source<->engine parity across all five workflows
(DEFECT.GENERATIVE-FIX) plus fast-check property coverage of the stamping.
Closes#1515
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013vX5eUtWa2wsZEyeMf5i3r
* chore(#1515): backfill changeset PR number (#1519)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013vX5eUtWa2wsZEyeMf5i3r
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Proactive checkpoint guard fires at each wave boundary before spawning agents.
Self-assesses context pressure against context-budget.md degradation tiers and
warns (warn, default) or auto-invokes /gsd:pause-work (auto) when POOR tier
(70%+) is detected. Config key validated; defaults to \"warn\".
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(#1369): refresh wave manifest and re-check base before each wave in execute-phase
Two compounding issues caused wave N+1 worktrees to fork from the stale
pre-wave-N commit, immediately tripping the worktree_branch_check FATAL
guard in every executor:
1. worktree.base-check auto-degrade only ran once at initialize time.
After wave N merges advanced orchestrator HEAD past origin/HEAD, new
worktrees were still forked from origin/HEAD (Claude Code "fresh" base).
2. WAVE_WORKTREE_MANIFEST was never unset between waves, so wave N+1
reused the consumed wave-N manifest file, which would have blocked the
step 5.5 manifest guard (#3384) on subsequent waves.
Fix: add two safeguards in execute-phase.md —
- Step 0.5 (start of each wave): re-runs worktree.base-check; auto-degrades
USE_WORKTREES=false for that wave when HEAD has diverged from origin/HEAD.
- Step 7c (end of each wave): unsets WAVE_WORKTREE_MANIFEST so wave N+1
creates a fresh per-wave manifest; re-asserts worktree.set-baseref
(idempotent) and re-evaluates base degradation after wave merges land.
17 regression tests added in tests/bug-1369-wave-stale-base.test.cjs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(#1369): rename test to fix-NNN convention; update workflow size baseline
Rename tests/bug-1369-wave-stale-base.test.cjs → tests/fix-1369-wave-stale-base.test.cjs
to satisfy the lint-regression-test-names gate (new files cannot use bug-NNN prefix).
Update tests/workflow-size-baseline.json for execute-phase.md: 93157 → 97393
(LF-normalized byte count after adding step 0.5 inter-wave base re-check and
step 7c between-wave manifest reset).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(#1369): add issue reference to allow-test-rule comment
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(#1369): extract new execute-phase steps to references; satisfy ADR-857 cap
Step 0.5 (inter-wave worktree base re-check) and steps 7b–7c (pre-wave
dependency check + between-wave manifest reset/base refresh) added by this
PR grew execute-phase.md to 97393 bytes, violating the ADR-857 phase-6
architectural mandate that host-loop bodies remain strictly below the
pre-phase-6 baseline of 93166 bytes.
Extract both new blocks into dedicated reference files:
- gsd-core/references/execute-phase-wave-guard.md (step 0.5)
- gsd-core/references/execute-phase-between-wave-reset.md (steps 7b + 7c)
Replace inline prose with @-reference pointers. File now measures 92851
bytes (LF-normalized), satisfying the ADR-857 capstone conformance gate.
Also update tests/workflow-size-baseline.json to 92851 and add both new
reference files to docs/INVENTORY-MANIFEST.json.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(#1369): update regression tests to read from extracted reference files
Steps 0.5 and 7b+7c were moved to reference files to satisfy the ADR-857
size cap on execute-phase.md. Tests now check @-reference pointers in the
workflow for ordering and read content assertions from the reference files.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(#1453): clean up stale get-shit-done paths in Codex skill mirror on upgrade
Extends planLegacyCleanup in gsd-core/bin/lib/legacy-cleanup.cjs to scan
skills/gsd-* subdirectories for .md files that still embed the pre-rename
get-shit-done/ path (e.g. ~/.agents/skills/gsd-docs-update/SKILL.md).
These stale copies are removed by cleanupLegacyGsdCc during the next
install/upgrade so Codex can no longer discover and select them.
Adds 5 regression tests to tests/issue-607-legacy-cleanup.test.cjs covering
the stale path detection, non-flagging of fresh skills and user-owned dirs,
and the end-to-end ~/.agents/skills scenario from the issue.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(#1453): add gsd-allow-legacy-name markers to intentional legacy name references
Comments and test descriptions in legacy-cleanup.cjs and its test file
legitimately cite the old 'get-shit-done' directory name to explain what
the cleanup logic removes. Add the lint-exemption marker to each line so
lint-legacy-dir-name passes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(#1478,#1479,#1480): prohibit ungrounded baselines, error-suppressing fallbacks, and stale-artifact authority in verify blocks
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore: add changeset for #1478/#1479/#1480 planner verify gate fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: correct changeset format
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(#1478,#1479,#1480): fix test contract violations from new planner dimensions
gsd-planner.md exceeded both the planner-decomposition 48K char limit and
the reachability-check 50K char limit after the new HARD RULE blocks were
added inline. The full rule details already exist in planner-antipatterns.md
(added in the same PR); replace the verbose inline blocks with a single
@-reference pointer to the antipatterns file, reducing the file from 50981
to 49130 chars (under both limits).
Also regenerate tests/agent-size-baseline.json to reflect the new sizes of
gsd-planner.md and gsd-plan-checker.md.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
#2551/#3182/#2361 are pre-migration get-shit-done-redux issue numbers with no
equivalent in open-gsd/gsd-core; they mislead triage and manufacture phantom
blockers. Repoint to real successors (#717 byte-budget rework, #720) or rewrite
as prose referencing the discuss-phase/modes progressive-disclosure split.
Correct co-located 'line budget'/'<500 lines' framing to the byte-based reality
(#717). Add a CI guard (tests/no-phantom-issue-refs.test.cjs) that fails if a
phantom ref is reintroduced. SSH-key patterns (id_ed25519) left untouched.
No user-facing runtime behavior change.
Closes#1073
* 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>
ADR-1244 Phase 5 (D7). dispatchOverlayCapabilityCommand in gsd-tools.cjs dispatches an installed third-party capability command family via loadRegistry({includeInstalled}), gated on a committed ledger entry (consent) and confined to the capability's install root (defaultRequireFromInstallRoot: bare-.cjs basename + realpath containment, rejects ../ traversal + symlink escape); same own-property/function/sync/ExitError guards as the first-party path. capability-loader records _overlay.commandRoots only for accepted overlay caps with a committed, structurally-valid ledger entry (fail closed). First-party graphify/intel/audit unchanged (already on the registry seam). 3 Codex rounds converged + /security-review (no HIGH) + /code-review (Approve); gsd-test green both platforms; CI green.
Closes#1434.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#1441): antigravity resolver prefers GSD-owned dir over first-existing
resolveConfigHomeFromDescriptor's dot-home-nested probe returned the first
bare-existing candidate, so a CLI user (~/.gemini/antigravity-cli) who also
had the IDE's ~/.gemini/antigravity dir was silently shadowed to the legacy
dir (probed first). Regression from #217 — pre-#217 returned
~/.gemini/antigravity unconditionally.
Add an optional probeMarker (gsd-core/VERSION) to the dot-home-nested
descriptor: a two-pass probe prefers the candidate GSD installed into, then
bare existence, then probe[0]. Behavior is byte-identical when probeMarker is
absent (windsurf etc. unaffected). Adds detectAntigravityDirAmbiguity() for
installer/operator guidance on already-misinstalled users (auto-relocation
ruled out per ADR-0008's single-configDir migration bound).
Regression tests fail before / pass after: coexistence + marker-priority
cases, end-to-end through the registry descriptor.
Claude-Session: https://claude.ai/code/session_01JU2WB23JLE3QTysPXuFJjB
* chore(#1441): add changeset for antigravity resolver fix
Claude-Session: https://claude.ai/code/session_01JU2WB23JLE3QTysPXuFJjB
* feat(#1431): runtime capability registry overlay (ADR-1244 Phase 2)
Promote the registry from a frozen data file to loadRegistry({includeInstalled}),
composing the first-party registry with a validated installed overlay (ADR-1244 D2):
- Extract the conformance validator to a shared runtime-callable module
(gsd-core/bin/lib/capability-validator.cjs); the generator re-exports it
verbatim, guarded by a generative-parity test (no build-time/runtime drift).
- capability-loader.cts: loadRegistry({includeInstalled}) composes first-party
∪ validated overlay from $GSD_HOME/.gsd/capabilities (global) and
<root>/.gsd/capabilities (project) via the canonical buildRegistry. First-party
always wins (id/skill/agent/config/command-family + reserved gsd-/anthropic-
prefixes); full merged-set cross-capability validation; engines.gsd load-time
re-gate (skip-with-warning); gate-kind capabilities FAIL CLOSED; fragment-path
escapes rejected.
- semverSatisfies (hand-written, no dep) for the engines.gsd gate, fail-closed.
- Wire surface/state + loop to the overlay; loop injects a blocking gate for each
skipped gate-kind overlay (fail-closed).
- cwd-aware overlay config-key federation: config-loader _federatedConfigSchema(cwd)
+ config-schema isValidConfigKey(key, cwd) compose the overlay per loadConfig/
config-set call (never eager at module load, never wrong-cwd); first-party path
unchanged with no cwd.
- run-tests.cjs sandboxes GSD_HOME (idempotent — nested spawns reuse it) for test
hermeticity; capability-loader.cjs git+eslint-ignored (tsc artifact);
capability-validator.cjs stays linted (#551 migration coverage).
Closes#1431
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(#1431): add changeset for runtime capability registry overlay
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(#1431): kill config-schema cwd-aware federation mutants (Stryker ≥52)
The cwd-aware overlay config-key federation added to config-schema.cts
(_capabilityConfigSchema(cwd) + isCapabilityConfigKey/isValidConfigKey cwd
threading) introduced mutable surface uncovered by config-schema's mutation
test set, dropping its score to 39.58% (below the 52 break threshold). Add a
real-overlay-fixture describe block exercising every branch (cwd guard, overlay
loadRegistry, found-branch, first-party fallback, cwd threading); local Stryker
score 39.58% -> 77.08%.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(#1430): versioned capability manifest + native stamping (ADR-1244 Phase 1)
Make the capability manifest versioned — the data substrate the Capability
Ecosystem (ADR-1244) keys off:
- capability.json gains a REQUIRED semver `version` plus the optional
ecosystem envelope (`engines.gsd`, `compatVersions`, `integrity`,
`provenance`); the build-time conformance validator enforces them via a new
`validateVersionEnvelope()` (exported for the Phase 2 runtime overlay).
- All 32 native capabilities stamped with `version` (= package version,
lockstep) + `engines.gsd`; `sync-manifest-versions.cjs` gains a glob sweep
that keeps them in sync, and the issue-844 regression guard is extended.
- Strict SemVer 2.0.0 grammar blocks metacharacter/space/unicode smuggling in
version strings; range/integrity fields are shape-validated (satisfaction
and the load-time gate are deferred to Phase 2/4).
- Capability rel-paths emitted forward-slash for cross-platform git correctness.
Closes#1430
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(#1430): add changeset for versioned capability manifest
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(#1355): detect-and-warn guard for claude-code agent-teams
GSD's multi-agent orchestration can stall under claude-code's experimental
agent-teams (a subagent's completion fails to route to the orchestrator). Per
the maintainer decision, the accepted scope is a read-only detector + one
non-fatal warning — NOT the declined run_in_background/TaskOutput conversion.
- New Teams Status Module (src/teams-status.cts → gsd-core/bin/lib/teams-status.cjs):
pure resolveTeamsStatus({runtime, env}) + thin CLI cmdTeamsStatus reusing
resolveRuntime. active = strictly-truthy env flag AND runtime === 'claude'.
- Wire `gsd-tools query teams-status [--active]` (read-only; no capability
registration needed — conformance gates govern features, not query commands).
- One non-fatal warning in plan-phase.md before the first Agent spawn, gated on
`query teams-status --active`; zero behavior change on non-claude/teams-off.
- Hermeticity: clear CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS in run-tests.cjs +
SESSION_ENV_KEYS. Docs reference + CONTEXT.md glossary. Built lib gitignored.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore(#1355): add changeset for teams-detect guard
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(#1355): bump plan-phase.md workflow size baseline (+407B for teams warning)
The non-fatal agent-teams warning block added to plan-phase.md grew it
92759 → 93166 bytes, past its committed per-file baseline ratchet. The growth
is small, deliberate, and still well under the workflow tier hard cap. Regenerate
the baseline via `npm run size:baseline` (only plan-phase.md changed).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore(#1355): register teams-status.cjs in the inventory manifest
The new teams-status CLI module is a tracked surface; regenerate
docs/INVENTORY-MANIFEST.json (cli_modules family) via
gen-inventory-manifest.cjs --write so the inventory-manifest-sync gate passes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#1359): migrate workflows off deprecated TaskOutput to Read(outputFile)
The map-codebase and docs-update workflows collected background sub-agent
results with the deprecated Claude Code `TaskOutput` tool using `block: true`,
which has a confirmed main-session hang after the agent completes
(anthropics/claude-code#20236).
Migrate the collection steps to the upstream-recommended pattern: keep
`run_in_background=true` on the Agent spawn, then `Read` each agent's
`outputFile` (from the `async_launched` result) once it reports completion.
Completion-marker contracts and on-disk verification are unchanged, and the
non-Claude runtime fallbacks (sequential_mapping / sequential_generation) are
preserved byte-for-byte. docs-update's timeout note no longer references the
unwired `workflow.subagent_timeout` key (it kept a literal before).
Regression coverage folded into tests/subagent-timeout.test.cjs (the owning
module for background-subagent collection). Workflow size baseline regenerated
for the justified prose growth.
Refs #1355 (same latent hang surface).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore(#1359): add changeset for TaskOutput migration
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Delivers option (a) from the #1314 maintainer review: thread a fourth flat
scalar check_violation_fixture through the projection so a prohibition authored
at spec-phase machine-proves fail-first and greens through the deterministic
path alone — zero hand-authoring at verify time.
- src/probe-core.cts: Prohibition gains check_violation_fixture?; projectProhibitions
emits it (both kinds) ONLY for a well-formed descriptor and ONLY when non-empty
(blank/absent -> projects absent -> producer hard-gates, never a partial green).
- src/prohibition-enforcement.cts: descriptorFromProjection reads it back into
violationFixture via the same numeric-coercion-safe scalar() normalizer.
- Tests (RED-first, proven non-vacuous by reverting both src edits): CHK-02(#1346)
projection emit, CHK-08(#1346) read-back, CHK-03(D) example round-trip, the
fast-check round-trip property extended to the 4th scalar (the contract trek-e
blocked #1301 on), and a real-subprocess COMPOSE capstone greening end-to-end
through project -> descriptorFromProjection -> default prover+runner.
- Docs flipped from 'hard-gates until #1346' to 'composes end-to-end': verify-phase.md,
prohibition-probe.md, spec-phase.md authoring, ADR-550 addendum, changeset.
#1346 now tracks only the node-test causation residual.
190 affected-suite tests green; eslint + tsc clean; size baseline regenerated.
Addresses the #1314 maintainer review (trek-e):
- Major 1 (fail-OPEN): defaultProveFailFirst's node-test branch only guarded
`if (!fixture)`. A missing/typo'd/stale violationFixture made GSD_PROHIB_SUBJECT
point at a missing file; an honest negative test threw ENOENT *inside its
callback* (a failing test named distinctly from the file), which
isNonVacuousNodeTestRed accepted as proof -> a green forged from a setup crash.
Now requires fs.existsSync(path.resolve(cwd, fixture)) before spawning, symmetric
with the lint-rule path's file-result guard. Regression test pins it (RED without
the guard); a second test pins cwd-relative fixture resolution.
- Major 2 (misleading prose): the #1278 projection carries no violationFixture, so
the deterministic-locate path always hard-gates (fail-closed) until a
check_violation_fixture scalar is threaded through. verify-phase.md and
prohibition-probe.md no longer read as if the projected path produces greens;
the ADR-550 addendum records both items. Tracked as follow-up #1346.
- Documented residual: existence is necessary but not sufficient (a red caused by
the env being set vs the subject's content); recorded as a constraint, in #1346.
- Nit: stale 'NOT attested fail-first' comment -> 'NOT machine-proven fail-first'.
64 tests pass; eslint + tsc clean; changeset valid.
git-base-branch.cjs is the compiled output of src/git-base-branch.cts (emitted by `npm run build:lib`), but was the only TS-generated artifact under gsd-core/bin/lib/ still tracked in git — its 117 ADR-457 build-at-publish siblings are all gitignored. The committed copy had already drifted stale (build:lib regenerates a 1-line comment diff), producing spurious worktree diffs.
Add it to the ADR-457 block in .gitignore and git rm --cached it, matching its siblings. The working-tree copy is regenerated by build:lib (pretest/prepublish); module behavior and tests/git-base-branch.test.cjs are unchanged.
Closes#1321
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Adds a region/function-scoped negative-grep idiom to the gsd-planner verification guidance plus a warn-only `validate_plan` check (`scanFileWideNegativeGateConflict`) that flags when a task's file-wide negative grep bans a construct a sibling task legitimately requires elsewhere in the same file. ReDoS-safe (linear, no RegExp on author patterns); region-scoped gates are exempt. Warn-only — never errors, never flips `valid`.
Closes#968
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(1278): RED-first descriptor parity + fail-closed guards + CHK-07 byte-stability (wave 1)
- CHK-03 (RED): extend PROB-14 parity in prohibition-probe.schema.test.cjs to carry the flat
check_kind/check_target/check_rule scalars through project->write->parseMustHavesBlock; the
non-droppable check_kind-presence assertion is the load-bearing RED trigger (fails because
projectProhibitions strips check_* on the current build).
- CHK-07 (GREEN forward-guard): probe-core.test.cjs pins descriptor-less byte-stability +
dispositionForProhibition fail-closed policy, with a t.todo marker forward-locking plan 01-02.
- CHK-06 (RED): prohibition-enforcement.test.cjs asserts descriptorFromProjection export +
fail-closed on absent/partial/unknown descriptors via the projection adapter (RED until 01-03).
- No src/*.cts or .cjs edits; no new test files; lint-test-file-count clean.
* feat(1278): add optional flat-scalar check descriptor fields to Prohibition interface (wave 2)
- check_kind?/check_target?/check_rule? mirror CheckDescriptor.kind/target/rule (minus caller-attested failFirst, #1279)
- optional so existing Prohibition consumers compile unchanged
* feat(1278): project check descriptor as flat scalars in projectProhibitions (wave 2)
- emit check_kind/check_target (+ check_rule only for lint-rule with a rule) when descriptor well-formed
- under-specified/descriptor-less items project byte-identically (CHK-07); flat scalars ride existing parseMustHavesBlock continuation-KV path (no parser rewrite)
- add CHK-02 probe-core unit cases pinning the projection
- turns CHK-03 parity test GREEN; dispositionForProhibition untouched
* feat(1278): descriptorFromProjection read-back adapter feeds fail-closed locate (wave 3)
- Add descriptorFromProjection(projected) -> CheckDescriptor | null to
src/prohibition-enforcement.cts: renames the projected flat scalars
check_kind/check_target/check_rule -> {kind,target,rule?}, or null when
the descriptor is absent/non-object (no check_kind key).
- failFirst is NEVER sourced from the projection (stays caller-attested; #1279).
- rule is set only when check_rule is a non-empty string; the adapter does NOT
re-validate kind/target/rule — an under-specified descriptor reconstructs to
one the EXISTING runProhibitionEnforcement LOCATE guard rejects (located:false,
never green). The merged #1259 guard stays the single source of fail-closed truth.
- Turns the RED CHK-06 fail-closed tests (plan 01-01) GREEN end-to-end; CHK-03 /
CHK-07 stay green. CheckDescriptor type, locate guard, dispositionForProhibition,
and parseMustHavesBlock are unchanged (additive +36/-0).
* feat(1278): verify-phase locates prohibition check from projected descriptor (wave 3)
- request.check kind/target/rule sourced from projected check_kind/check_target/check_rule via descriptorFromProjection, not verifier invention (CHK-05)
- replaces the #1278 author-supplied / tracked-follow-up note with the delivered deterministic-locate behavior
- preserves fail-closed routing: absent/partial descriptor -> never green, hard-gate in both modes
- failFirst stays a verify-time caller attestation; #1279 bounds the remaining fail-first proof
* feat(1278): spec-phase captures wired-check descriptor on test-tier resolution (wave 3)
- Step 5.6 'Keep it' / verification: test path captures check_kind/check_target/check_rule, projected onto must_haves.prohibitions for verify-phase deterministic locate (CHK-04)
- SOFT capture: a test-tier prohibition without a descriptor is still allowed (no hard authoring block); stays fail-closed/flagged downstream
- --auto captures only an unambiguous descriptor, never fabricates a check path
- failFirst NOT captured at spec-phase (verify-time attestation; #1279)
- PROB-06 soft-gate + text-mode (PROB-09) behavior unchanged
* chore(1278): re-baseline workflow size for grown verify-phase + spec-phase prose (wave 3)
- spec-phase.md 28438 -> 30343 (+1905), verify-phase.md 35362 -> 36498 (+1136)
- regenerated via npm run size:baseline (no hand-picked numbers); growth is the #1278 deterministic-locate + descriptor-capture prose
- workflow-size-budget guard green (122/122)
* docs(1278): ratify optional check descriptor in dated ADR-550 addendum + type:Changed changeset
- Append dated 2026-06-15 ADR-550 addendum ratifying the D3 prohibition-item
shape extension (optional flat-scalar check_kind/check_target/check_rule)
- Document flat-scalar rationale, deterministic projection/read-back,
fail-closed on partial/invalid/absent, #1279/policy out-of-scope
- Add .changeset/1278-prohibition-check-descriptor.md (type: Changed)
* docs(1278): document optional check descriptor in prohibition-probe reference + FEATURES
- Add 'Optional wired-check descriptor (deterministic locate, #1278)' section
to the prohibition-probe reference (flat-scalar keys, projection/read-back,
fail-closed + backward-compat, failFirst stays attested)
- Add deterministic prohibition-check descriptor source entry to FEATURES.md
- No CONTEXT.md glossary change: descriptor reuses existing wired-check /
verification:test vocabulary, no new glossary term introduced
* fix(1278): pass packaging gates — changeset pr field + retired slash-form fix
- Add required pr: 1278 to changeset (lint:changeset MISSING_PR hard requirement;
plan's 'omit if unknown' was inaccurate — issue number per #1259 convention,
updated to real PR number when opened) [Rule 3 - blocking]
- Fix retired /gsd-spec-phase -> /gsd:spec-phase at verify-phase.md:83 (wave-3
prose; caught by slash-namespace invariant #3443/bug-2543, blocked CHK-09
full-suite-green) [Rule 1 - bug]
- size:baseline + INVENTORY manifest verified in-sync post-build (no diff)
* docs(1278): add check descriptor + descriptorFromProjection to CONTEXT.md prohibition glossary
* fix(1278): harden descriptorFromProjection round-trip (numeric-coercion + stray-rule) per review
- MD-01/LW-01: narrow projected scalars to primitives + String()-coerce, so a
numeric-looking check_target (parseMustHavesBlock coerces ^\d+$ to number)
reconstructs as a string and locates instead of silently un-locating; no
as-string type-lie, satisfies no-base-to-string.
- LW-02: attach rule only for the lint-rule kind (drop a stray node-test rule).
- LW-03: document the optional check_* keys in the reference Output schema.
RED->GREEN tests added in prohibition-enforcement.test.cjs.
* chore(1278): set changeset pr to 1301
* test(1278): add fast-check property for the check-descriptor round-trip + fail-closed (trek-e review)
RULESET.TESTS.property-based-testing: the projectProhibitions -> render ->
parseMustHavesBlock -> descriptorFromProjection chain is a bijective/transformation
contract. Adds 2 fc properties to tests/probe-core.property.test.cjs (no new file;
ratchet stays at 2 for probe-core):
- well-formed descriptors survive the round-trip across the full string domain
incl. the numeric-coercion case (target/rule reconstruct as strings);
- under-specified/invalid descriptors (absent / target-less / rule-less /
unknown-kind) are always fail-closed (never green, flagged, unlocated).
Stability is asserted at the descriptorFromProjection layer (the raw parse step is
intentionally lossy for numeric scalars; the shared parser is unchanged).
---------
Co-authored-by: Tom Boucher <trekkie@nomorestars.com>
Add an optional activationKey to the feature role of capability.json — the
dotted config key that gates the whole capability (e.g. graphify.enabled).
gen-capability-registry validates it (non-empty string, reserved-name guard,
must be declared in the capability's own config slice, feature-only) and emits
it per-capability in the generated registry. Declared on graphify + intel.
No runtime consumption yet (resolver wiring lands in #1305). Part of #1302.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* enhance(execute): isolated-executor rejected/over-reaching run fails safe (#1292)
When an isolated (worktree) executor run is rejected — the user declines to
merge it, the orchestrator surfaces recovery for a blocked/halted plan, or the
run over-reached the requested scope — the orchestrator must no longer
default/propose recovery by editing the primary checkout (`main`). Absent an
explicit guardrail, the LLM orchestrator could improvise "continue on main",
inverting the isolation contract at the moment it matters most.
Added an ISOLATED-RUN RECOVERY — FAIL SAFE policy: default to a safe halt that
offers a fresh, narrowly-scoped worktree or inspect/discard; editing the primary
checkout requires explicit, clearly-labeled confirmation and is never the
default/proposed option.
To respect the ADR-857 phase-6 host-loop size cap on execute-phase.md (it sits
just under the pre-phase-6 baseline), the policy is delivered as an extracted
reference fragment rather than inline:
- New `execute-phase/steps/worktree-recovery-policy.md` holds the recovery policy
(the existing FAIL-CLOSED rule #48 for base/HEAD mismatches + the #1292
fail-safe guardrail). No #48 behavior change — moved verbatim.
- execute-phase.md references the fragment at the worktree-spawn recovery point,
the step-5.5 merge decision, and the stalled-agent "switch to inline execution"
menu (which for an isolated run now follows the fail-safe policy). Net effect:
execute-phase.md shrinks below its cap.
- quick.md carries the fail-safe guardrail inline at its post-return merge/discard
decision (quick.md is not size-capped).
Scoped to the recovery offer only — no automatic scope-overreach detection
(explicitly out of scope per the issue) and no new config key. Adds content
regression tests, a USER-GUIDE note, and a workflow size-baseline update.
Closes#1292
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore(changeset): Changed fragment for #1292 isolated-executor fail-safe recovery
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#1296): align config docs/prompts/schema with consumers
The user-facing config surface disagreed with what the consumers actually do
(subset of the #1216 audit). No runtime consumption behavior changes.
- workflow.subagent_timeout: settings-advanced.md prompt + docs/CONFIGURATION.md
said "seconds (default 600)" but the consumer (map-codebase.md) uses
milliseconds (default 300000). Relabeled all four spots in settings-advanced.md
(prompt, parse-default list, example, confirmation table) + the CONFIGURATION.md
row.
- review.models.<cli>: settings-integrations.md, docs/CONFIGURATION.md (Integration
Settings), and docs/CLI-TOOLS.md documented a shell command, but review.md injects
the value into a --model/-m flag. Relabeled to a bare model id and reconciled the
contradictory CONFIGURATION.md sections.
- workflow.test_command + workflow.build_command: consumed via config-get
(test_command in verify-phase/execute-phase/audit-fix/post-merge-gate;
build_command in post-merge-gate) and documented, but absent from validKeys so
`config set` rejected them. Registered both in config-schema.manifest.json and
documented them in references/planning-config.md (overview + complete reference).
Regression tests: behavioral config-set tests (tests/config.test.cjs) + doc-parity
content guards (tests/config-field-docs.test.cjs).
Deferred to other #1216 clusters: security-gate wiring, search_gitignored wiring,
mvp_mode, source_grounding_authority labeling, and config-set enum enforcement.
Closes#1296
Refs #1216
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore(changeset): Fixed fragment for #1296 config-surface alignment
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* refactor(#1294): T-final — delete the core.cjs re-export spine (epic #1267 complete)
After T0–T6 nothing imports core, so retire the spine and its scaffolding:
- delete src/core.cts (and the gitignored gsd-core/bin/lib/core.cjs artifact;
remove its .gitignore + eslint-ignore entries)
- delete scripts/lint-core-spine-imports.cjs + its allowlist; drop it from the
package.json lint:ci chain
- regenerate docs/INVENTORY-MANIFEST.json (drops the core.cjs surface)
- sweep stale references: CONTEXT.md glossary back-compat clauses (spine retired,
callers import the leaf directly), planning-config.md CONFIG_DEFAULTS owner,
and false present-tense core.cjs claims in leaf-module docstrings
The ADR-857 decomposition is complete: the former Core god-module is fully
dissolved into its leaf modules; no re-export spine remains. No behaviour change.
Closes#1294
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#1294): migrate the computed-path core.cjs importers the literal grep missed
bin/install.js used require(path.join(_gsdLibDir, 'core.cjs')) (a computed
path, and bin/install.js was never in the convergence lint's scan roots), and
~8 test files referenced core.cjs via path.join/readFileSync/existsSync/FILE_ARG
forms the literal-string migration grep missed. Route install.js's symbols to
their leaves (RUNTIME_PROFILE_MAP->model-catalog, resolveTierEntry/EFFORT_SET->
model-resolver) and repoint/adjust the test references to the leaves. Recovers
the 161 'Cannot find module core.cjs' failures from the spine deletion.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* enh(#966): gate behavior-dependent truths on behavioral evidence in gsd-verifier
Introduce a per-truth PRESENT_BEHAVIOR_UNVERIFIED state for must-haves that assert a state transition or a cancellation/cleanup/ordering invariant whose only evidence is symbol presence + wiring. Such truths are excluded from the verified_truths score, reported as a behavior_unverified count, recorded in an always-on behavior_unverified_items frontmatter list, and routed to the existing human_needed sink — so a clean N/N can no longer be reached on symbol presence alone.
The overall-status vocabulary and the src/verification.cts seam are unchanged (the new state is per-truth only); gaps_found keeps decision-tree precedence; override-passed truths still count toward verified_truths. Mirrors the calibration into the shipped verify-phase.md workflow (with an infra/foundation carve-out), the VERIFICATION.md templates, and docs (planning-artifacts.md, AGENTS.md). gsd-verifier.md kept under its 48KB LARGE cap; size baselines regenerated.
Closes#966
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore(#966): add changeset fragment for gsd-verifier behavior-unverified calibration
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Maintainer CHANGES_REQUESTED (reviewed e08667e5, pre-portability-fix):
- B1: lint-rule no longer greens an unparseable target (eslintHasFatalError -> fail
closed on any fatal/parse error) or an inline-suppressed violation (eslintJsonHasRule
now scans suppressedMessages too). RED-first + real-runner repros.
- B2: both child spawns get a bounded timeout (30s node / 60s eslint) + 16MiB maxBuffer;
timeout fails closed. Injectable timeoutMs (positive-only — 0/negative can't disable
the bound) enables a fast 1.5s hang test.
- M1: scoped verify-phase.md — the check descriptor is author-supplied for now; filed
#1278 for deterministic auto-locate of the descriptor (the locate half).
- M2: added tests/prohibition-enforcement.property.test.cjs (fast-check fail-closed
invariants; within the <=2-file budget).
- m1: tapTestNames excludes # SKIP/# TODO; parseNodeTestSummary tracks # cancelled;
isNonVacuousNodeTestPass requires cancelled===0.
- m2: scoped the determinism claim to the decision/parse layer (real runner is env-dependent).
- m3: filed #1279 for machine-proven fail-first (violation-fixture probe).
- n1: -- before target in both arg builders (option-injection). n2: dropped dead token.
- B3 (Windows npx) was already fixed in 2af76306 (pushed ~65s after the review).
Verified node 22 + 24; size baseline regenerated for the verify-phase note.
Re-home the 6 implementation functions squatting in the core.cjs re-export
spine (ADR-857) into the modules whose interface they belong to, with core
re-exporting them BY REFERENCE so all 32 callers + the shim-identity tests
keep resolving unchanged:
- worktree-safety: resolveWorktreeRoot, pruneOrphanedWorktrees
- git-base-branch (broadened to the Git Query Module): gitWorktreeInfoInternal
- agent-install-check (new leaf): getAgentsDir, checkAgentsInstalled
- delete the _resetRuntimeWarningCacheForTests wrapper; consumers use a
shared resetRuntimeWarningCaches() helper in tests/helpers.cjs
Add scripts/lint-core-spine-imports.cjs (migration-convergence lint with a
30-importer allowlist, wired into lint:ci) so the staged spine retirement
provably converges: CI fails on any new ./core import. Register the new
generated agent-install-check.cjs in eslint-ignore + .gitignore +
INVENTORY-MANIFEST.json.
No behaviour change. First tranche (T0) of epic #1267.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Adversarial pre-submission review found the injected-runCheck tests masked a
non-functional real runner. Fixes:
- BL-01 (false green on vacuous test): the node-test runner now parses the TAP
summary and requires a NON-VACUOUS pass (>=1 test, >=1 pass, 0 fail) AND a
reported test named distinctly from the file — node --test counts an empty file
as one passing test, so counts alone could not catch it.
- SF-01 (lint anchor never greened): the lint-rule runner now runs the project
eslint as --format json and filters by ruleId, so plugin rules (local/*) load
via the flat config — bare --rule cannot load a plugin. local/no-source-grep
now genuinely greens (covered by a real, non-injected test).
- BL-02 (tautological fail-first): the runner no longer echoes the caller's
failFirst as if confirmed. failFirst is documented as caller-ATTESTED; the
producer requires attestation + a genuine non-vacuous pass. Machine-proven
fail-first (needs a violation fixture) is flagged as a tracked follow-up in
ADR-550, the changeset, FEATURES, the reference doc, and verify-phase.
- SF-02: added real-runner end-to-end tests (no injected runCheck) + pure,
exported parse/filter helpers (parseNodeTestSummary, tapTestNames,
eslintJsonHasRule, eslintFileResultCount) so the shipping branches are
mutation-pinned.
- NIT-01/02: LOCATE guard rejects empty-string rule and unknown kinds.
- Hardening: spawn checks with NODE_TEST_CONTEXT/NODE_OPTIONS scrubbed so an
ambient test-runner context cannot corrupt a verify-time result.
- Docs reconciled to the shipped behavior (no 'confirms fail-first' overclaim).
The default lint-rule runner passed check.target as BOTH the --rule id and the
eslint path, so it could never pass (eslint tried to lint a file named after the
rule). Add a distinct check.rule field (rule id) vs check.target (path to lint),
extract a pure exported buildLintArgs() so the mapping is mutation-testable
without spawning eslint, fail-closed on a lint-rule missing its rule id, and carry
the rule into enforcement evidence. Updates verify-phase descriptor docs.
Edge-probe now surfaces a zero-classification requirement (non-empty prose, no
shape cue matched, no `shapes` override) as a single soft `unclassified — review
manually` candidate instead of silently dropping it — the exact blind spot the
probe exists to catch. Dismissible like any edge; the `shapes: []` opt-out stays
silent; `TAXONOMY` (the closed 8 categories) is unchanged. Under `--auto` the
candidate is left `unresolved`, never auto-`backstop` (a missing shape is not
evidence an edge exists).
Closes#1110