182 KiB
Context
Format: this document is machine-greppable. Each operational fact is a single-line predicate (
CLASS.subkey=value). Agent briefs cite predicates by ID verbatim (perMETA.RULE.brief-must-cite-doc) — never paraphrase from this file. New learnings go in as predicates; chronological prose belongs in the session log at the bottom.
Glossary — Domain modules and seams
Milestone Module
Module owning milestone complete (archive roadmap/requirements/phases, build MILESTONES.md entry, update STATE.md), requirements mark-complete (checkbox + table update with regex-global-state fix), and phases clear. Key behaviors: milestone-phase scoping (extract phases from ROADMAP.md milestone slice, support project-code-prefix dirs e.g. CK-01-name, exclude prior-milestone phases), milestone-archive layout (resolve phase dirs from .planning/milestones/v*-phases/ when .planning/phases/ absent), fenced-code-block boundary tracking in extractCurrentMilestone. Source of truth: gsd-core/bin/lib/milestone.cjs (query handlers for milestone.complete, phases.archive). Test consolidation: PR #3753 (10 files → 4). (The SDK milestone surface and GSD.run() milestone runner were retired with the SDK package per ADR-0174.)
Dispatch Pipeline Module
Module that composes Dispatch Policy Module, Query Execution Policy Module, and per-stage handlers (input-validation, plan, execution, result-builder, formatting, error-mapping, observability) into the end-to-end pipeline that produces a QueryDispatchResult. The SDK-era pipeline collapsed onto the Command Routing Hub per ADR-0174; current dispatch seam: gsd-core/bin/lib/command-routing-hub.cjs (see Command Routing Hub below).
Phase Id Module
Module owning the pure phase-id parsing and matching helpers: phase-name normalization, phase-token extraction/matching, milestone- and phase-dir id parsing, and phase-markdown regex builders (escapeRegex, normalizePhaseName, comparePhaseNum, extractPhaseToken, phaseTokenMatches, phaseMarkdownRegexSource/phaseMarkdownRegexSourceExact, getMilestoneFromPhaseId, getPhaseDirFromPhaseId). Pure string/regex — no I/O, no config, no other core dependency. Extracted from the Core module per ADR-857 rollout phase 2a (#865) as the cycle-free leaf that unblocks the roadmap-parser and phase-locator extractions; the core.cjs re-export spine was retired in epic #1267, so callers import this leaf directly. Source of truth: gsd-core/bin/lib/phase-id.cjs (generated from src/phase-id.cts).
Phase Lifecycle Module
Module owning phase create, rename, complete, remove, list, and plan-index operations, plus phase-dir prefix validation, STATE.md staleness detection, and auto-prune behaviour. Entry point: gsd-core/bin/lib/phase.cjs (CJS surface). Typed phase events: GSDPhaseStartEvent, GSDPhaseStepStartEvent, GSDPhaseStepCompleteEvent, GSDPhaseCompleteEvent. (The SDK native-query surface, the types.ts event definitions, phase-runner.ts, and phase-prompt.ts were retired with the SDK package per ADR-0174.)
Phase Locator Module
Module owning phase-directory search and location: active-phase discovery against the .planning/phases/ tree (searchPhaseInDir, findPhaseInternal) and archived-phase-dir enumeration (getArchivedPhaseDirs), matching phase ids/tokens against the filesystem. Depends only on leaf modules (phase-id for token/name matching, core-utils for fs-scan/path helpers, planning-workspace for planningDir) — no loadConfig, no other core dependency. Extracted from the Core module per ADR-857 rollout phase 2d (#881); the core.cjs re-export spine was retired in epic #1267, so callers import this leaf directly. Source of truth: gsd-core/bin/lib/phase-locator.cjs (generated from src/phase-locator.cts).
Dispatch Policy Module
Module owning dispatch error mapping, fallback policy, timeout classification, and CLI exit mapping contract.
Canonical error kind set:
unknown_commandnative_failurenative_timeoutfallback_failurevalidation_errorinternal_error
Command Definition Module
Canonical command metadata Interface powering alias, catalog, and semantics generation.
Query Runtime Context Module
Module owning query-time context resolution for projectDir and ws, including precedence and validation policy used by query adapters.
Native Dispatch Adapter Module
Adapter Module that satisfies native query dispatch at the Dispatch Policy seam, so policy modules consume a focused dispatch Interface instead of closure-wired call sites.
Query CLI Output Module
Module owning projection from dispatch results/errors to CLI { exitCode, stdoutChunks, stderrLines } output contract.
STATE.md Document Module
Module owning STATE.md parse, field extraction, field replacement, status normalization, and frontmatter reconstruction. It does not scan .planning/phases and does not own persistence or locking; phase/plan/summary counts arrive from inventory/progress Modules as inputs, and read-modify-write paths remain Adapters. Source of truth: gsd-core/bin/lib/state-document.cjs.
Query Execution Policy Module
Module owning query transport routing policy projection (preferNative, fallback policy, workstream subprocess forcing) at execution seam.
Query Subprocess Adapter Module
Adapter Module owning subprocess execution contract for query commands (JSON/raw invocation, @file: indirection parsing, timeout/exit error projection).
Query Command Resolution Module
Canonical command normalization and resolution Interface (query-command-resolution-strategy) used by internal query/transport paths after dead-wrapper convergence.
Command Topology Module
Module owning command resolution, policy projection (mutation, output_mode), unknown-command diagnosis, and handler Adapter binding at one seam for query dispatch.
Init Command Module
Module owning the init.* family of query handlers that compose atomic queries into the flat JSON bundles consumed by init workflows (/gsd-execute-phase, /gsd-plan-phase, /gsd-verify-work, /gsd-new-project, /gsd-manager, /gsd-progress, /gsd-resume, etc.). Source of truth: gsd-core/bin/lib/init.cjs — the basic handlers (plus withProjectRoot project-identity injection) and the 3 heavyweight handlers (initNewProject, initProgress, initManager). All handlers return { data: <flat JSON> }. Test seams: tests/init.test.cjs and tests/init-manager.test.cjs (cover withProjectRoot precedence, progress/manager precedence regression #2674, workstream scoping regression #3196, and cross-milestone dependency regression #2267). (The SDK handlers/init/*.ts sources and the init*.test.ts seams were retired with the SDK package per ADR-0174.)
Command Routing Hub
Single dispatch seam (gsd-core/bin/lib/command-routing-hub.cjs) that centralizes CJS routing, the no-throw pure-result contract, typed error variants, and dispatch-event emission for all command family adapters. Interface: createHub({ cjsRegistry, manifest, logger }) → hub; hub.dispatch({ family, subcommand, args, cwd, raw, parentTraceId? }) → Result where Result = { ok: true, data } | { ok: false, kind, ...typedPayload } and kind ∈ { UnknownCommand, InvalidArgs, HandlerRefusal, HandlerFailure }. The Hub is single-runtime (no mode selection, no sdkLoader), never prints, never exits, never throws. Adapters call createHub, dispatch, then translate the pure Result to output()/error() calls. Source: gsd-core/bin/lib/command-routing-hub.cjs; ADR: docs/adr/0174-retire-gsd-sdk-package-boundary.md.
Runtime Source Layout Module
Single-runtime seam layout for this repository after SDK retirement. Runtime execution paths live under gsd-core/bin/lib/ and are grouped by seam concern (dispatch, manifest, handlers, runtime, observability, installer). ADR-0174 preserves the seam vocabulary and defines the canonical long-term shape as a seam-aligned TypeScript src/ tree (src/dispatch/, src/handlers/, src/errors/, src/manifest/, src/config/, src/state/, src/workstream/, src/runtime/, src/cli/, src/observability/) compiled to CJS.
Runtime Launcher Module
Canonical space-safe shell preamble (gsd_run) used by every workflow bash block to invoke the GSD runtime CLI. Resolves gsd-core/bin/gsd-tools.cjs via node when present, falls back to a gsd-tools binary on PATH, else errors. Single source of truth: gsd-core/workflows/_runtime-launcher.snippet.sh; propagated by scripts/sync-runtime-launcher.cjs; enforced by tests/runtime-launcher-parity.test.cjs. Replaced the retired unquoted $GSD_SDK variable (#373). gsd_run is also shipped as a standalone executable (gsd-core/bin/gsd_run) via the npm bin field; on runtimes that run each fenced bash block in a fresh shell (e.g. Claude Code), the per-file preamble appends the bin directory to CLAUDE_ENV_FILE so gsd_run resolves from PATH in later blocks — the inline function definition remains the fallback for all other runtimes.
Dispatch Observability Module
Module owning dispatch-event creation, redaction, and logger behavior for the Command Routing Hub. Core files: gsd-core/bin/lib/observability/event.cjs, gsd-core/bin/lib/observability/logger.cjs, gsd-core/bin/lib/observability/redaction.cjs. Contract: silent on success by default, structured JSON to stderr on error, and opt-in audit trail at .planning/.gsd-trace.jsonl via GSD_AUDIT=1 or config (audit.enabled). Each dispatch carries a traceId; composed dispatches set parentTraceId for correlation.
Query Pre-Project Config Policy Module
Module policy that defines query-time behavior when .planning/config.json is absent: use built-in defaults for parity-sensitive query Interfaces, and emit parity-aligned empty model ids for pre-project model resolution surfaces.
Configuration Module
Module owning legacy-key normalization, defaults merge, and explicit on-disk migration for .planning/config.json. Interface: normalizeLegacyKeys(parsed) → { parsed, normalizations[] } (idempotent, pure, returns the list of normalizations applied), mergeDefaults(parsed) → MergedConfig (deep-merge of parsed config over canonical defaults), migrateOnDisk(cwd) → MigrationReport (explicit, opt-in, called by the installer and by gsd-tools migrate-config). Invariants: legacy top-level keys (branching_strategy, sub_repos, multiRepo, depth) are normalized into their canonical nested locations in the returned value; defaults come from the shared gsd-core/bin/shared/config-defaults.manifest.json; schema (VALID_CONFIG_KEYS, RUNTIME_STATE_KEYS, DYNAMIC_KEY_PATTERNS) comes from gsd-core/bin/shared/config-schema.manifest.json. Note: loadConfig (project config read + merge) was extracted to the Config Loader Module (config-loader.cjs) per ADR-857 phase 2e (#885); configuration.cjs now provides only the pure normalization and defaults primitives that config-loader.cjs depends on. Source of truth: gsd-core/bin/lib/configuration.cjs, consumed via bin/lib/config-loader.cjs and bin/lib/config-schema.cjs. Eliminates the recurring #3523-class drift bug structurally.
Planning Workspace Module
Module owning .planning path resolution, active workstream pointer policy (session-scoped > shared), pointer self-heal behavior, and planning lock semantics for workstream-aware execution.
Workstream Inventory Module
Module owning workstream directory discovery, per-workstream state projection, phase/plan/summary counting, roadmap-declared phase count, active marker projection, and active-workstream collision inputs. Command handlers render list/status/progress outputs from this inventory instead of rescanning .planning/workstreams/* directly. Source of truth for the pure projection is gsd-core/bin/lib/workstream-inventory-builder.cjs (a Builder Module); the Reader Adapter gsd-core/bin/lib/workstream-inventory.cjs collects filesystem inputs and delegates projection to the Builder.
Project-Root Resolution Module
Module owning project-root resolution from any starting directory. Walks the ancestor chain (bounded by FIND_PROJECT_ROOT_MAX_DEPTH = 10) applying five heuristics in order: (0) own .planning/ guard (#1362), (1) parent .planning/config.json sub_repos traversal, (2) legacy multiRepo: true boolean + ancestor .git, (3) .git heuristic with parent .planning/, (4) nearest-ancestor .planning/ walk-up (#1414, epic #1411) — a last-resort second walk (same depth bound, stops at os.homedir()) that anchors a plain descendant subdirectory of a single-repo project to its nearest ancestor .planning/ instead of degrading to defaults; ordered after (1)–(3) so sub_repos/multiRepo resolution always wins (the Resolution Provenance deterministic-anchoring rule). Returns startDir when no ancestor qualifies. Sync node:fs I/O. Source of truth: gsd-core/bin/lib/project-root.cjs; the core.cjs re-export spine was retired in epic #1267, so callers import this leaf directly.
Planning Path Projection Module
Module owning projection from project/workstream context to concrete .planning paths. Policy precedence is explicit workstream > env workstream > env project > root. Invalid workspace context is a validation error at this seam rather than a silent fallback.
Resolution Provenance
Cross-seam principle (ADR-1411, epic #1411): context resolution — config loading, project-root anchoring, workstream resolution — must report its provenance, not fall open silently to defaults. A resolver anchors deterministically to the project root (one walk-up module, no dependence on an arbitrary descendant cwd), returns what it resolved and where it came from (source/degraded), and surfaces a diagnostic when a configured input resolves empty (not configured and configured-but-empty are distinguishable). The resolution-side analog of ADR-227 (input-validation shape). Target seams: Config Loader Module (loadConfig → ConfigResolution { config, source, degraded }), Project-Root Resolution Module (single nearest-.planning/ walk-up, retiring ad-hoc resolvers like resolvePlanningCwd), I/O Module (Resolution<T> { value, configured, reason, warnings } output envelope). A configured input resolving empty without a reason is a CI-guarded regression. P1 (nearest-.planning/ heuristic) shipped in #1413; P2 (loadConfigResolved + agent-skills diagnostic) shipped in #1415 / closes #1366: loadConfigResolved now implements the Config Loader seam target; cmdAgentSkills uses findProjectRoot + loadConfigResolved and emits configured/reason/source/degraded in its --json IR.
Resolution Convention
Diagnostic-output convention for the Resolution Provenance principle (ADR-1411 P3, #1416). Config-interpreting read verbs expose Resolution<T> { value, configured, reason, warnings } (src/resolution.cts); agent-skills is the first adopter, where value = { block, skills_count } and source/degraded remain config-provenance extras outside the envelope. Other read verbs expose at least warnings[] (e.g. capability-state { runtimeConfigDir, capabilities, warnings? }) without configured/reason, which are meaningful only for config-interpreting verbs. Mutation verbs expose warnings[] (advisory) PLUS errors[] (operation-not-applied), e.g. capability-writer { capabilities, warnings, errors }. The shared seam across all shapes is warnings: string[]; a single generic Resolution<T> across read+write verbs was rejected by the deletion test (configured/reason are meaningless for capability verbs; errors[] cannot fold into warnings[]) — ADR-1411 P3 amendment. Recurrence prevention is delivered by P4's CI guard (a configured input resolving empty must carry a reason), not by a shared envelope. A CI guard (scripts/lint-resolution-provenance.cjs, wired into lint:ci) enforces that every registered config-interpreting read verb keeps a configured_empty/not_configured contract test; the registry in that script is the registration point for future verbs (ADR-1411 P4 / #1417).
Worktree Safety Policy Module
CJS Module owning worktree lifecycle safety policy for the GSD orchestration layer. Interface: resolveWorktreeContext(cwd, deps) → WorktreeContext (linked-worktree root mapping), parseWorktreePorcelain(output) → WorktreeEntry[] (porcelain parser, skips detached HEAD), planWorktreePrune(repoRoot, opts, deps) → PrunePlan (metadata-prune plan, never destructive by default), executeWorktreePrunePlan(plan, deps) → PruneResult (executes prune; degrades gracefully on git timeout), listLinkedWorktreePaths(repoRoot, deps) → LinkedPathsResult, inspectWorktreeHealth(repoRoot, opts, deps) → HealthResult (orphan + stale detection), snapshotWorktreeInventory(repoRoot, opts, deps) → InventoryResult, planWorktreeWaveCleanup(repoRoot, manifest) → CleanupPlan (manifest-scoped, fail-closed), executeWorktreeWaveCleanupPlan(plan, deps) → CleanupResult. Source of truth: gsd-core/bin/lib/worktree-safety.cjs. Timeout path: all git subprocess calls are bounded; callers receive ok:false, reason:'git_timed_out' rather than a thrown exception. Test anchor: tests/worktree-safety.test.cjs. The core.cjs re-export spine was retired in epic #1267: this module absorbed the two thin compositional wrappers that squatted in Core — resolveWorktreeRoot(cwd, deps) (a projection over resolveWorktreeContext) and pruneOrphanedWorktrees(...) (sequences planWorktreePrune + executeWorktreePrunePlan with a timeout warning) — so callers reach this single worktree-lifecycle seam directly. gitWorktreeInfoInternal did NOT move here — worktree-info detection belongs to the Git Query Module.
Worktree Lifecycle Module
Workflow contract seam covering agent worktree lifecycle orchestration rules. The worktree_branch_check block lives in one canonical fragment (gsd-core/references/worktree-branch-check.md) that execute-phase.md, quick.md, diagnose-issues.md, and execute-plan.md embed at dispatch. Key invariants: worktree_branch_check is verify-only and fail-closed — the orchestrator owns worktree lifecycle and base recovery, so the sub-agent holds no state-correction primitives; HEAD attachment verified via git symbolic-ref; positive allow-list ^worktree-agent-* enforced; git update-ref on protected refs is prohibited; on base mismatch the sub-agent halts with exit 42 and surfaces to the orchestrator (#48); the orchestrator runs a cwd-drift guard at execute_waves entry that resolves the worktree root and refuses drift into an agent worktree (#48); cleanup is manifest-scoped (WAVE_WORKTREE_MANIFEST) not global-discovery-based; worktree spawning is sequential (one run_in_background at a time to avoid config.lock contention). Test anchor: tests/worktree.test.cjs.
Worktree Root Resolution Adapter Module
Adapter Module owning linked-worktree root mapping and metadata-prune policy (git worktree prune non-destructive default) for planning/workstream callers.
Git Query Module
Module owning bounded, never-throw git repository introspection — the single seam for read-only git queries that degrade gracefully rather than throwing. Adapter 1 — base-branch detection (gsd_run query git.base-branch): Implements a full precedence ladder: (1) git.base_branch config override from .planning/config.json; (2) git symbolic-ref --short refs/remotes/origin/HEAD; (3) git remote show origin HEAD branch (authoritative when origin/HEAD is unset — the common case for git init + remote add + fetch without set-head); (4) local branch existence (master present and main absent → master; main present → main); (5) "main" last-resort default. All git subprocesses are bounded with timeouts (5–15 s) and degrade gracefully to the next tier; the function never throws. Replaces duplicated per-workflow bash detection that silently fell through to :-main on master repos (#1146). Adapter 2 — worktree-info detection: gitWorktreeInfoInternal (git rev-parse --is-inside-work-tree + --show-toplevel), absorbed from the Core module when the core.cjs re-export spine was retired and aligned to this module's bounded-timeout / degrade-don't-throw convention (worktree-info detection is a query concern, distinct from the Worktree Safety Policy Module's lifecycle policy). Source: src/git-base-branch.cts → gsd-core/bin/lib/git-base-branch.cjs. Wired into execute-phase.md, quick.md, ship.md, complete-milestone.md, and pr-branch.md.
Runtime Name Policy Module
Module owning runtime identity normalization at runtime-selection seams. Canonicalizes alias signals from env/config (GSD_RUNTIME, .planning/config.json:runtime) to supported runtime IDs so output emitters and query runtime gates stay consistent across naming variants (for example codex-app/codex-cli -> codex). Sources: gsd-core/bin/lib/runtime-name-policy.cjs, alias manifest gsd-core/bin/shared/runtime-aliases.manifest.json.
Installer Migration Authoring Guard Module
Module owning validation for Installer Migration Module records and planned actions. It enforces migration metadata, explicit install scopes, ownership evidence for destructive/config actions, and runtime contract citations for runtime config rewrites before a migration can enter planning or apply.
Installer Module
Primary installer for all runtimes. Single production file: bin/install.js (generated). Exports: install(isGlobal, runtime[, configDir]) → typed result { runtime, configDir, settingsPath, settings, statuslineCommand, updateBannerCommand }; uninstall(isGlobal, runtime[, configDir]); installRuntimeArtifacts(runtime, configDir, scope, resolvedProfile); uninstallRuntimeArtifacts(runtime, configDir, scope); writeManifest(configDir, runtime). Runtime enum: allRuntimes (15 values: claude, antigravity, augment, cline, codebuddy, codex, copilot, cursor, gemini, hermes, kilo, opencode, qwen, trae, windsurf). Directory helpers: getDirName(runtime) → local dir name; getConfigDirFromHome(runtime, isGlobal) → shell-quoted path fragment. Per-runtime global config-dir resolution is delegated to gsd-core/bin/lib/runtime-homes.cjs:getGlobalConfigDir(runtime[, explicitDir]) — the canonical, env-var–aware projection (explicitDir override + opencode/kilo *_CONFIG file-path precedence); the legacy in-installer getGlobalDir/getOpencodeGlobalDir/getKiloGlobalDir were retired into it (#56). The same module exposes detectAntigravityDirAmbiguity(opts) — a side-effect-free probe reporting whether multiple ~/.gemini/antigravity{,-ide,-cli} dirs coexist and which one GSD's gsd-core/VERSION marker (the dot-home-nested probeExists) resolves to, for installer / /gsd-update operator guidance when a pre-#217 install landed in the wrong sibling dir (#1441). Runtime-specific helpers: resolveKiloConfigPath(configDir), configureKiloPermissions(isGlobal[, explicitDir]). Claude-specific permission helpers: mergeClaudePermissions(settings) — non-destructively appends GSD-owned allow/deny entries (see GSD_CLAUDE_ALLOW_PERMISSIONS, GSD_CLAUDE_DENY_PERMISSIONS constants) to a Claude Code settings object; called from finishInstall for runtime === 'claude' only; uninstall removes exactly these entries (#768). Layout-driven artifact copy/removal delegates to gsd-core/bin/lib/runtime-artifact-layout.cjs:resolveRuntimeArtifactLayout (throws TypeError for unknown runtimes). Seven runtimes with non-recursive skill loaders (claude global, cline, qwen, hermes, augment, trae, antigravity) use a nested router layout: 6 gsd-ns-* router bundles emitted as top-level skills, with concrete skills nested at <router>/skills/<name>/SKILL.md (hermes prefix='': skills/gsd/ns-*/…). The remaining skills-runtimes (cursor, codex, copilot, windsurf, codebuddy, opencode, kilo) use the flat skills/gsd-<stem>/ layout unchanged. See Skill Surface Budget Module and Runtime Artifact Layout Module.
I/O Module
Module owning the tool's CLI I/O primitives: output() result emission (with large-payload temp-file spillover via GSD_TEMP_DIR/ensureGsdTempDir/reapStaleTempFiles), error() stderr emission with exit-code mapping, and the JSON-error-mode toggle (setJsonErrorMode/getJsonErrorMode, ERROR_REASON). Extracted from the Core module per ADR-857 rollout phase 1 (#859) so feature modules (graphify, intel, audit, profile-pipeline) depend on a small I/O seam instead of the core god-module; the core.cjs re-export spine was retired in epic #1267, so callers import this leaf directly. Source of truth: gsd-core/bin/lib/io.cjs (generated from src/io.cts).
Markdown Sectionizer
Canonical markdown-structure parsing seam (gsd-core/bin/lib/markdown-sectionizer.cjs, generated from src/markdown-sectionizer.cts). Pure functions, Node built-ins only. Exports: stripFencedCode(content) → { text, unterminatedFence } (CommonMark-correct state machine, CRLF-safe, signals unterminated fences); tokenizeHeadings(content) → HeadingToken[] (ATX headings outside fenced blocks, { level, text, line, offset }); collectSections(content, stopPredicate) → Section[] (line-by-line section collection driven by a heading predicate); collectSection(content, headingPredicate, { levelBounded, stripFences }) → Section | null (single named section with level-bounded stop); iterateBullets(sectionText) → BulletItem[] (dash/checkbox/numbered markers with indented continuation); extractTaggedBlocks(content, tagName) → string[] (inner text of every <tagName>…</tagName> block in document order, tagName regex-escaped, caller decides fence-stripping — generalises decisions.cts's bespoke extractor for T1); replaceSection(content, section, newBody) → string (pure character-offset splice using Section.bodyStart/bodyEnd for read-modify-write callers — eliminates T6 state.cts's 7× inline content.replace pattern). Section carries bodyStart/bodyEnd offsets for replaceSection. ADR-1372 (epic #1372) establishes this seam and a tiered migration plan (T0–T7) to retire the 8+ ad-hoc markdown parsers and ~20 inline section-collects across src/*.cts. New src/*.cts modules must import this seam instead of hand-rolling fence strippers or heading-regex section walks (enforced by the no-adhoc-markdown-parsing ESLint rule landing in tier T7).
Roadmap Parser Module
Module owning ROADMAP.md parsing: shipped-milestone slicing, current-milestone extraction, milestone/phase lookups, and milestone-phase filtering (stripShippedMilestones, extractCurrentMilestone, replaceInCurrentMilestone, getRoadmapPhaseInternal, getMilestoneInfo, getMilestonePhaseFilter). Depends only on leaf modules (phase-id, planning-workspace, shell-command-projection) — no loadConfig, no other core dependency. Extracted from the Core module per ADR-857 rollout phase 2b (#870), resolving the ROADMAP.md parse/write straddle so the Roadmap module (roadmap.cjs, which owns ROADMAP.md mutation) imports parsing directly instead of through Core; the core.cjs re-export spine was retired in epic #1267, so callers import this leaf directly. Source of truth: gsd-core/bin/lib/roadmap-parser.cjs (generated from src/roadmap-parser.cts).
Core Utilities Module
Module owning the shared low-level utility primitives extracted from Core: POSIX path normalization (toPosixPath), filesystem scanning (detectSubRepos, readSubdirectories, getPhaseFileStats, pathExistsInternal), and small pure helpers (generateSlugInternal, extractOneLinerFromBody, filterPlanFiles, filterSummaryFiles, extractCanonicalPlanId, timeAgo). Depends only on Node built-ins and already-leafed modules (phase-id for comparePhaseNum, planning-workspace for findContextMdIn) — no loadConfig, no other core dependency. Extracted from the Core module per ADR-857 rollout phase 2c (#877) as the shared leaf that unblocks the phase-locator fs-search extraction (2d); the core.cjs re-export spine was retired in epic #1267, so callers import this leaf directly. Source of truth: gsd-core/bin/lib/core-utils.cjs (generated from src/core-utils.cts).
Agent Install Check Module
Module owning agent-presence resolution and verification, extracted from the Core module as the cleanup step that retired the core.cjs re-export spine (the final ADR-857 decomposition, epic #1267). Interface: getAgentsDir(runtime?, env?) — env-var-aware, runtime-aware agents-directory resolution (the claude runtime resolves __dirname-relative); checkAgentsInstalled(...) — multi-runtime agent-presence check that validates gsd-file-manifest.json completeness and confirms the declared agents exist on disk. Pure read/verify — no install-write side effects (writes remain the Installer Module's). Consumed by the Init Command Module, the verify workflow, and the docs workflow. Source of truth: gsd-core/bin/lib/agent-install-check.cjs (generated from src/agent-install-check.cts); replaced the two functions that squatted in core.cts. See Installer Module and ADR-857.
Config Loader Module
Module owning project configuration loading: reads .planning/config.json, merges built-in defaults (CONFIG_DEFAULTS/CANONICAL_CONFIG_DEFAULTS), normalizes legacy keys, applies the active-workstream overlay, validates against the config schema, and warns on unknown keys/profile overrides. Primary interface: loadConfigResolved(cwd, options) → ConfigResolution { config, source, degraded } (provenance-aware, ADR-1411 P2 / #1415) — source ∈ 'workstream' | 'root' | 'builtin-defaults' | 'global-defaults'; degraded:true when a workstream was requested but its config.json was absent (fell back to root config). loadConfig(cwd, options) → Record<string,unknown> is the back-compat thin wrapper over loadConfigResolved (byte-identical result). Resolution is caller-anchored, not loader-anchored: loadConfigResolved resolves cwd as-is (no walk-up), so loadConfig stays byte-identical for its callers; callers that need cwd-drift tolerance (e.g. cmdAgentSkills) anchor to the project root via findProjectRoot (Project-Root Resolution Module) before calling loadConfigResolved. Helper exports: _deepMergeConfig, isGitIgnored, _warnUnknownProfileOverrides. Depends only on leaf modules (configuration, config-schema, planning-workspace, shell-command-projection, core-utils, model-catalog) — no other core dependency. Extracted from the Core module per ADR-857 rollout phase 2e (#885) as the prerequisite for the model-resolver extraction (the resolvers call loadConfig); the core.cjs re-export spine was retired in epic #1267, so callers import this leaf directly. Source of truth: gsd-core/bin/lib/config-loader.cjs (generated from src/config-loader.cts).
Model Resolver Module
Module owning model and effort resolution policy: resolves the model, runtime tier, planning granularity, reasoning effort, and fast-mode for a given agent by reading project config and resolving against the model profiles and catalog (resolveModelInternal, resolveModelPolicy, resolveTierEntry, resolveModelForTier, resolveGranularityInternal, resolveEffortInternal, resolveFastModeInternal, resolveEffortForTier, nextEffort, assertValidGranularityOverride). Depends only on leaf modules (config-loader for loadConfig, configuration for defaults, model-profiles and model-catalog for the static tables) — no other core dependency. Extracted from the Core module per ADR-857 rollout phase 2f (#888) — the final core.cts decomposition step; the core.cjs re-export spine was retired in epic #1267, so callers import this leaf directly. Source of truth: gsd-core/bin/lib/model-resolver.cjs (generated from src/model-resolver.cts).
Package Identity Module [Planned]
Single seam owning GSD's published-package coordinates so a repoint/rename is a one-line change instead of a tree-wide sweep. Source of truth is package.json; values are derived, not re-typed: packageName (.name → @opengsd/get-shit-done-redux), binName (Object.keys(.bin)[0] → get-shit-done-redux), repoSlug (parsed from .repository.url → open-gsd/get-shit-done-redux), plus derived changelogRawUrl and manualInstallCommand({ scope, runtime }). Generated .cjs per ADR-457 (generated-single-source); shipped under gsd-core/bin/lib/. Three consumer worlds: Node consumers require() it at runtime (worker, check-latest-version.cjs, bin/install.js); the bash launcher snippet receives the literal injected by scripts/sync-runtime-launcher.cjs at sync time; prose/help literals (update.md, installer help) carry a committed copy. A drift-guard lint (scripts/lint-package-identity-drift.cjs, sibling to check:alias-drift) fails CI on any raw package/repo literal outside package.json, the generated module, and the value-checked materialization sites — this is what keeps the seam real (two adapters, not one). Replaces the contradictory pair it consolidates: the runtime-broken require('../package.json').name in hooks/gsd-check-update-worker.js (#378, resolves to undefined post-install) and the hardcoded constant in check-latest-version.cjs (#2992). Avoid: "package name string", "the npm name" (when you mean the seam). See ADR-457 and Installer Module.
Update Context Module [Planned]
Module owning install detection for /gsd:update. resolveUpdateContext({ home, cwd, env, fs, preferredConfigDir, preferredRuntime }) is a pure, injected-fs port of update.md's former ~280-line get_installed_version bash; it reproduces the full precedence cascade — preferred-config-dir fast path, local-over-global probe with same-path dedup, env-var overrides (CLAUDE_CONFIG_DIR, OPENCODE_CONFIG, KILO_CONFIG, XDG_CONFIG_HOME, CODEX_HOME, …), and semver validation — and returns the 4-field contract { installedVersion, scope, runtime, gsdDir } (scope ∈ LOCAL/GLOBAL/UNKNOWN). Antigravity is modelled first-class (its .gemini/antigravity{,-ide,-cli} dirs probe before bare .gemini; #3608). Exposed to the workflow as gsd-tools update-context [--config-dir <d>] [--runtime <r>] --json; loadUpdateContext wires the real fs. The workflow keeps only the execution_context path → PREFERRED_* derivation (the one input it alone knows). Source: gsd-core/bin/lib/update-context.cjs; tests: tests/issue-498-update-context.test.cjs. See Installer Module and Package Identity Module.
Skill Surface Budget Module
Module owning which skills and agents are written to runtime config directories at install time (Phase 1) and at runtime via cluster-level toggles (Phase 2). Phase 1: gsd-core/bin/lib/install-profiles.cjs defines named profiles (core, standard, full), computes transitive closure over requires: frontmatter, stages skills/agents to runtime config dirs, and persists the chosen profile in a .gsd-profile marker. Profile resolution precedence: explicit --profile= flag > .gsd-profile marker > full. --minimal/--core-only are back-compat aliases for --profile=core. Phase 2: gsd-core/bin/lib/surface.cjs implements the /gsd:surface slash command for cluster-level enable/disable without reinstall; cluster definitions live in gsd-core/bin/lib/clusters.cjs; per-runtime state persists in <runtimeConfigDir>/.gsd-surface.json independent from the .gsd-profile marker. See ADR-0011.
Runtime Artifact Layout Module
Module owning the per-runtime mapping from artifact kind to filesystem placement. ADR-3660 defines the typed kinds per runtime (commands, agents, skills) with destination subpath, prefix, and stage adapter (with per-runtime converters in bin/install.js: convertClaudeCommandToClaudeSkill, …CodexSkill, …CopilotSkill, …AntigravitySkill). Owns the per-runtime nested skill-bundle decision (#69): a skillsKind flag in src/runtime-artifact-layout.cts drives whether a runtime receives the nested router layout (6 gsd-ns-* routers + concrete skills under <router>/skills/<name>/) or the flat skills/gsd-<stem>/ layout; the evidence/doc-link matrix is recorded in a comment above resolveRuntimeArtifactLayout. Phase 1 applies this seam to the Runtime Surface Module (surface.cjs:applySurface); as of #813, applySurface applies the same per-runtime skill-body path rewrites as installRuntimeArtifacts for skills kinds — re-surfacing no longer overwrites installed SKILL.md bodies with converter-default ~/.claude paths. The shared accessor getInstallExports (exported from runtime-artifact-layout.cjs) is the single-source seam through which surface.cjs reaches computePathPrefix and applyRuntimeContentRewritesInPlace; the resolved scope ('local'|'global') is now carried on the Layout object returned by resolveRuntimeArtifactLayout so applySurface derives the same pathPrefix (global $HOME form vs. absolute) as a fresh install. Phase 2 is planned to migrate install/uninstall in bin/install.js so all lifecycle sites iterate one shared layout table instead of re-encoding runtime layout logic. This design is intended to remove the #3659 class of omissions. Migrations remain under the Installer Migration Module (ADR-0008). See ADR-3660.
Runtime Artifact Conversion Module
Sibling Module to Runtime Artifact Layout Module. Owns projection from canonical Claude-authored command/agent/skill markdown into runtime-specific artifact bodies, including converter selection, frontmatter/body normalization, runtime path rewrites, and staged artifact generation. Runtime Artifact Layout remains responsible for filesystem placement (kind, destination subpath, prefix, nesting); Runtime Artifact Conversion owns the content Implementation behind that placement seam so install, uninstall/surface parity, and future plugin/package projections stop reaching back through bin/install.js for converter functions or GSD_TEST_MODE-guarded installer exports. Chosen direction: sibling Module, not an expanded Layout Module, to preserve ADR-3660's narrow placement responsibility while deepening artifact content locality. First slice: relocate only the layout-reached conversion family (convertClaudeCommandTo*Skill, converted command-file emitters, buildKimiAgentArtifacts) plus the minimal helper closure they need; do not leave helper dependencies in bin/install.js because that would preserve the same shallow seam under a new filename. Installer integration decision: bin/install.js imports the conversion Module at top level and re-exports the moved names for compatibility; the conversion Module must not import bin/install.js or Runtime Artifact Layout, so the dependency direction becomes installer/layout Adapters -> conversion Module, never conversion -> installer. First-slice Interface decision: export the existing compatibility names only; do not introduce a grouped convertRuntimeArtifact Interface until after relocation proves byte-for-byte behavior.
Command Roster Module
Tiny read-only helper Module owning discovery of canonical commands/gsd/*.md command stems for artifact conversion and runtime projection. It is a sibling dependency of Runtime Artifact Conversion Module, not part of conversion itself: conversion consumes a roster to safely rewrite gsd: / /gsd- references, while roster discovery owns filesystem/catalog knowledge. First slice: extract existing readGsdCommandNames behavior behind this Module instead of moving it into Runtime Artifact Conversion Module or keeping it as installer-owned state.
Runtime Install Policy Module
Projects a pure, typed install plan for a given runtime by composing artifact placements (Runtime Artifact Layout Module), command text (Shell Command Projection Module), and per-runtime config intentions — with no filesystem IO or format-specific serialization. Runtime-specific adapters consume the plan and execute concrete file mutations and config rendering. See ADR-58.
Capability [Planned]
A bundle delivering one optional GSD feature, toggled as a unit at install or after install. Owns its skills, agents, hooks, federated config-key schema (keys + defaults + validation), and loop extension-point registrations, plus a requires list of other Capabilities, plus an optional activationKey (a dotted config key, e.g. graphify.enabled) naming the config toggle that gates the whole capability — consumed by the Capability State Resolver's per-capability active (absent → no config gate; see Capability State Resolver tri-state deepening). Declared co-located in the Capability's own folder and compiled into a generated central Capability Registry at build time. The five-step loop (Discuss → Plan → Execute → Verify → Ship) and shared-infrastructure skills (phase, config, help, update, surface, progress) are the privileged host, not Capabilities, in v1 — but host extension points are data so a loop step can become a Capability under a future uniform kernel. Supersedes the implicit feature-scattering across clusters, install-profiles, and config-schema. Generalizes the Skill Surface Budget Module and Runtime Install Policy Module.
Loop Host Contract
Generated description of what the five-step loop (Discuss → Plan → Execute → Verify → Ship) exposes as extension points: per-step loop points, agent roles, and core artifacts. Sourced from structured <!-- gsd:loop-host ... --> HTML-comment markers embedded near the top of each of the five step workflow files (discuss-phase.md, plan-phase.md, execute-phase.md, verify-work.md, ship.md). Generated by scripts/gen-loop-host-contract.cjs → gsd-core/bin/lib/loop-host-contract.cjs (ADR-894 §3 phase 3a-impl-2). Covers exactly the 12 canonical points (discuss:pre/post, plan:pre/post, execute:pre/wave:pre/wave:post/post, verify:pre/post, ship:pre/post). The generator enforces a drift guard: every declared non-orchestrator agent role must correspond to an actual agent reference in the workflow file. Consumed by gen-capability-registry.cjs (replaces the former inline LOOP_HOST_CONTRACT constant). Run node scripts/gen-loop-host-contract.cjs --write after editing a workflow step marker.
Capability Registry
Generated central manifest projecting all co-located Capability declarations into one validated artifact for runtime resolution and for the install, surface, config, and loop-extension adapters. Mirrors the research-profiles / package-identity generation pattern (co-located source → generated central file). Generated by scripts/gen-capability-registry.cjs → gsd-core/bin/lib/capability-registry.cjs (ADR-894 §5 phase 3a-impl). Role-partitioned indexes: bySkill, byAgent, byLoopPoint (hook ordering materialized), configKeys (ownership map: key→capId), configSchema (full per-key schema: key→{ owner, type, default, description }), runtimes, requiresClosure(id). Each feature capability's entry in capabilities now includes the optional activationKey field (the dotted config key that gates the whole capability, e.g. "graphify.enabled"; absent means no config gate). ADR-857 phase 3b adds configSchema with validated type/default/description per key, sourced from each capability's .config slice. ADR-857 phase 4a adds two derived views: capabilityClusters ({ <capId>: [<skill stems>] } — each cap's skills array, sorted, derived from the capability's skills declaration; consistency-gated against the hand-authored CLUSTERS) and profileMembership ({ <capId>: { tier, profiles: [...] } } — the tier-derived index: suffix of PROFILE_RANK starting at the capability's tier). Both views cover the same capability set: only capabilities that own skills (non-empty skills array). The generator enforces a HARD gate (throws) if a capId matching a CLUSTERS key has a mismatched skill set, and emits SOFT ⚠ pending-reconciliation warnings to stderr (never to the file) for skills not yet in the hand-authored profile at the capability's tier. install and surface are UNTOUCHED (still read hand-authored constants; derived views are emitted and tested but unconsumed until cutover). Validated against the Loop Host Contract (12 points; generated by gen-loop-host-contract.cjs from workflow markers, phase 3a-impl-2). Run node scripts/gen-capability-registry.cjs --write after editing any capabilities/<id>/capability.json.
Federated Config
ADR-857 phase 3b seam that merges capability-declared config slices into the loadConfig return value. Implemented in src/federated-config.cts → gsd-core/bin/lib/federated-config.cjs. Exports mergeFederatedConfig({ configSchema, isCentralKey, userConfig }) → { values, validKeys, warnings }. Rules: central-schema keys are skipped with a pending-migration warning; malformed slices are skipped with a warning (never throws); valid federated keys (absent from the central schema) resolve to the user-supplied value (if type-matches) or the slice default. Object writes are guarded against prototype pollution with inline literal __proto__/constructor/prototype key checks. ADR-857 phase 6 made the channel live for migrated Capability keys: config-schema.cjs exposes isCentralConfigKey() for central ownership and isValidConfigKey() accepts central + runtime + dynamic + Capability-owned registry keys. loadConfig exposes _setFederatedRegistryForTests/_resetFederatedRegistryForTests seams for injecting a synthetic registry in tests.
Capability Registry Overlay
Runtime seam (gsd-core/bin/lib/capability-loader.cjs, ADR-1244 D2) that composes the frozen first-party Capability Registry (capability-registry.cjs) with a validated installed overlay of third-party capability manifests discovered at load time. Install roots are global ($GSD_HOME/.gsd/capabilities/<id>/capability.json, where GSD_HOME defaults to ~) and project (<projectRoot>/.gsd/capabilities/<id>/capability.json). Primary interface: loadRegistry({ includeInstalled }) → registry — when includeInstalled is true the overlay is merged via the canonical buildRegistry so all derived views (bySkill, byAgent, byLoopPoint, configKeys) cover first-party and overlay entries identically. First-party always wins: any overlay entry whose id, owned skill/agent stem, or federated config key collides with first-party, or whose id uses a reserved gsd-/gsd-core-/anthropic- prefix, is rejected at load time. Load-time re-gate: an overlay failing schema validation or whose engines.gsd semver range does not satisfy the running GSD version is skipped with a warning and never crashes the load loop. Per-hook-kind policy: a skipped capability that declared a gate-kind hook fails CLOSED (the loop resolver injects a blocking gate); skipped step or contribution capabilities skip open. A capability dir whose co-located ledger entry carries an in-flight _pending intent (a crashed/uncommitted install or upgrade, ADR-1244 Phase 4) is skipped OPEN (never activated until reconciliation commits or rolls it back). #1459 user-owned consent gate: a PROJECT-scope overlay is activated (declarative surfaces AND command dispatch) ONLY when the user-owned Capability Consent Store holds a record for (realpath(projectRoot), id) whose stored contentHash equals the bundle content hash the loader RECOMPUTES at load (bundleContentHash(capDir) over the whole on-disk bundle) — NOT the repo-plantable ledger integrity nor the executable-only disclosure signature — otherwise the cap is DISCOVERED-BUT-INACTIVE (a warning carrying kind:'unconsented', no surfaces, empty commandRoots), so a forged/cloned in-repo project ledger or any post-consent tamper no longer activates anything; GLOBAL scope (under the user's own home) is trusted without a record, and the global-vs-project root dedup/escalation is realpath-keyed so a symlinked GSD_HOME aliasing the project root cannot bypass the gate (finding 1). The consent lookup is wrapped to fail CLOSED (inactive); both the per-scope ledger AND the capability.json manifest are read via the shared bounded readSmallRegularFile (a repo-planted FIFO/oversized ledger or manifest can no longer hang or OOM the loader — finding 2). The loader reuses the ledger's shared isValidLedgerEntry for committed-entry parity. Consumers wired to the overlay-aware registry: config-loader.cjs, config-schema.cjs, capability-state.cjs, loop-resolver.cjs.
Capability Validator
Shared conformance validator (gsd-core/bin/lib/capability-validator.cjs, ADR-1244 D2) extracted from scripts/gen-capability-registry.cjs so the build-time generator and the runtime overlay loader share one validator implementation. Exports the same validateCapability(manifest) surface consumed by both the generator (build-time) and capability-loader.cjs (runtime). Generative-parity is CI-guarded: a drift between the generator's validation logic and the extracted module is a hard failure. Callers that previously inlined validation against the generator's internal helpers are migrated to import this module directly. Source of truth: gsd-core/bin/lib/capability-validator.cjs.
Capability Source Resolver
ADR-1244 D3 fetch-and-stage seam (gsd-core/bin/lib/capability-source.cjs). Primary interface: resolveCapabilitySource(spec, opts) → { id, version, stagedDir, integrity, source }. Parses specs via parseSpec and dispatches to one adapter per source kind: local (fs copy from a ./-prefixed path), git (clone --depth 1 + checkout via execGit; https/ssh/git transports only — ext:: and file:// are rejected), npm (pack via execNpm --ignore-scripts + tar extract — NEVER npm install, no lifecycle scripts; shell-metacharacter spec rejection for Windows shell safety), tarball (HTTPS download + sha512 integrity verify BEFORE extraction + tar extract), and registry (explicit stub — no first-party endpoint yet). Security contract: install never executes capability code (copy/extract only); integrity is verified before staging; tar-slip member paths and symlinks are rejected. Staging is atomic: a per-pid/timestamp scratch directory under .staging/ is renamed into $GSD_HOME/.gsd/capabilities/<id>/ on success and removed on failure. The Phase 1/2 validator suite runs on the fetched manifest before finalizing; engines.gsd is pre-checked. Test seam: _setCapabilitySourceHttpGet.
Capability Ledger
ADR-1244 D4 per-runtime install manifest (gsd-core/bin/lib/capability-ledger.cjs). Leaf module (only node:fs/node:path plus shell-command-projection's platformWriteSync). Records { id, version, source, integrity, files[], sharedEdits[{file,marker}] } per installed capability in .gsd-capabilities.json at the runtime config dir root. Exports: readLedger (structural-validated, never throws), writeLedger (atomic via platformWriteSync), recordInstall (idempotent, prototype-pollution-guarded), removeEntry, and reconcile (reports orphans whose files[] are missing on disk; hardened against non-string/.. members; never mutates). Serves as the atomic commit point for Phase-4 upgrade/remove and the reconciliation basis for detecting stale entries after out-of-band deletions.
Capability Consent Store
Issue #1459 user-owned consent seam (gsd-core/bin/lib/capability-consent.cjs, generated from src/capability-consent.cts). Leaf module (node:fs/node:path/node:os/node:crypto + the ledger's shared bounded readSmallRegularFile/readSmallRegularFileBuffer + the shared capability-lock primitive). Stores { version:"1", records: { "<JSON disk key {r:realpath(projectRoot),i:id}>": { projectRoot, id, scope:'project', integrity, disclosureSignature, contentHash, consentedAt } } } at ${GSD_HOME||homedir()}/.gsd/consent.json — a USER-OWNED file OUTSIDE any repository. Exports: consentStorePath(gsdHome?), readConsentStore(gsdHome?) (bounded via readSmallRegularFile + 8 MiB cap, NON-THROWING — missing/corrupt/oversized/FIFO/wrong-shape → empty {records:{}}; caps records at MAX_RECORDS=4096), bundleContentHash(capDir) (THE security binding — a sha512-<base64> over a DETERMINISTIC, INJECTIVE, LOSSLESS serialization of EVERY regular file AND directory under the bundle: length-FRAMED entry COUNT + per-entry TYPE tag + uint32 path-byte-len + RAW path bytes from a {encoding:'buffer'} dir walk [finding 4] + for files uint64 content-byte-len + RAW content bytes via readSmallRegularFileBuffer [finding 1b], plus typed DIR markers binding empty directories [finding 2]; symlinks/non-regular rejected; size+count bounded), hasProjectConsent({gsdHome,projectRoot,id,contentHash}) (true iff a record for ${realpath(projectRoot)}<id> exists AND its stored contentHash equals the supplied recomputed hash — the binding is contentHash, NOT integrity and NOT disclosureSignature (those remain on the record purely for the human disclosure + re-consent-on-executable-change UX); unsafe ids → false; prototype-pollution-safe NUL-joined keys + Object.prototype.hasOwnProperty), recordProjectConsent({gsdHome,projectRoot,id,integrity,disclosureSignature,contentHash}) (LOCKED, atomic+durable write — tmp wx/fsync/rename/dir-fsync mirroring writeLedger; enforces the record cap at write time) and revokeProjectConsent({gsdHome,projectRoot,id}) (LOCKED atomic delete, no-op if absent) — BOTH THROW rather than perform an UNLOCKED read-modify-write when the consent-store lock cannot be acquired (finding 3; the lifecycle treats a consent-write failure as non-fatal, and the trust revoke CLI catches the throw and emits a clean error). This is the authoritative consent signal the loader recomputes (bundleContentHash(capDir)) and checks at load before activating a PROJECT-scope third-party overlay (declarative surfaces AND command dispatch): a forged/cloned in-repo project ledger, OR any post-consent tamper (swapped declarative manifest, edited hook script, empty-integrity local install — all change the recomputed hash), leaves the cap DISCOVERED-BUT-INACTIVE until the user consents on THIS machine to the EXACT bundle (the lifecycle records the consent on a consented project install/upgrade and revokes it on remove; install/lookup/revoke share one canonical consentProjectRoot root key). GLOBAL-scope overlays (under the user's own home) need no record; and when GSD_HOME resolves (via realpath, defeating symlink aliasing — finding 1) to a genuine project root the in-repo bundle still requires a record. The consent lock is the SHARED hardened primitive (below), so it never stale-steals a slow-but-live writer (finding 4). See docs/explanation/capability-trust-model.md "project-scope trust boundary".
Capability Lock
Issue #1459 finding 4 shared cross-process lock primitive (gsd-core/bin/lib/capability-lock.cjs, generated from src/capability-lock.cts). Leaf module (node:fs/node:path/node:os/node:crypto + the ledger's bounded readSmallRegularFile + shell-command-projection's execTool for the rare start-time shell-out). THE single hardened lockfile protocol shared by BOTH capability-lifecycle (the .gsd/capabilities/.lock mutation lock) and capability-consent (the consent-store .consent.lock) — extracted so the two locks cannot diverge (mirrors the shared-validator / shared bounded-reader lessons). Exports: acquireLock(lockPath, opts?) (O_EXCL create with a JSON {token,pid,hostname,startTime,ts} body; steal protocol binds age to the body's own ts, never stale-steals a VERIFIED-LIVE same-host holder — pid alive AND recorded start-time matches the pid's current start-time, defeating pid-reuse without ever stealing a live holder — and reclaims only a dead/unverifiable holder via the dead-pid fast path or the hard LOCK_DEADMAN_MS deadman; opts.maxAttempts raises the bounded retry budget and opts.waitForFresh makes a contended fresh/live holder be WAITED FOR rather than failed-fast so genuinely-racing consent writers serialize), releaseLock(handle) (token + inode owner-safe — never deletes a successor's lock), getProcessStartTime, and the _setLockProbes/_resetLockProbes test seams. Carries the #1462 lifecycle-lock invariants (process-start-time liveness, TOCTOU-safe pre-rename identity recheck, bounded iterative loop).
Capability Trust Gate
ADR-1244 Phase 4 (D5) PURE policy module (gsd-core/bin/lib/capability-trust.cjs). Computes what a capability would do and whether policy permits it; performs no mutation and no I/O beyond existence-checking declared artifacts. Exports: discloseExecutableSurfaces(manifest, stagedDir?) (enumerates the three executable surfaces — hooks, command modules, mcpServers — and flags hasExecutable); evaluateInstallTrust(args) (composes source policy + reserved-namespace + engines gate + disclosure into { allowed, requiresConsent, disclosure, engines, blockReasons }); evaluateSourceAllowed(parsed, strictKnownRegistries) enforcing capabilities.strict_known_registries (unset/null → permissive-with-consent; [] → block all external; non-empty → host-based allowlist, never substring); checkEngines(manifest, hostVersion) (engines.gsd hard gate via semverSatisfies + compatVersions graceful-downgrade picking the newest working version); executableSetChanged(old, new) (auto-update re-consent trigger); checkReservedNamespace (gsd-/gsd-core-/anthropic-). The MCP disclosure also captures each server's env (string→string, filtered) and cwd (#1459) — disclosureSignature folds them in as STABLE SORTED JSON so any env/cwd add/change forces re-consent while a key reorder does not; signatureForManifest(manifest, stagedDir?) is the single source of truth for that signature (consumed by the loader's consent check and the lifecycle's consent binding). #1459 finding 5: each MCP surface also carries rawConfig — the FULL declared server config the writer persists ({...config}), prototype-pollution-cleaned — folded into the signature as STABLE SORTED JSON so a change to ANY persisted field (not just the explicit whitelist — a future envFile/workingDir/launch option) forces re-consent, while a pure key reorder does not; the human summary stays readable via the key fields only. The barrier is consent + integrity + reversibility, NOT a sandbox — see docs/explanation/capability-trust-model.md.
Capability Lifecycle
ADR-1244 Phase 4 (D5+D6) orchestration seam (gsd-core/bin/lib/capability-lifecycle.cjs) composing the source resolver, ledger, and trust gate into the mutating operations. Exports: installCapability (pre-fetch source gate → resolve copy-only with promote:false → trust verdict → promote + apply marker-stamped shared edits → ledger commit; nothing written on block/abort), upgradeCapability (atomic stage-then-swap: old set aside, new swapped in, shared edits re-derived, ledger committed, backup dropped; re-prompts when the executable set changed), removeCapability (strip only _gsdCapability-marked shared-config entries — user hand-edits preserved — delete exactly the ledger-recorded files, then drop the entry; CAPABILITY_DATA preserved unless removeData), reconcileCapabilities (crash recovery driven by the ledger's _pending {kind,backupName,sharedFiles} INTENT — not a version comparison: roll an uncommitted upgrade back by restoring the backup, an uncommitted fresh install away entirely, and re-sync shared config from the winning bundle, guaranteeing no half-state), plus applyCapabilitySharedEdits/stripCapabilitySharedEdits (marker-isolated JSON edits, prototype-pollution-guarded). All four mutating ops + reconcile take a cross-process lock (.gsd/capabilities/.lock, atomic stale-steal) so a concurrent reconcile can't clear a live intent. Capability code never executes during any operation. The source resolver's promote:false/skipEnginesGate options are the seams that let this module own the swap/commit ordering and the engines gate (with compatVersions downgrade hint).
Capability Command Dispatch
ADR-1244 Phase 5 (D7) registry-driven dispatch of capability command families. First-party families (graphify/intel/audit, shipped in bin/lib/) dispatch via dispatchCapabilityCommand (gsd-core/bin/gsd-tools.cjs) against the FROZEN capability-registry.cjs commandFamilies (confined to bin/lib/) — unchanged. Third-party (installed overlay) families dispatch via dispatchOverlayCapabilityCommand: after the first-party path returns false, it calls loadRegistry({ includeInstalled, cwd }) and dispatches a family iff its capId is in _overlay.commandRoots — which capability-loader.cjs populates ONLY for accepted overlay capabilities that declare commands AND pass the loader's activation gate (a committed ledger entry, present and non-_pending, PLUS — for PROJECT scope — a matching user consent record in the Capability Consent Store; GLOBAL scope needs no consent record). A bundle dropped on disk with no install (no ledger entry) or no on-this-machine consent is NOT command-dispatchable. The router module is require()'d FROM the capability's install root via defaultRequireFromInstallRoot (bare-.cjs basename + realpath containment, rejecting .. traversal and symlink escape); same own-property/function/sync-only guards as the first-party path. Wired into the runCommand default arm before "Unknown command". A repo-planted project ledger no longer activates anything on its own (#1459) — see docs/explanation/capability-trust-model.md "project-scope trust boundary".
Loop Extension Point
A named, stable site on a host loop step (per-step pre/post plus per-wave in Execute; 12 total) where Capabilities register hooks. Three hook kinds: step (runs as its own sequenced unit), contribution (injects into the core step's prompt/context), and gate (checks and optionally blocks via a declared blocking flag). Each hook declares the artifacts it produces and consumes; hook order is derived by topological sort of that produces/consumes graph (capability-id tiebreak), which also defines data flow — file-artifact based, surviving /clear and fresh executor contexts. Hooks are surfaced by runtime resolution with concrete projection: the workflow calls a query that resolves the active hooks and returns fully-rendered, ordered markdown for the executor. Failure is default-resilient — a non-gate hook that errors is skipped with a warning; a hook may opt into onError: halt. Part of the Capability system. ADR-857 phase 3c ships the registry-consuming query layer: gsd-core/bin/lib/loop-resolver.cjs exposes resolveLoopHooks({ point, registry, config }) (pure, no I/O), renderLoopHooks(resolved) (pure markdown renderer), and cmdLoopRenderHooks(cwd, point, raw, opts) (I/O entry point); activated via gsd-tools loop render-hooks <point> which emits { point, activeHooks[], rendered }. Activation is driven by when (dotted config key resolved against loadConfig), with inline literal __proto__/constructor/prototype prototype-pollution guard. The first phase-6 cutovers wiring workflows to this query have landed — ui-phase at plan:pre and ui-review at verify:post (in plan-phase.md/autonomous.md); further per-feature cutovers are ongoing.
Capability State Resolver
ADR-857 phase 4b/6 unified resolver that composes the three toggle systems (install profile, runtime surface, config activation) into one per-capability view consumed by workflow hook rendering. Source of truth: gsd-core/bin/lib/capability-state.cjs (generated from src/capability-state.cts). Interface: resolveCapabilityState({ registry, installedSkills, surfacedSkills, config, cwd? }) → { capabilities: CapabilityStateEntry[] } (pure, no I/O); resolveCapabilityRuntimeState(cwd, runtimeConfigDir) (I/O resolver shared by workflow dispatch and diagnostics — returns { runtimeConfigDir, warnings, capabilities } only; registry and config are NOT returned — callers that need them import capability-registry.cjs and call loadConfig(cwd) directly); cmdCapabilityState(cwd, runtimeConfigDir, raw, opts) (I/O output entry point); isCapabilityActive(capId, cwd): boolean (convenience predicate — calls resolveCapabilityRuntimeState, finds the entry, returns entry.active; false when capability not found). CLI surface: gsd-tools capability state [--config-dir <path>] — emits { runtimeConfigDir, capabilities[] }. Per-capability output: { id, tier, skills[], installed, surfaced, enabled, active, hooks[] } where installed = every owned skill ∈ installedSkills (or installedSkills==='*'; vacuously true for empty-skills caps), surfaced = every owned skill ∈ surfacedSkills (vacuously true for empty-skills caps), enabled = installed && surfaced (unchanged — install+surface toggle only), active = enabled && configActivation (tri-state deepening: configActivation resolves the capability's activationKey via _resolveActivationValue; absent activationKey → configActivation=true, so active===enabled for ungated capabilities), and hooks = [{ point, kind: 'step'|'gate'|'contribution', when, configured, active }] derived from the cap's steps, gates, contributions arrays (configured resolves when; active = enabled && configured). Capabilities sorted by id for determinism. Defensive: malformed registry → { capabilities: [] }, never throws; inline literal __proto__/constructor/prototype prototype-pollution guard on capability id keys.
Capability State Writer
The write-side mirror of the Capability State Resolver. Takes a desired capability state — per-capability enabled plus per-hook gates — and projects it onto the substrates: enabled drives the runtime surface (.gsd-surface.json) as the capability on/off switch; gates drive the federated config keys (config.json workflow.*) for hook-level granularity; the install profile (.gsd-profile) is a read-only floor it never writes. Writes the surface once and config once (atomic per substrate), then re-runs the resolver and reports divergence (assert-and-report) — so 'off means off' holds as a write-time invariant rather than by caller discipline. Source of truth: src/capability-writer.cts; the surface and config writers become its internal adapters.
Capability Command Family [Planned — mechanism built, unconsumed]
ADR-959 (phase 4d) — a CLI command family (a top-level gsd-tools command and its subcommands) owned by a Capability via a new optional commands: [{ family, module, router }] field on the feature role. The Capability declares the family name, a first-party in-tree module (under gsd-core/bin/lib/), and the exported router — a standard route*Command({ args, cwd, raw, error }) function identical in shape to the 12 existing host routers (so it routes through the stateless CommandRoutingHub via routeCjsCommandFamily, owning its own subcommand list and arg parsing). The registry materializes a commandFamilies index (family → { capId, module, router }); the formerly-dead _dispatchNonFamily shim is replaced by a real dispatchCapabilityCommand (exported from gsd-core/bin/gsd-tools.cjs) consulted in runCommand's default case — an unmigrated command hits its hardcoded case; a migrated command's case is removed so it reaches default → registry → router, making collision structurally impossible. The registry discovers a router (it does not rebuild a handler table). First-party only; third-party command loading deferred. Mechanism built (4d-impl-1): commands schema + validator + single-family-ownership cross-check in gen-capability-registry.cjs; commandFamilies index emitted in the generated capability-registry.cjs (currently {} — no capability declares commands yet); dispatchCapabilityCommand wired into runCommand's default case (behavior-preserving today). Pilot complete (4d-impl-2): graphify cut over as the first real capability command family — capabilities/graphify/capability.json bundles the command (family: graphify, module: graphify-command-router.cjs, router: routeGraphifyCommand), skill (graphify), config gate (graphify.enabled), and tier: full; the case 'graphify': arm removed from gsd-tools.cjs; dispatch flows default → dispatchCapabilityCommand → commandFamilies.graphify → graphify-command-router.cjs → routeGraphifyCommand; behavior proven equivalent (all subcommands: build, query, status, diff, build snapshot, unknown subcommand error, usage error, disabled gate). Template for phase-6 per-feature cutovers. Audit cutover (4d-impl-3): audit-uat and audit-open cut over as the second capability command family pair — capabilities/audit/capability.json declares two commands (family: audit-uat, module: audit-command-router.cjs, router: routeAuditUat) and (family: audit-open, module: audit-command-router.cjs, router: routeAuditOpen); the case 'audit-uat': and case 'audit-open': arms removed from gsd-tools.cjs; commandFamilies now holds audit-uat, audit-open, and graphify; dispatch flows default → dispatchCapabilityCommand → commandFamilies["audit-uat"|"audit-open"] → audit-command-router.cjs → routeAuditUat|routeAuditOpen; behavior equivalence proven by existing regression tests (bug-2659, bug-2911, uat.test.cjs) plus new cutover tests. Confirms hyphenated family names pass registry validator (no format restriction beyond non-empty + non-reserved). Intel cutover (4d-impl-4, last first-party cutover): intel cut over — capabilities/intel/capability.json declares the command (family: intel, module: intel-command-router.cjs, router: routeIntelCommand) and the existing config gate (intel.enabled, default false); the case 'intel': arm removed from gsd-tools.cjs; commandFamilies now holds intel, audit-uat, audit-open, and graphify; dispatch flows default → dispatchCapabilityCommand → commandFamilies.intel → intel-command-router.cjs → routeIntelCommand; all 9 subcommands (query, status, update, diff, snapshot, patch-meta, validate, extract-exports, api-surface) and both usage-error paths preserved; non-raw timeAgo transform on status.files[*].updated_at preserved exactly. intel.enabled is Capability-owned config after ADR-857 phase 6. Completes the initial 4d capability command cutover batch.
Runtime Capability [Planned]
A role: runtime variant of a Capability (a Capability carries role: feature | runtime) that projects GSD's produced artifacts (skills/agents/hooks/commands) onto one host CLI's conventions — config-surface format, artifact-layout kinds, command template, hooks manifest, sandbox tier. It is a declarative descriptor over a fixed first-party primitive vocabulary (not a code adapter); install composes active Feature Capabilities × the chosen Runtime Capability at the InstallPlan seam (ADR-0058). First-party runtimes are authored through the same descriptor a third party would write (dogfooding the interface); tier-1 (Claude Code, Codex, Antigravity) is fully tested, the other existing runtimes ship lower-tier, none dropped. Third-party runtime loading is deferred to a purely additive external loader + trust gate. Note: "third-party" here is the authorship/distribution axis (who wrote/ships it), distinct from the integration-shape axis (in-host vs Connected Capability).
Connected Capability [Planned — deferred design]
A Capability whose integration shape brings its own external process, service, or persistent state — for example an MCP server plus a backing database — rather than running entirely within the host's process and trust boundary as declarative artifacts and in-tree first-party code referenced by closed name. Orthogonal to authorship: a Connected Capability may be first-party (e.g. MemPalace, issue #956) or third-party. Contrast with a plain Capability (declarative artifacts + in-host-trust code) and a Runtime Capability (closed-vocabulary projection descriptor), both of which run within host trust. The Connected Capability contract — external-process/MCP-server/backend-provider contributions plus a trust and load gate — is deferred design (ADR-857 §7); vehicle issue #956. It is NOT expressed by the current capability schema.
RULESET.CAPABILITY.off-means-off=the host derives shared outputs from the ACTIVE hook set (via loop.render-hooks); a hook may ADD a labeled block or be COUNTED into a host-computed aggregate (e.g. a score denominator), but NEVER mutates host source — so a disabled capability yields the base output by construction, not by authoring discipline. Ratify in ADR-894; proven by spike #1018.
RULESET.CAPABILITY.cutover-self-gating=a phase-6 per-feature cutover moves the host's phase-context detection + mode/flag logic INTO the skill (self-gating, per ADR-894); the loop hook is intentionally COARSE — "invoke skill X at point Y when config Z" — and carries no detection/mode. WORKED EXAMPLE: plan-phase.md §5.6 UI gate (frontend-detection via ui-safety-gate.cjs + --auto/manual branch + --skip-ui bypass) must move into gsd-ui-phase before its plan:pre hook can replace the inline call without behavior loss. Spike #1018 finding.
RULESET.CAPABILITY.step-additive-gate-blocks=a stephook is purely additive (invoke skill + produce artifacts, NEVER halts the host); host-blocking preconditions aregates (blocking:true, onError:halt); runtime/mode context (auto/chain vs manual) self-gates IN THE SKILL, not via when (config-only). §5.6 = plan:pre step (ui-phase; skill self-gates on frontend+pipeline, auto-fires only in pipelines) + a NEW plan:pre gate (frontend-and-no-UI-SPEC → halt, when:workflow.ui_safety_gate); the loop.render-hooks dispatch template handles steps AND gates. Resolves #1022.
RULESET.CAPABILITY.precedence-engine-single-owner=the config-key four-level precedence walk (loadConfig result → workstream config.json → root config.json → registry.configSchema default → absent) is owned solely by src/capability-activation.cts: raw-value primitive resolveConfigKey(dotKey, {config,cwd,registry}) and boolean wrapper _resolveActivationValue(dotKey,config,cwd,registry); loop-resolver.cts imports the engine (no duplicate); resolveConfigValues in loop-resolver.cts delegates to resolveConfigKey; resolveCapabilityRuntimeState does NOT return registry/config — callers import capability-registry.cjs and call loadConfig(cwd) directly.
Teams Status Module
Pure read-only detector for claude-code's experimental agent-teams feature (issue #1355). Stops gsd-core hanging silently when run under CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS. Source of truth: src/teams-status.cts → gsd-core/bin/lib/teams-status.cjs. Exports: resolveTeamsStatus({ runtime, env }) → TeamsStatus (pure, env injected, no process.env/disk inside — hermetic for tests) and cmdTeamsStatus(cwd, { active? }) (I/O entry point; reuses resolveRuntime from runtime-slash.cjs for canonical GSD_RUNTIME → config.runtime → 'claude' precedence). TeamsStatus shape: { active: boolean, runtime: string, env_present: boolean, source: 'on: env' | 'off: flag absent' | 'off: non-claude' }. active is true only when the flag is strictly truthy ("1" or "true", case-insensitive) AND the runtime is "claude". CLI surface: gsd-tools query teams-status [--active] (default: JSON; --active: exit 0/1 boolean). Used only by a non-fatal --active check warning in plan-phase.md init block — does NOT block execution, does NOT activate capabilities, does NOT change behavior on any non-claude runtime.
Wing
A MemPalace organizational unit corresponding to one project or repository. GSD derives the wing name from project_code or the project directory when mempalace.wing is unset. A wing contains Rooms. Cross-project Tunnels connect rooms across wings. MemPalace vocabulary — see Connected Capability, MemPalace memory capability (issue #956).
Room
A named bucket inside a Wing that groups drawers by semantic kind. GSD maps its phase artifacts to five fixed rooms: decisions (CONTEXT.md), planning (PLAN.md), milestones (SUMMARY.md/UAT.md excerpts), problems (confirmed bug→fix pairs), and learnings (extract-learnings output). MemPalace vocabulary — see Wing.
Drawer
A verbatim-content unit stored inside a Room. GSD files phase artifacts as drawers using mempalace_add_drawer; duplicate-check via mempalace_check_duplicate makes capture idempotent. GSD stores verbatim text (not AAAK summaries) to preserve recall fidelity. MemPalace vocabulary — see Room.
Tunnel
A cross-Wing knowledge connection created by mempalace_create_tunnel. GSD proposes tunnels at ship:post when mempalace.cross_project_tunnels: true, linking rooms in the current wing to semantically related rooms in other project wings. MemPalace vocabulary — see Wing.
Diary
A per-agent narrative entry written by mempalace_diary_write. GSD's gsd-mempalace-curator writes a diary entry at ship:post when mempalace.diary_journal: true, recording a session summary scoped to the project and agent role. MemPalace vocabulary — see Connected Capability.
memory_mode
The mempalace.memory_mode config key controlling how tightly MemPalace couples to GSD's native memory. Three declared values: augment (default — implemented; palace is an additional write-mostly recall layer; lowest coupling), kg_backend (declared; routing seam not yet implemented — intended to route graphify KG queries through MemPalace's temporal graph; selecting today behaves as augment), replace (declared; not yet functional — intended to make the palace the durable store; selecting today behaves as augment). Only augment has effect in the current release; kg_backend and replace are forward-declared for a future release. Read at hook-render time; switching is a config change, not a reinstall. See MemPalace Settings in docs/CONFIGURATION.md.
Runtime Hooks Surface Module
Standalone hook-surface writer module extracted from bin/install.js as ADR-857 phase 5f-1 (behavior-preserving relocation, no logic change). Owns: Cline rules-body/agents-md/pre-tool-use hook generation (buildClineRulesBody, buildClineAgentsMdBody, buildClinePreToolUseHook, mergeGsdAgentsMd, writeClineArtifacts); Cursor hooks.json lifecycle (buildCursorHookEntry, isManagedCursorHookEntry, reconcileCursorHooksJson, writeCursorHooksJson, removeCursorHooksJson); Copilot session-hook config (buildCopilotHookConfig, writeCopilotHookConfig); Codex hook-block and event management (buildCodexHookBlock, rewriteLegacyCodexHookBlock, reconcileCodexHooksJsonEvent, reconcileCodexHooksJsonSessionStart, ensureCodexHooksJsonSessionStart, ensureCodexHooksJsonEvent, removeCodexHooksJsonEvent, removeCodexHooksJsonSessionStart, buildCodexHookWindowsShimIR); and shared hook command helpers (buildHookCommand, rewriteLegacyManagedNodeHookCommands, normalizeNodePath, resolveNodeRunner). bin/install.js delegates to this module via thin wrappers and re-exports its functions unchanged so existing tests require no modification. Source: src/runtime-hooks-surface.cts. Built output: gsd-core/bin/lib/runtime-hooks-surface.cjs.
Runtime Config Adapter Registry
Module owning the explicit per-runtime config-mutation dispatch table for the installer. resolveRuntimeConfigIntent(runtime) projects a typed config intent — installSurface (settings-json | codex-toml | copilot-instructions | cline-rules | cursor-hooks-json | profile-marker-only), writesSharedSettings (the finishInstall shared-settings write gate), and finishPermissionWriter (opencode | kilo | none) — that bin/install.js dispatches on instead of inline runtime === '...' branching. Owns adapter selection only: it performs no filesystem IO and does not execute config mutations (the install/finishInstall handlers and the per-runtime writers do that). Unknown runtimes fail loudly with a TypeError, guarded by an Object.hasOwn own-property check so prototype-chain keys (__proto__, constructor) also throw. Also exports resolveInstallPlan(runtime) — the ADR-58 InstallPlan capstone — which collects the install-level descriptor axes (installSurface, writesSharedSettings, finishPermissionWriter, hookEvents, extendedHookEvents, hooksSurface, sandboxTier) into one typed InstallPlan value consumed by install() and finishInstall() in bin/install.js. sandboxTier (none | codex-agent-sandbox) gates per-agent sandbox_mode emission in the codex TOML path and fails loud on a missing/invalid value (#1151). The spatial axes (configHome, artifactLayout, commandStyle) remain behind their self-resolving adapter modules and are not part of the plan; they are the execution adapters. Realizes both the adapter-selection and plan-collection halves of the Runtime Install Policy Module boundary. Source: gsd-core/bin/lib/runtime-config-adapter-registry.cjs. See ADR-58, #60.
Claude Code Plugin Manifest Module
Module owning the projection of gsd-core's artifact surfaces (commands, agents, hooks) onto the Claude Code plugin contract (.claude-plugin/plugin.json + hooks/hooks.json) — the plugin-contract sibling of the Runtime Artifact Layout Module (which projects the same surfaces onto filesystem placements). Defined mapping: name=binName (drives the /gsd-core: command namespace), repository/homepage=repoUrl (Package Identity Module), version/description/license from package.json (version is required for claude plugin validate --strict), commands=./commands/gsd/, agents via Claude Code's default agents/ discovery (the explicit string form is schema-rejected), hooks=./hooks/hooks.json. The hook projection carries ONLY the always-on subset of the Installer Module's Claude settings.json wiring (check-update, context-monitor, prompt-guard, read-guard, worktree-path-guard, read-injection-scanner) via ${CLAUDE_PLUGIN_ROOT}; config-gated opt-in hooks are excluded because a static manifest cannot honor per-project config gates, and plugin-shipped agents cannot carry hook frontmatter (so all plugin-path hook wiring lives in hooks.json). hooks.json covers all seven Claude Code lifecycle events: SessionStart, PreToolUse, PostToolUse, SubagentStop, Stop, PreCompact (all wired to context-monitor for context-headroom awareness), and FileChanged (matcher: config.json → config-reload, injects additionalContext when .planning/config.json changes mid-session). Additive — the file-copy path (Runtime Artifact Layout / Install Policy / Installer Modules) is unchanged. Conformance is validated by claude plugin validate --strict plus the in-repo drift-guard tests/issue-766-plugin-manifest.test.cjs. Avoid: "the plugin API", "the plugin file" (when you mean the seam). See ADR-766 and Runtime Artifact Layout Module.
Gemini Extension Package
The repo-root gemini-extension.json + GEMINI.md pair that projects gsd-core onto the Gemini CLI extension contract, enabling one-step lifecycle management via gemini extensions install <git-url> / update / remove (and gemini extensions link <path> for dev). The Gemini-CLI sibling of the Claude Code Plugin Manifest Module — same additive idea, different runtime package format. Defined mapping: name=binName (gsd-core; lowercase-dashes per Gemini's extension naming rule), version tracks package.json (Gemini's gemini extensions update keys off the manifest version field), description (required by the manifest schema), contextFileName=GEMINI.md (the extension's context payload, loaded into every Gemini session). Intentionally minimal: no mcpServers (gsd-core ships no MCP server). Slash-command / agent / hook projection into the extension (which would require committing the Gemini-format TOML/agent conversions the Installer Module produces at --gemini install time) is deferred — the manual npx gsd-core --gemini path remains the way to install the /gsd:* commands, and is unchanged (additive, no breaking change). Conformance is guarded by the in-repo drift test tests/issue-775-gemini-extension.test.cjs (manifest validity, version↔package.json parity, contextFileName existence, files[] publication). Avoid: "the Gemini plugin" (Gemini calls them extensions, not plugins). See #775, ADR-766, Claude Code Plugin Manifest Module, and Runtime Artifact Layout Module.
Knowledge Graph Module
Module owning the graphify integration: tri-state capability gate (isCapabilityActive('graphify', cwd) from capability-state.cjs — requires installed AND surfaced AND config-enabled; replaces the former config-only isGraphifyEnabled gate, cutover in #1306), disabled response (disabledResponse), subprocess helper (execGraphify, typed GRAPHIFY_REASON enum), presence detection (checkGraphifyInstalled), version checking (checkGraphifyVersion), query surface (graphifyQuery — BFS seed-expand + budget trim), status surface (graphifyStatus — node/edge counts, mtime staleness, commit-staleness tri-state via built_at_commit/commits_behind/commit_stale), diff surface (graphifyDiff — added/removed/changed nodes+edges), build pre-flight (graphifyBuild), snapshot management (writeSnapshot). Config leg reads .planning/config.json:graphify.enabled; all three legs (install, surface, config) must be active; writes to .planning/graphs/. Auto-update hook (hooks/gsd-graphify-update.sh) triggers a detached background rebuild after HEAD-advancing git operations on the default branch when graphify.auto_update=true. Status file .planning/graphs/.last-build-status.json carries { ts, status, exit_code, duration_ms, head_at_build, graphify_version }. Graph IR uses nodes[], edges[] (or links[] for graphify ≥0.7 compat), hyperedges[], built_at_commit. commit_stale is tri-state: false (known fresh), true (stale), null (unknown — no git or pre-v0.7 graph). Source: gsd-core/bin/lib/graphify.cjs. Skill: commands/gsd/graphify.md.
Intel Module
Module owning the code-intelligence store: tri-state capability gate (isCapabilityActive('intel', cwd) from capability-state.cjs — honours installed+surfaced+config-enabled; replaces the former config-only isIntelEnabled gate, cutover in #1307; intel has skills:[] so installed/surfaced are vacuously true and the effective gate is intel.enabled in config), disabled response, query surface (intelQuery — full-text search across all intel JSON files), status surface (intelStatus — per-file freshness, 24-hour staleness threshold), diff surface (intelDiff — added/changed/removed files vs last-refresh snapshot), snapshot management (saveRefreshSnapshot/intelSnapshot), validation (intelValidate — existence, JSON validity, _meta.updated_at recency), api-surface render (intelApiSurface — generates .planning/intel/API-SURFACE.md from api-map.json), plus ungated utilities (intelPatchMeta — patches _meta.updated_at in any JSON file; intelExtractExports — extracts CJS/ESM exports from any JS file). Loop hook rendering gates on state.active (not state.enabled) so the activationKey config gate is honoured even without a per-hook when guard (Phase 4 tri-state alignment, #1307). Source: gsd-core/bin/lib/intel.cjs (generated from src/intel.cts). Router: gsd-core/bin/lib/intel-command-router.cjs. See Capability Command Family Module (ADR-959 4d-impl-4) and Loop Extension Point.
Research Module
The GSD-RESEARCH capability behind an L2-hybrid seam: code owns cache + provider policy + package legitimacy; MCP owns the actual fetch. Reachable via gsd-tools query research-plan|research-store|package-legitimacy. Source: src/research-{store,provider}.cts + src/package-legitimacy.cts (generated to gsd-core/bin/lib/*.cjs per ADR-457). Replaces the prose provider-waterfall duplicated across the researcher agents and the pip-install slopcheck bolt-on.
GSD-RESEARCH.MODULE.research-store=content-addressed cache; key=sha256(ecosystem+library+version+query+kind); getResearch->{hit,stale} never throws (mirrors graphify staleness); ttlForSource curated HIGH 30d|MED 7d|web LOW 1d; tiers: curated-doc kinds -> ~/.gsd/research-cache (cross-project), web/synthesis -> project .planning/research/.cacheGSD-RESEARCH.MODULE.research-provider=single source of truth PROVIDER_WATERFALL (docs Context7->Ref->Jina->websearch; web Exa->Tavily->Perplexity->Brave->websearch; scrape Firecrawl->Jina); planResearch returns cache-hits+fetch-plan; classifyConfidence stamps HIGH|MEDIUM|LOW by provider AUTHORITY + verification EVIDENCE (HIGH requires code-computed ground-truth corroboration e.g. legitimacyVerdict OK; provider authority alone caps at MEDIUM; SLOP caps at LOW); Firecrawl is scrape-only (not in docs/web discovery)GSD-RESEARCH.MODULE.package-legitimacy=registry-API verdicts (npm/PyPI/crates.io injectable adapters) computed from thresholds {minAgeDays:30,minWeeklyDownloads:1000,requireRepo:true}; verdict OK|SUS|SLOP per package; slopcheck=optional adapter that can only escalate, never the install-or-degrade gateGSD-RESEARCH.INTEGRATION.L2-hybrid=code owns cache+legitimacy+confidence+provider-pick (gsd-tools query research-plan/research-store/package-legitimacy); MCP owns the fetch; agent returns RESEARCH.md path, never raw fetchesGSD-RESEARCH.PROVIDER.availability=config flags brave_search/exa_search/firecrawl/tavily_search/ref_search/perplexity/jina (env <X>_API_KEY or ~/.gsd/<x>_api_key); context7/jina/websearch always available; planResearch falls through waterfall to websearch terminalGSD-RESEARCH.CONTEXT-DISCIPLINE=less-context levers: subagent isolation + compact provider output + fetches-to-disk + cache-returns-digest; API clear_tool_uses/memory tool are the conceptual model, not a Claude Code harness knobDEFECT.RESEARCH-PROVIDER-PROSE-DRIFT=provider waterfall duplicated across N researcher agent .md files drifts independently (META.RULE.brief-no-paraphrase); fix-forward=research-provider.cjs single source of truth + generated agents (#657)
UAT-Passed Predicate
Runtime-neutral predicate evaluating *-UAT.md / *-VERIFICATION.md result fields with markdown-aware parsing that ignores false-positive contexts (frontmatter body, fenced code, HTML comments, blockquotes). Returns passed: true only when all required checks pass; supports --require-verification to demand at least one VERIFICATION.md file alongside UAT results. Output envelope: { passed, uat_files[], verification_files[], checks[], blockers[], policy }. Source: gsd-core/bin/lib/uat-predicate.cjs (generated from src/uat-predicate.cts). Wired via phase uat-passed alias → phase-command-router → cmdPhaseUatPassed.
Probe Core Module
Generic spec-phase probe resolution model — the shared seam underlying spec-completeness probes (ADR-550 Decision 7). Owns the status × verification model (status: resolved | dismissed | unresolved × a per-probe verification tier), structural validation (validateResolution, validateRequirement — fail-closed: verification must be null unless status is resolved, and an out-of-enum status, a dismissed-without-reason, or an unresolved carrying a resolution/reason/tier payload all throw rather than silently miscount), the analyzeCoverage(items, resolutions?, validators) merge/rollup/orphan-reject pipeline, the byVerification per-tier rollup, and the runProbeCli I/O scaffold (parse → validate → analyze → emit, structurally guarding the report shape before write — a malformed report fails closed with stderr + exit 2 instead of stringifying as green). Adapter-agnostic: consumed by the Edge Probe Module today and the Prohibition Probe Module (#644) next. Exports (generic surface): VALID_STATUS, validateResolution, validateRequirement, analyzeCoverage, runProbeCli — the prohibition adapter exports that also ship from this module (projectProhibitions, PROHIBITION_VALIDATORS, validateProhibitionResolution, dispositionForProhibition) are documented under the Prohibition Probe Module's own locked-surface line. Source of truth: gsd-core/bin/lib/probe-core.cjs (generated from src/probe-core.cts, gitignored per ADR-457). Tests: tests/probe-core.test.cjs. See ADR-550 and Edge Probe Module. Under ADR-857 (phase-6 boundary, settled 2026-06-12) this seam is classified core verification substrate on the contract side: its deterministic validators are the verifier↔predicate contract's CI-testable surface (ADR-550 Decision 5) — core and non-toggleable, never an off-by-default Feature Capability. (The recall-gapped generator is the probe adapters that propose predicates, not this resolution engine — see Edge Probe Module and Verification substrate (predicate boundary).)
Edge Probe Module
First adapter of the Probe Core Module (ADR-550 Decision 7): the spec-phase edge-completeness probe wired into spec-phase Step 5.5. Owns shape classification (classifyShape), the applicable-category relevance filter (applicableCategories over the 8-category edge TAXONOMY), edge proposal (proposeEdges), and the {explicit, backstop} verification validators; delegates merge/rollup/CLI to probe-core. Fail-closed input contract: an edge requirement with missing/empty text and no shapes override is rejected (a { id }-only requirement no longer classifies to zero edges and silently drops), while the legitimate shapes: [] opt-out is preserved. Downstream, the plan-phase planner lifts every covered/backstop edge from the SPEC ## Edge Coverage section into must_haves.truths. Exports (locked surface): classifyShape, applicableCategories, proposeEdges, analyzeCoverage, validateResolution, validateRequirement, plus the constants TAXONOMY (the closed 8 edge categories), UNCLASSIFIED_CATEGORY (the unclassified review-manually sentinel for zero-cue prose, #1110 — deliberately not a 9th taxonomy entry: it stays out of TAXONOMY but is present in EDGE_VALIDATORS.categories), VALID_SHAPES, SHAPE_CUES, and EDGE_VALIDATORS (the {explicit, backstop} validators bundle injected into probe-core's generic engine; its categories = the TAXONOMY ids plus UNCLASSIFIED_CATEGORY). Source of truth: gsd-core/bin/lib/edge-probe.cjs (generated from src/edge-probe.cts, gitignored per ADR-457). Tests: tests/edge-probe.test.cjs, tests/edge-probe-spec-phase-contract.test.cjs, tests/edge-probe-planner-contract.test.cjs. See ADR-550 and Probe Core Module. Per ADR-857's phase-6 boundary (2026-06-12) the predicates this module generates are core verification substrate (they set the verifier's reach), so it is wired onto the core predicate rail as a core-default module rather than migrated to an off-by-default capabilities/edge-probe/ Feature Capability.
Verification substrate (predicate boundary)
The ADR-857 classification (settled 2026-06-12, prompted by @davesienkowski's boundary analysis on #857) that predicate-generation — the must-NOT-have / edge predicates that set the verifier's reach — is core, not an off-by-default Feature Capability. Load-bearing premise: verifier reach = spec reach (the verifier can only catch what the spec concretely names). Decomposes into: the verifier↔predicate contract (the verifier always expects predicates and grades exogenously against them — core, non-toggleable, a stability contract alongside the Loop Extension Point names), and the generator (the probe adapters that propose predicates — edge-probe's classifyShape/proposeEdges, the prohibition probe's adversarial LLM-propose — core-default but independently versionable, kept their own module because of a measured recall gap; probe-core's deterministic validators sit on the contract side, not the generator side). Altitude rule distinguishing it from gate hooks: a gate runs against the spec (hook); predicate-generation defines the spec's reach (core). The decision-#6 produces/consumes artifact flow is the internal rail from generation to the core verifier. See ADR-857 Verification substrate vs. plug-in tier (the predicate boundary) and the ADR-550 cross-reference.
Probe Family
The set of spec-phase completeness probes that share the Probe Core Module seam (ADR-550 Decision 7): the Edge Probe (Step 5.5, data-shape edges) and the Prohibition Probe (Step 5.6, unwritten must-NOT constraints) today, with room for a third nearly-free adapter. A "probe" walks each SPEC requirement, surfaces candidate omissions, and resolves each through the shared status × verification model — but each family member owns its own recall mechanism: a deterministic closed-taxonomy classifier for edges (shape→category), open-vocabulary adversarial LLM prose for prohibitions (recall is model-driven, not a compute adapter — ADR-550 D7b). What is shared is the resolution/validation/rollup engine and the soft-gate lifecycle; what diverges is how candidates are recalled. See Probe Core Module, Edge Probe Module, Prohibition Probe Module.
Verification Tier
The orthogonal verification dimension a resolved probe item carries alongside its status (ADR-550 D7a — status: resolved | dismissed | unresolved is the shared resolution lifecycle; verification is the probe-defined enforcement axis). Each probe defines its own tier vocabulary: the edge probe uses explicit | backstop; the prohibition probe uses test | judgment. For prohibitions the tier names how a must-NOT can be enforced — test (a negative test can fail-close on it) vs judgment (an irreducible values/safety rule only human/LLM judgment can assess). Verify-phase routes on the tier: test-tier items must be provably wired and fail closed when unwired (never a silent green); judgment-tier items take the mode-dependent soft-gate (ADR-550 D4 — interactive demands human resolution, autonomous records a non-authoritative LLM-judge verdict + an unverified-prohibition flag, never a silent pass and never a hard halt). The rollup exposes coverage.byVerification: { <tier>: count } so verify-phase reads the per-tier denominator without re-scanning. See Probe Core Module, Prohibition Probe Module.
Bespoke vs Canon Prohibition
The ownership seam between the prohibition probe and security/compliance tooling (ADR-550 D6). The probe owns bespoke product/values prohibitions — the unwritten must-NOTs specific to this feature's intent (e.g. "the streak reminder must not manipulate the user into returning"). When precision classifies an item as a canon security/compliance concern (OWASP / GDPR / fairness / prototype-pollution / path-traversal — the codified, cross-project rule sets), the probe does not mint a SPEC prohibition: it emits a one-line breadcrumb ("possible canon-security concern X — owned by /gsd:secure-phase / eslint") and stops. Canon checks are referred, not duplicated — keeping the surfaced list short (#644's ~2–3-item precision goal) and the secure-phase boundary explicit. See Prohibition Probe Module.
Prohibition Probe Module
Second adapter of the Probe Core Module (ADR-550 Decision 7): the spec-phase prohibition-completeness probe wired into spec-phase Step 5.6, surfacing the unwritten must-NOT constraints (values/safety/ethics) the spec never forbids. Unlike the Edge Probe, recall is prose-orchestrated, not a compiled engine (ADR-550 D7b) — a two-stage pass per requirement: Stage 1 an adversarial recall question, Stage 2 a one-pass precision classifier (drop routine engineering, keep genuine prohibitions). The code surface is schema/projection only: projectProhibitions() (deterministic SPEC↔must_haves.prohibitions projection backing the DEFECT.GENERATIVE-FIX parity assertion), the {test, judgment} PROHIBITION_VALIDATORS, validateProhibitionResolution, and dispositionForProhibition() (the fail-closed default — an unwired test-tier item resolves to unverified/flagged, never green). Deterministic test-tier locate (#1278, ADR-550 D3 addendum): a resolved test-tier prohibition MAY carry an optional flat-scalar check descriptor — check_kind (node-test | lint-rule), check_target, and check_rule (lint-rule only) — that projectProhibitions emits into must_haves.prohibitions when well-formed, and descriptorFromProjection() (the read-back seam in the #1259 enforcement producer, src/prohibition-enforcement.cts) reconstructs into a {kind, target, rule?} CheckDescriptor, so verify-phase locates the wired check with zero LLM/author authoring. Flat scalars, never a nested check:{} object — so the round-trip rides the unchanged shared parseMustHavesBlock (the #644 no-parser-rewrite precedent); an absent/partial descriptor falls through to the producer's existing fail-closed locate, and failFirst stays caller-attested (machine-proof is #1279). No proposeProhibitions() — recall is LLM prose. plan-phase lifts every resolved prohibition from the SPEC ## Prohibitions (must-NOT) section into the must_haves.prohibitions sibling block (never truths). Exports (locked surface): projectProhibitions, PROHIBITION_VALIDATORS (the {test, judgment} validators bundle injected into probe-core's generic engine), validateProhibitionResolution, and dispositionForProhibition (the fail-closed disposition) — the prohibition adapter surface, shipped from probe-core alongside the generic engine. Source of truth: gsd-core/bin/lib/probe-core.cjs (the prohibition exports live in src/probe-core.cts, gitignored per ADR-457) + gsd-core/references/prohibition-probe.md. Tests: tests/prohibition-probe.*.test.cjs. See ADR-550, Probe Core Module, Edge Probe Module, Verification Tier, Bespoke vs Canon Prohibition.
MVP Mode
Phase-level planning mode that frames work as a vertical slice (UI → API → DB) of one user-visible capability instead of horizontal layers. Resolved at workflow init via the precedence chain: --mvp CLI flag → ROADMAP.md **Mode:** mvp field → workflow.mvp_mode config → false. All-or-nothing per phase (PRD #2826 Q1). Surfaced as MVP_MODE=true|false to the planner, executor, verifier, and discovery surfaces (progress, stats, graphify). Canonical parser: roadmap.cjs **Mode:** field; canonical resolution chain documented in workflows/plan-phase.md. Concept index: references/mvp-concepts.md.
User Story
Phase-goal format under MVP Mode: As a [role], I want to [capability], so that [outcome]. Required regex shape: /^As a .+, I want to .+, so that .+\.$/. Used as the framing input by gsd-planner (emits as bolded ## Phase Goal header in PLAN.md) and as the verification target by gsd-verifier (the [outcome] clause is the goal-backward verification anchor). Authored interactively by /gsd-mvp-phase, validated by SPIDR Splitting when too large.
Walking Skeleton
Phase 1 deliverable under --mvp on a new project: the thinnest end-to-end stack proving every layer (framework, DB, routing, deployment) works together. Emitted as SKELETON.md capturing the architectural decisions subsequent vertical slices inherit. Gate fires when phase_number == "01" AND prior_summaries == 0 AND MVP_MODE=true. Scope intentionally narrow (PRD #2826 Q2) — does not retrofit existing projects.
Vertical Slice
Single-feature task that moves one user capability from open-to-close (happy path) end-to-end. Contrast with the horizontal layer (all models, then all APIs, then all UI). The MVP Mode planning unit; SPIDR Splitting axes (Spike, Paths, Interfaces, Data, Rules) are the canonical decomposition tools when a slice is too large for one phase.
Behavior-Adding Task
Predicate over a PLAN.md task: tdd="true" frontmatter AND <behavior> block names a user-visible outcome AND <files> includes at least one non-*.md / non-*.json / non-*.test.* source file. Pure doc/config/test-only tasks are exempt. The MVP+TDD Gate (in references/execute-mvp-tdd.md) only halts execution on this predicate; the gsd-executor agent applies all three checks at runtime. Currently a prose-only specification — no shared utility.
MVP+TDD Gate
Per-task runtime gate in /gsd-execute-phase that, when both MVP_MODE and TDD_MODE are true, refuses to advance a Behavior-Adding Task until a failing-test commit (test({phase}-{plan})) exists for it. The tdd_review_checkpoint end-of-phase review escalates from advisory to blocking under the same condition. Documented contract: references/execute-mvp-tdd.md. Reserved escape hatch --force-mvp-gate is documented but not implemented.
SPIDR Splitting
Five-axis story decomposition discipline (Spike, Paths, Interfaces, Data, Rules) used by /gsd-mvp-phase when a User Story is too large for one phase. Full interactive flow per PRD #2826 Q3 (not a lightweight filter). Reference: gsd-core/references/spidr-splitting.md.
Clock seam
An injectable time abstraction accepted as an optional parameter by production code ({ clock = Date } = {}). Test code substitutes node:test mock.timers to control time deterministically without waiting for real OS scheduler events. Canonical pattern established by ADR 456 (docs/adr/456-test-rigor-architecture.md).
Deterministic scheduler
Test-execution model in which all timing and concurrency outcomes are fully controlled by the test (via clock seam, explicit await ordering, or synchronous stepping) rather than by the OS thread scheduler. Opposed to real-race tests, which are non-deterministic on loaded CI runners.
Property-based test
A test that generates many adversarial inputs automatically (via fast-check) and asserts that a stated invariant holds for all of them, rather than asserting on a fixed set of hand-chosen examples. Invariant categories used in this codebase: round-trip, monotonicity, boundary containment, idempotency. See RULESET.TESTS.property-based-testing.
Mutation testing / mutation score
Stryker injects small code mutations (e.g., flipping a > to >=, deleting a return statement) and reruns the test suite for each. A mutation is "killed" if at least one test fails; "surviving" if all tests pass despite the mutation. Mutation score = killed / total. Score below 80 % on the changed scope blocks PR merge. See RULESET.TESTS.mutation-score.
ESLint harness
The canonical lint infrastructure adopted in ADR 452 (docs/adr/452-eslint-lint-harness.md): ESLint flat config (eslint.config.mjs) with typescript-eslint, eslint-plugin-n, eslint-plugin-no-only-tests, and a local AST-rule plugin at scripts/eslint-rules/. Replaces the homegrown scripts/lint-*.cjs regex scanners. The three custom test-rigor rules (local/no-source-grep, local/no-magic-sleep-in-tests, local/no-elapsed-assertion) initially ship at warn; they become error after the cleanup sweep tracked at issue #453 merges.
External-job-waiting half-state
A legal deferred state of an Execute step (external_job_waiting): the executor has dispatched a long-running async external job and committed an async-job manifest at .planning/async-jobs/<job>.json instead of a SUMMARY.md. Distinct from the synchronous "mid-production-commits" half-state and from an illegal partial-plan state. The core loop's step-completion + safe-resume/pause contract treats a non-terminal manifest as legal and reconciles against it (never re-dispatching the plan, which would duplicate the external job); SUMMARY.md is deferred until the job reaches a terminal state and its expected_artifacts are verified. The manifest is a versioned stability contract (docs/reference/planning-artifacts.md); core consumes it while a default-off scheduler-adapter Capability (#1164) produces it at execute:wave:post — the contract-is-core / producer-is-capability seam mirrors ADR-857's verification-substrate decision. Status enum is closed and scheduler-agnostic: submitted, running, completed-unverified, failed, cancelled, timeout.
Test rules and lint
RULESET.TESTS.no-source-grep=scripts/lint-no-source-grep.cjs rejects readFileSync source + .includes()/.match()/.startsWith() on the bound var; CI hard-fail
RULESET.TESTS.no-source-grep.stdout-extension=also flags assert.match/doesNotMatch on .stdout/.stderr — emit JSON from SUT, parse, assert on typed fields
RULESET.TESTS.no-source-grep.exemption=// allow-test-rule: <runtime-contract-is-the-product> with one-line justification; reserved for tests where the file content IS the product surface (STATE.md, config.toml, hooks.json, agent .md). Migration to typed-IR parser tracked in #2974.
RULESET.TESTS.no-source-grep.tmp-file-traps=reading tmp files written by the SUT in tests still trips lint; round-trip through CLI (e.g. frontmatter get) instead of readFileSync+.includes()
RULESET.TESTS.escape-regex=new RegExp("prefix${var}") must escapeRegex(var); phase-id.cjs exports escapeRegex (core.cjs re-export spine retired in epic #1267); phase IDs like 5.1 contain . which is metacharacter
RULESET.TESTS.no-dead-regex-in-includes=src.includes("foo.*bar") is always false — .* is regex metacharacter not wildcard; use new RegExp(...).test(src) or delete
RULESET.TESTS.guard-toplevel-readFileSync=module-level const src = readFileSync(...) throws before any test() registers — wrap in try/catch in test() or use lazy load
RULESET.TESTS.coderabbit-fix-prefer=behavioral tests (call exported fn, capture JSON, assert typed fields) over source-grep
RULESET.TESTS.diagnostics=after JSON.parse, assert output shape (Array.isArray(output.phases)) with raw-output-prefix diagnostics before .map() — prevents opaque TypeErrors when CLI output shape changes
RULESET.TESTS.boundary-coverage=tests MUST exercise inputs at and near the threshold/limit, not only trivial-fit and trivial-overflow; pick inputs where N ∈ {limit-1, limit, limit+1} and where pre-trim/pre-check accumulators ≈ effective limit; "very small" and "very large" inputs alone do not constitute edge-case coverage and routinely miss off-by-one + reservation-accounting bugs
RULESET.TESTS.boundary-coverage.fixtures=for any code with budget/limit/quota/threshold parameter, test suite MUST include: (a) input where SUT estimate == limit exactly, (b) input where estimate == limit - 1, (c) input where estimate == limit + 1, (d) input where any internal reserve/safety constant pushes baseline within reserve-distance of limit (catches early-pressure firing)
RULESET.TESTS.boundary-coverage.anti-pattern=test suites that pair budget:1_000_000 (trivially fits) with budget:1 (trivially overflows) and skip the boundary region; failure mode that shipped PR #3708 UNNEEDED_TRIM + FALSE_HARDFAIL regressions (commit 2df566ed, fixed bde1ae8f)
LEARNING.prompt-budget.boundary-gap=PR #3708 commit 2df566ed reserved NOTE_RESERVE_TOKENS in pressure-threshold AND in minSet pre-check; both buggy paths only fire when baseTokens ∈ (effectiveBudget - NOTE_RESERVE_TOKENS, effectiveBudget]; original test suite used budgets far from that band so neither path was exercised; fix bde1ae8f confines NOTE_RESERVE accounting to post-trim assembly path only; future budget/limit code MUST add boundary fixtures per RULESET.TESTS.boundary-coverage.fixtures
RULESET.TESTS.no-timing-assertion=do not assert on wall-clock elapsed time (Date.now() delta, performance.now(), process.hrtime() comparison); such assertions test the host machine not the SUT and flake on loaded CI runners; enforcement: local/no-elapsed-assertion ESLint rule (warn → error after #453); canonical replacement: clock-seam pattern with node:test mock.timers
RULESET.TESTS.clock-seam=concurrency logic must accept an optional {clock=Date} parameter; tests control time via t.mock.timers.enable(['Date']) + t.mock.timers.setTime(0) + t.mock.timers.tick(N); real OS scheduler races are not a permitted test pattern after ADR 456 (2026-05-28); real-race tests are deleted once deterministic seam tests cover the same logical path; clock.cjs realClock adds nowIso() (→ new Date(this.now()).toISOString()) and today() (→ nowIso().split('T')[0]) so all date-stamping in state.cjs routes through the seam; subprocess time-pin adapter: set GSD_TEST_MODE=1 + GSD_NOW_MS=<epoch-ms> in runGsdTools env to pin the date written by the SUT without touching real wall-clock (issue #474)
RULESET.TESTS.property-based-testing=modules implementing parsing / transformation / budget-limit / bijective contracts must include at least one fast-check (fc) property test asserting a domain invariant; invariant categories: round-trip, monotonicity, boundary-containment, idempotency; property tests live in *.test.cjs alongside unit tests; CI signal: Stryker mutation score below 80% blocks merge
RULESET.TESTS.mutation-score=Stryker runs incremental (--since origin/next) on ubuntu-latest/Node24 CI leg; default threshold 80% killed/total; surviving mutants in scope block merge unless path is listed in stryker.config.mjs with documented reason; treat surviving mutant as a failing test specification
RULESET.TESTS.delete-bad-tests=pass-always / vacuous-truth / source-grep / elapsed-time / real-race / permanent-allow-test-rule tests are DELETED and replaced with compliant tests in the same PR; not skipped, not commented out, not permanently exempted; replacement must cover the same logical path via typed-surface assertion or clock-seam pattern
RULESET.TESTS.eslint-harness=ADR 452 (2026-05-28): ESLint flat config + typescript-eslint + eslint-plugin-n + eslint-plugin-no-only-tests + local plugin at scripts/eslint-rules/; replaces scripts/lint-*.cjs regex scanners; three test-rigor rules (local/no-source-grep, local/no-magic-sleep-in-tests, local/no-elapsed-assertion) ship at warn, promoted to error after #453 cleanup sweep merges
RULESET.AUDIT.search-source-not-generated=verify an invariant/validation EXISTS by searching the AUTHORED source (src/*.cts OR the scripts/gen-*.cjs generator), never the generated bin/lib/*.cjs (gitignored, ADR-457); gen-time checks live in gen-*.cjs not the .cts it consumes → search BOTH before declaring absent; read generated .cjs only for output drift. Repro: grep src/*.cts for VALID_CONVERTER_NAMES → false "5e ConverterName unenforced"; actually enforced in gen-capability-registry.cjs. cf RULESET.TESTS.no-source-grep
RULESET.WORKFLOW_MARKDOWN.FENCES=preserve opening language fence when editing shell snippets in workflow markdown; malformed fence creates fresh CR threads (MD040)
RULESET.WORKFLOW_SIZE_BUDGET=workflow size enforcement (#1074; BYTES not lines per #717; LF-normalized per #683) = per-file baseline (PRIMARY anti-creep: tests/workflow-size-baseline.json pins each file's exact size) + loose tier hard caps (outer red lines, NEVER raised on approach: XL<=98304 / LARGE<=61440 / DEFAULT<=40960) + new-file cap (un-baselined files <32768, the Codex anchor) + discuss-phase<32000; a file that grew fails the baseline guard — fix with npm run size:baseline, commit the one-line diff, and justify the growth in the PR (or extract LAZILY-loaded content; eager @-imports don't reduce loaded context); crossing a hard cap means EXTRACT, not bump
RULESET.AGENT_SIZE_BUDGET=agent-size-budget (#1074; sibling of WORKFLOW_SIZE_BUDGET; BYTES not lines per #717/#683, rebased from lines in PR 3/3) = per-file baseline (PRIMARY anti-creep: tests/agent-size-baseline.json pins each agents/gsd-*.md exact byte size) + loose tier hard caps (red lines, never raised on approach: XL<=57344 / LARGE<=49152 / DEFAULT<=24576); net-new agents are DEFAULT-tier (no separate new-file cap). One 'npm run size:baseline' regenerates BOTH workflow and agent baselines via the shared scripts/workflow-size.cjs measureMdFiles(dir,predicate) counter. A grown agent fails the baseline guard — regenerate + justify, or extract LAZILY to gsd-core/references/. DISTINCT from DEFECT.AGENT-FILE-SIZE-CAP-BREACH (a separate 45K-CHAR extraction-evidence threshold on gsd-planner via planner-decomposition/reachability tests): that guard proves mode-sections were extracted; this one bounds total agent bytes. Two guards, two units (chars vs bytes), two purposes
RULESET.WORKFLOW_FILE_NAMES=workflow files use hyphens; <step name="..."> XML attributes must match (extract-learnings not extract_learnings); tests should pin exact hyphenated name
RULESET.WORKFLOW_EXECUTION_CONTEXT=@-ref in commands/gsd/*.md must resolve to an existing file on disk; regression test in tests/bug-3135-capture-backlog-workflow.test.cjs; INVENTORY.md row + INVENTORY-MANIFEST.json families.workflows must stay in sync; "Invoked by" attribution must move when a flag absorbs a micro-skill
RULESET.WORKFLOW_EXECUTE_END_TO_END=ADR-0002 standard for single-workflow commands is "Execute end-to-end." (no bolded **Follow the X workflow** fragments); flag-dispatch routing uses "execute the X workflow end-to-end." in routing bullets
RULESET.ALLOWED-TOOLS-FRONTMATTER=command's allowed-tools must cover every tool the workflow calls (including Write for file creation); thin-wrapper pattern makes this easy to miss
RULESET.ARGUMENTS-SANITIZE=any workflow step constructing .planning/.../{SLUG}.md path from user input ($ARGUMENTS, parsed remainder) must sanitize inline ([a-z0-9-] only, reject ..//\\, max-length) — "(already sanitized)" must trace back to explicit guard; RESUME/fallback modes need own guards
RULESET.SHARED-HELPERS-LINT-VS-TEST=when a lint script and test suite both implement same constant (CANONICAL_TOOLS) or parser (parseFrontmatter, executionContextRefs), extract to scripts/*-helpers.cjs required by both — silent divergence otherwise
RULESET.GEMINI.TOOLS.ask_user=Gemini CLI has no ask_user tool; filter both AskUserQuestion and lowercase ask_user from tools frontmatter and neutralize both names in body text
RULESET.GEMINI.TEST_SENTINEL=convertClaudeToGeminiAgent regression should assert tools excludes ask_user, body excludes AskUserQuestion/ask_user, and Read still maps to read_file
RULESET.ADR-HEADER=every docs/adr/NNNN-*.md must open with - **Status:** Accepted|Proposed|Deprecated + - **Date:** YYYY-MM-DD immediately after title
RULESET.MANIFEST-CANONICAL-KEY=docs/INVENTORY-MANIFEST.json has a single top-level key: families; ALL SIX families.* arrays (agents/commands/workflows/references/cli_modules/hooks) are canonical, consumed by test suites — tests/inventory-manifest-sync.test.cjs reads all six, edit-phase/enh-2380/enh-2430 tests read commands+workflows; the old generated date field and the stale top-level workflows key are both gone; regen via node scripts/gen-inventory-manifest.cjs --write
RULESET.PR-SCOPE.one-concern-per-pr=split unrelated changes into separate PRs; cherry-pick doc changes to dedicated docs/ branch immediately, then force-push original to remove the commit
RULESET.TRIAGE-EXISTING-WORK=before writing agent brief for confirmed bug, check (1) local branches git branch -a | grep <issue>, (2) untracked/modified files on that branch, (3) stash, (4) open PRs with matching head branch — recover existing work rather than re-implement
RULESET.CR-THREAD-RESOLVE=after adding // allow-test-rule: to silence lint, resolve existing inline CR threads via graphql resolveReviewThread mutation before merge — open threads mislead future reviewers; pattern: gh api graphql -f query='mutation { resolveReviewThread(input:{threadId:"PRRT_..."}) { thread { isResolved } } }'
CodeRabbit + repo-process guards (machine-oriented predicates)
RULESET.CONTRIB.GATE.ORDER=issue-first -> approval-label -> code -> PR-link -> changeset/no-changelog
RULESET.CONTRIB.CLASSIFY.fix=requires confirmed/confirmed-bug before implementation
RULESET.CONTRIB.CLASSIFY.enhancement=requires approved-enhancement before implementation
RULESET.CONTRIB.CLASSIFY.feature=requires approved-feature before implementation
Workspace seams (machine-oriented predicates)
RULESET.GH.AUTH.DEFAULT=source .envrc GITHUB_TOKEN before gh; exception=ambient allowed only when user explicitly says machine-only fallback
RULESET.CODERABBIT.GUARD.OPEN_PRS=gh pr list --repo open-gsd/gsd-core --author @me --state open; repeat near end because open PR set can change mid-run
RULESET.CODERABBIT.GUARD.COMPLETE=required_checks_green && coderabbit_check_pass && graphQL(reviewThreads.unresolved_count)==0
RULESET.CODERABBIT.GUARD.GRAPHQL=reviewThreads(first:100){nodes{id isResolved comments{nodes{author body path line originalLine url}}}}; use unresolved threads as authoritative, not badge text alone
RULESET.CODERABBIT.GUARD.RERUN=after every push wait for CodeRabbit completion, then re-query unresolved threads; CodeRabbit can add new findings after earlier threads were resolved
RULESET.CODERABBIT.GUARD.RESOLVE=fix validated finding -> focused tests -> commit/push -> resolveReviewThread(threadId) -> wait CI/CodeRabbit -> final unresolved_count query
RULESET.CODERABBIT.GUARD.SCOPE=if a new @me open PR appears during final list, include it in the same guard pass before declaring all-open-PRs complete
RULESET.TESTS.CODERABBIT_FIX=prefer exported-function behavioral tests over source-grep; lint-no-source-grep rejects readFileSync source assertions without allow-test-rule
RULESET.WORKFLOW_MARKDOWN.FENCES=when editing shell snippets inside workflow markdown, preserve the opening language fence; malformed fence can create fresh CodeRabbit threads
RULESET.GEMINI.TOOLS.ask_user=Gemini CLI has no ask_user tool; filter both AskUserQuestion and lowercase ask_user from tools frontmatter and neutralize both names in Gemini body text
RULESET.GEMINI.TEST_SENTINEL=convertClaudeToGeminiAgent regression should assert tools excludes ask_user, body excludes AskUserQuestion/ask_user, and Read still maps to read_file
CI.GATE.issue-link-required=hard-fail if PR body lacks closes/fixes/resolves #<issue>
CI.GATE.changeset-lint=hard-fail for user-facing code diffs unless .changeset/* or PR has no-changelog label
CI.GATE.repair-sequence(PR)=create issue -> apply approval label -> edit PR body w/ closing keyword -> apply no-changelog if appropriate -> re-run checks
PR.3267.POSTMORTEM.root-cause=[missing issue link, missing changeset/no-changelog]
PR.3267.POSTMORTEM.recovery=[issue#3270 created, label approved-enhancement applied, PR reopened, body includes "Closes #3270", label no-changelog applied]
WORKTREE.SEAM.current=Worktree Safety Policy Module
WORKTREE.SEAM.files=[gsd-core/bin/lib/worktree-safety.cjs]
WORKTREE.SEAM.interface=[resolveWorktreeContext, parseWorktreePorcelain, planWorktreePrune, executeWorktreePrunePlan]
WORKTREE.SEAM.default-prune-policy=metadata_prune_only (non-destructive)
WORKTREE.SEAM.decision-1=retain non-destructive default; destructive path only as explicit future opt-in scaffold
WORKSTREAM.INVARIANT.migrate-name=must normalize through canonical slug policy
WORKSTREAM.INVARIANT.slug-contract=all .planning/workstreams/<name> must be addressable by set/get/status/complete
WORKSTREAM.REGRESSION.test-anchor=tests/workstream.test.cjs::normalizes --migrate-name to a valid workstream slug
ARCH.SKILL.improve-codebase.next-candidates=[Workstream Name Policy Module, Workstream Progress Projection Module, Active Workstream Pointer Store Module]
WORKTREE.SEAM.test-policy=cover all decision branches in policy module before changing prune behavior
WORKTREE.SEAM.test-anchors=[resolveWorktreeContext:has_local_planning|linked_worktree|not_git_repo|main_worktree, planWorktreePrune:git_list_failed|worktrees_present|no_worktrees|parser_throw_fallback, executeWorktreePrunePlan:missing_plan|skip_passthrough|unsupported_action|metadata_prune_only]
WORKTREE.SEAM.invariant=parser failure must degrade to metadata_prune_only and never escalate to destructive removal
WORKTREE.SEAM.execution-rule=prefer node --test tests/worktree-safety-policy.test.cjs for fast seam validation; avoid full npm test loop for seam-only changes
WORKTREE.SEAM.inventory-interface=[listLinkedWorktreePaths, inspectWorktreeHealth]
WORKTREE.SEAM.caller-rule=verify.cjs must consume inspectWorktreeHealth for W017 classification; no ad-hoc porcelain parsing in callers
WORKTREE.SEAM.test-anchor-w017=tests/orphan-worktree-detection.test.cjs + tests/worktree-safety-policy.test.cjs
WORKTREE.SEAM.inventory-snapshot=snapshotWorktreeInventory(repoRoot,{staleAfterMs,nowMs}) is canonical linked-worktree health snapshot for callers
PLANNING.PATH.PARITY.project-scope=.planning/<project> (never .planning/projects/<project>); mirror planning-workspace.cjs planningDir()
PLANNING.PATH.SEAM.helpers=helpers.planningPaths delegates to workspacePlanningPaths + resolveWorkspaceContext; precedence explicit-ws > env-ws > env-project > root
PLANNING.PATH.SEAM.init-handlers=[initExecutePhase, initPlanPhase, initPhaseOp, initMilestoneOp] consume helpers.planningPaths().planning (no direct relPlanningPath join)
WORKSTREAM.NAME.POLICY.cjs-module=gsd-core/bin/lib/workstream-name-policy.cjs owns toWorkstreamSlug + active-name/path-segment validation
WORKSTREAM.POINTER.SEAM.cjs-module=gsd-core/bin/lib/active-workstream-store.cjs owns read/write self-heal for .planning/active-workstream
CONFIG.SEAM.loadConfig-context=loadConfig(cwd,{workstream}) replaces env-mutation fallback; no temporary process.env GSD_WORKSTREAM rewrites
Release notes standard
RELEASE-NOTES.SCOPE=GitHub Releases body for tags vX.Y.Z, vX.Y.Z-rcN; not CHANGELOG.md (changeset workflow owns that)
RELEASE-NOTES.DEFAULT-STATE=auto-generated body is "What's Changed" PR list + Full Changelog link; treat as draft, not final
RELEASE-NOTES.GATE.hotfix=manual edit required; auto-generated body for vX.Y.{Z>0} is "Full Changelog only" and must be replaced with structured body
RELEASE-NOTES.GATE.rc=manual edit recommended; auto-generated PR list is acceptable for early RCs but final RC before vX.Y.0 should match standard
RELEASE-NOTES.GATE.minor=auto-generated body acceptable when PR titles are clean; promote to structured body when >20 PRs or contains feature+refactor+fix mix
RELEASE-NOTES.STANDARD.taxonomy=Keep-a-Changelog 1.1.0: Added | Changed | Deprecated | Removed | Fixed | Security | Documentation
RELEASE-NOTES.STANDARD.heading-level=## for category, ### for subgroup (area), - for bullet
RELEASE-NOTES.STANDARD.bullet-shape=**Bold user-visible change** — explanation of what was broken or what's new, leading with symptom not implementation. Trailing (#NNN) PR ref.
RELEASE-NOTES.STANDARD.subgroups=phase-planning-state | workstream | query-dispatch-cli | code-review | install | capture | docs | architecture | security
RELEASE-NOTES.STANDARD.footer.hotfix=Install/upgrade: \npx @opengsd/gsd-core@latest` RELEASE-NOTES.STANDARD.footer.rc=Install for testing: `npx @opengsd/gsd-core@next` (per branch->dist-tag policy) RELEASE-NOTES.STANDARD.footer.full-changelog=Full Changelog: https://github.com/open-gsd/gsd-core/compare/... RELEASE-NOTES.STANDARD.intro=optional one-paragraph framing for RC/feature releases; omit for pure-fix hotfixes`
RELEASE-NOTES.SOURCE.commits=git log <prev-tag>..<this-tag> --pretty=format:'%s%n%n%b' --no-merges
RELEASE-NOTES.SOURCE.changesets=.changeset/*.md (frontmatter pr: + body bullets)
RELEASE-NOTES.SOURCE.pr-bodies=gh pr view <NNN> --json title,body for fixes lacking a changeset
RELEASE-NOTES.SOURCE.precedence=changeset body > commit body > PR body > commit subject (prefer authored content over auto-generated)
RELEASE-NOTES.WORKFLOW.edit=gh release edit <tag> --notes-file <path>
RELEASE-NOTES.WORKFLOW.view=gh release view <tag> --json body --jq .body
RELEASE-NOTES.WORKFLOW.token=must use .envrc GITHUB_TOKEN per project CLAUDE.md; never ambient gh auth
RELEASE-NOTES.WORKFLOW.idempotency=gh release edit overwrites body wholesale; safe to re-run after refining
RELEASE-NOTES.ANTI-PATTERN=raw "What's Changed" PR list as final body for hotfix or feature release; "Full Changelog only" body for tagged release with >0 user-facing fixes
RELEASE-NOTES.ANTI-PATTERN.implementation-first=do not lead bullet with file path or function name; lead with symptom/user-visible behavior
RELEASE-NOTES.ANTI-PATTERN.risk-commentary=do not include "may break", "be careful", "test thoroughly" - per global CLAUDE.md no-risk-commentary rule
RELEASE-NOTES.EXAMPLE.hotfix=v1.41.1 (https://github.com/open-gsd/gsd-core/releases/tag/v1.41.1) - 14 fixes grouped by 6 subgroups
RELEASE-NOTES.EXAMPLE.rc=v1.42.0-rc1 (https://github.com/open-gsd/gsd-core/releases/tag/v1.42.0-rc1) - intro + Added/Changed/Fixed/Documentation taxonomy
RELEASE-NOTES.EXAMPLE.minor-auto-acceptable=v1.41.0 - kept auto-generated body; many small fixes with clean conventional-commit titles
RELEASE-NOTES.TEMPLATE.hotfix=## Fixed\n\n### <subgroup>\n- **<bold change>** — <explanation>. (#<PR>)\n\n---\n\nInstall/upgrade: \npx @opengsd/gsd-core@latest`\n\nFull Changelog: RELEASE-NOTES.TEMPLATE.rc=\n\n## Added\n### \n- — . (#)\n\n## Changed\n### Architecture\n- — . (#)\n\n## Fixed\n### \n- — . (#)\n\n## Documentation\n- — . (#)\n\n---\n\nThis is a release candidate. Install for testing:\n```bash\nnpx @opengsd/gsd-core@next\n```\n\nFull Changelog: `
RELEASE-NOTES.RELEASE-STREAM.main-branch=next (RCs) + latest (stable); install via @next or @latest
RELEASE-NOTES.RELEASE-STREAM.rule=streams do not mix; do not document @next in hotfix/stable notes
Repo-rule reinforcement — k320..k331
META.RULE.canonical-source-precedence=CONTRIBUTING.md > docs/adr/* > CONTEXT.md > agent memory
META.RULE.read-contributing-first=read CONTRIBUTING.md sections "Pull Request Guidelines" + "CHANGELOG Entries" before EVERY agent dispatch
META.RULE.brief-must-cite-doc=agent prompts MUST quote the canonical doc line being applied; paraphrasing from predicate memory drifts and produces violations
META.RULE.brief-no-paraphrase=writing "k040 — never leave changelog box unchecked" caused 5 of 8 agents to edit CHANGELOG.md in violation of CONTRIBUTING.md L110
PRED.k320.signal=changelog-direct-edit-forbidden
PRED.k320.canonical-source=CONTRIBUTING.md L110-123
PRED.k320.rule=do not edit CHANGELOG.md in feature/fix/enhancement PRs
PRED.k320.cure=drop .changeset/<adj>-<noun>-<noun>.md fragment ONLY
PRED.k320.tool=npm run changeset -- --type <T> --pr <NNN> --body "..."
PRED.k320.types=Added|Changed|Deprecated|Removed|Fixed|Security
PRED.k320.opt-out-label=no-changelog
PRED.k320.ci-enforcement=scripts/changeset/lint.cjs
PRED.k320.ci-paths-monitored=bin/ gsd-core/ agents/ commands/ docs/ hooks/ tests/ scripts/
PRED.k320.recovery=open Removed-typed cleanup PR deleting only the redundant row
PRED.k320.evidence=PR #3302 merge-conflict against #3308 CHANGELOG.md row 2026-05-09
PRED.k321.signal=cr-outside-diff-range-finding
PRED.k321.shape=CR posts "[!CAUTION] outside the diff" findings in review BODY, not in reviewThreads
PRED.k321.poll-shape=parse pulls/<n>/reviews body AND graphql reviewThreads
PRED.k321.resolution=address in code; no GraphQL resolveReviewThread needed for body-only findings
PRED.k321.evidence=PRs #3304/#3305 (2026-05-09): real Minor/Major findings in body, 0 threads
PRED.k322.signal=cr-sustained-throttle
PRED.k322.distinct-from=k080
PRED.k322.shape=ack posted, real review never lands within [5s, 410s] cooldown after burst of N PRs <15min
PRED.k322.cure-1=2nd retrigger ~10min after first ack
PRED.k322.cure-2=if silent at 50min, treat as silent-pass with maintainer flag in merge-commit body
PRED.k322.merge-gate-impact=k070 real_coderabbit_review_present unsatisfied; requires maintainer judgment
PRED.k322.evidence=PR #3306 (2026-05-09): 0 reviews after 50min + 2 retriggers
PRED.k323.signal=sibling-audit-cross-pr-overlap
PRED.k323.shape=2+ open issues touch same canonical bug site; each fix's sibling-audit produces overlapping diff
PRED.k323.cure-pre-dispatch=brief one agent canonical-owner; brief others to EXCLUDE shared site
PRED.k323.cure-alt=consolidate into single PR when 2+ issues share root cause
PRED.k323.recovery=close smaller PR as "subsumed by #N" or rebase second to drop overlap hunk
PRED.k323.evidence=#3300 (#3297) overlapped #3306 (#3298) on add-backlog.md hunks 2026-05-09
PRED.k324.signal=agent-terminates-mid-monitor
PRED.k324.k095-restatement=k095 confirmed shape: agent reports "waiting for monitor" / "tests still running" then terminates
PRED.k324.cure=verify via gh api on every agent-completion notification; never trust narrative
PRED.k324.poll-shape=gh pr view <n> --json mergeStateStatus,statusCheckRollup + pulls/<n>/reviews + graphql reviewThreads + issues/<n>/comments tail
PRED.k324.evidence=2026-05-09 session: 5+ mid-monitor terminations across PRs #3232/#3271/#3251/#3255/#3262
PRED.k325.signal=worktree-branch-lock-on-force-push
PRED.k325.shape=git checkout <branch> errors "already used by worktree at <agent-worktree>"
PRED.k325.cure=detached-HEAD: git checkout --detach $(git ls-remote origin <branch>); modify; commit; git push --force-with-lease=<branch>:<remote-sha> origin HEAD:refs/heads/<branch>
PRED.k325.cleanup=git worktree remove --force <path> for aged agent worktrees
PRED.k325.evidence=2026-05-09 CHANGELOG.md strip on PRs #3300/#3302/#3304/#3305 required detached-HEAD
PRED.k326.signal=brief-contradicts-canonical-doc
PRED.k326.shape=N parallel agents amplify a single brief-vs-doc contradiction into N violations
PRED.k326.cure=quote canonical doc verbatim in brief; mentally simulate "if all N agents follow this brief literally, do they violate any rule?"
PRED.k326.evidence=2026-05-09 brief "k040 — update CHANGELOG.md" → 5 of 8 agents violated CONTRIBUTING.md L110
PRED.k327.signal=cr-ack-vs-real-review
PRED.k327.ack-shape=body "✅ Actions performed - Full review triggered"
PRED.k327.real-review-shape=body starts "Actionable comments posted: N" OR "[!CAUTION] Some comments are outside the diff"
PRED.k327.distinguish-key=len(pulls/<n>/reviews) — ack=0, real=≥1
PRED.k327.cooldown-normal=[5s, 410s]
PRED.k327.cooldown-throttled=k322
PRED.k328.signal=pr-template-typed-heading-required
PRED.k328.canonical-source=CONTRIBUTING.md L101
PRED.k328.k100-restatement=heading must match issue class: bug→## Fix PR, enhancement→## Enhancement PR, feature→## Feature PR
PRED.k328.audit-list=[heading-matches-class, closing-keyword-present, changeset-fragment-or-no-changelog-label]
PRED.k329.signal=changeset-fragment-canonical-shape
PRED.k329.canonical-source=CONTRIBUTING.md L112-117 + .changeset/README.md
PRED.k329.filename=.changeset/<adj>-<noun>-<noun>.md
PRED.k329.frontmatter=---\\ntype: <Added|Changed|Deprecated|Removed|Fixed|Security>\\npr: <NNN>\\n---
PRED.k329.body=**<Bold user-visible change>** — <symptom-led explanation>. (#<NNN>)
PRED.k329.observed-clean=#3299 sunny-ibex-wave, #3301 sturdy-rams-caper, #3306 3298-phase-dir-prefix-drift-workflows
PRED.k330.signal=mempalace-diary-not-callable-by-ai
PRED.k330.shape=mempalace MCP tools require explicit user call; AI cannot trigger
PRED.k330.fallback=append predicate-format findings directly to CONTEXT.md
PRED.k331.signal=close-with-no-comment-is-literal
PRED.k331.shape=instruction "close with no comment (rationale)" — parenthetical is rationale, NOT comment body
PRED.k331.k101-restatement=k101 includes close-time --comment flag; rationale belongs in subsuming PR's squash-merge body
PRED.k331.cure=gh pr close <n> with NO --comment flag
PRED.k331.recovery=if violation lands, gh api -X DELETE repos/<o>/<r>/issues/comments/<id>
PRED.k331.evidence=2026-05-09 wave-3: violation on #3300 close, deleted within 30s
PROC.AGENT-DISPATCH.preflight=[read-CONTRIBUTING.md-fresh, read-relevant-ADRs, cite-specific-line-in-brief, require-closing-keyword, require-changeset-fragment, forbid-CHANGELOG.md-edit, require-isolation-worktree, forbid-self-PR-comment, mandate-trust-but-verify]
PROC.AGENT-DISPATCH.parallel-overlap-audit=before dispatching N sibling-audit fixers, compute file-set union and assign canonical owners
PROC.AGENT-DISPATCH.completion-verify=run k324.poll-shape on every agent-completion notification
PROC.MERGE-WAVE.ordering=[wave1: isolated-files, wave2: CHANGELOG-only-overlap (better: strip per k320), wave3: same-file-overlap with explicit decision]
PROC.MERGE-WAVE.preflight=gh pr view <n> --json files for every PR; identify overlap pairs; surface to maintainer
PROC.MERGE-WAVE.changelog-strip-pattern=detached-HEAD per k325 + git checkout main -- CHANGELOG.md + commit + force-with-lease
PROC.MERGE-WAVE.merge-tool=gh pr merge <n> --squash --delete-branch
PROC.MERGE-WAVE.merge-tool-warning=delete-branch may fail with "used by worktree at" — harmless; remote branch still deleted
Triage and merge-wave lessons
WAVE.LESSON.changelog-policy-violation-multiplier=brief contradicting CONTRIBUTING.md L110 produced violations on 5 of 8 PRs (#3300, #3302, #3304, #3305, #3308); k326 + k320 capture
WAVE.LESSON.cr-throttle-burst-correlation=8 PRs in <15min triggered k322 sustained-throttle on multiple PRs (#3306 worst case)
WAVE.LESSON.sibling-audit-overlap=k015-family parallel dispatch on #3297 + #3298 produced k323 add-backlog.md cross-PR overlap
WAVE.LESSON.agent-narrative-unreliable=k095/k324 confirmed at scale: 5 of 8 agents terminated mid-monitor with stale claims requiring direct verification
WAVE.LESSON.k101-still-trips=even after CONTEXT.md k101 reinforcement, agent of record posted self-PR comment on close; k331 adds explicit close-time literal-instruction guard
Defect anti-patterns and fix-forwards
DEFECT.SCOPE.window=PRs #3306..#3325 + sibling fixes #3240/#3242/#3245/#3257/#3261/#3267/#3286/#3287
DEFECT.FORMAT=class.sub-key=value | classes are greppable; each class carries detect / fix / anchor sub-keys when applicable
DEFECT.REMOVED-BUT-NEEDED.symptom=file/key removed because "no longer used" without verifying every consumer (workflows, docs, manifests, npm scripts)
DEFECT.REMOVED-BUT-NEEDED.examples=#3316 root package-lock.json (root package.json declares deps; workflows use cache:'npm' + npm ci), e3b52c70 docs referenced removed /gsd-new-workspace
DEFECT.REMOVED-BUT-NEEDED.detect=before deletion, grep filename across .github/workflows, gsd-core/, docs/, package.json scripts; if any reference exists removal is incomplete
DEFECT.REMOVED-BUT-NEEDED.fix-forward=restore the file or update every consumer in the same commit; do not paper over with --no-package-lock or workflow workarounds that lose reproducibility
DEFECT.STATE-TRAMPLE.symptom=state-mutation paths overwrite curated values when body-derived computation is narrower than what's stored in frontmatter
DEFECT.STATE-TRAMPLE.examples=#3242 (Last Activity overwrote progress.completed_plans), #3257 (nested plans/ files uncounted), #3261 (buildStateFrontmatter), #3265 (canonical fields), #3286 (record-metric/add-decision sections)
DEFECT.STATE-TRAMPLE.detect=any state writer that calls buildStateFrontmatter without preserving existing progress.* keys; any mutation surface that does not honor shouldPreserveExistingProgress
DEFECT.STATE-TRAMPLE.fix-forward=route through state-document.cjs/.ts shouldPreserveExistingProgress + normalizeProgressNumbers (extracted in #3316 SDK-first seams)
DEFECT.PHASE-DIR-PREFIX-DRIFT.symptom=multiple workflow files independently construct .planning/phases/{NN}-{slug} paths; project_code prefix or slug normalization missing in some surfaces
DEFECT.PHASE-DIR-PREFIX-DRIFT.examples=#3287 (init.phase-op + init.plan-phase first-touch), #3306/PRED.k015 (plan-milestone-gaps + import + add-backlog), #3297/#3298 (sibling reports)
DEFECT.PHASE-DIR-PREFIX-DRIFT.detect=grep mkdir/touch/path.join with {NN}-{slug} or padded_phase + phase_slug; if not consuming expected_phase_dir from init.* JSON it is drifting
DEFECT.PHASE-DIR-PREFIX-DRIFT.fix-forward=consume expected_phase_dir from init.phase-op / init.plan-phase output; never re-construct from padded_phase + slug in workflow steps
DEFECT.PHASE-DIR-PREFIX-DRIFT.anchor=tests/bug-3298-phase-dir-prefix-drift-in-workflows.test.cjs (broad regression across workflow surfaces)
DEFECT.STACKED-PR-AUTO-RETARGET.symptom=PR #N is stacked on branch B; branch B merges to main and is deleted; GitHub does not reliably auto-retarget #N to main; PR shows DIRTY/CONFLICTING with phantom conflicts
DEFECT.STACKED-PR-AUTO-RETARGET.examples=#3311 base fix/3255-add-json-errors-mode-gsd-tools deleted after #3304 merged
DEFECT.STACKED-PR-AUTO-RETARGET.detect=ls-remote shows base ref absent; PR base still points at the deleted ref; mergeable=CONFLICTING with no real diff conflicts
DEFECT.STACKED-PR-AUTO-RETARGET.fix-forward=PATCH /repos/{owner}/{repo}/pulls/{N} -f base=main; rebase head onto current main; resolve carry-over commits (parent commits will auto-drop as patch contents already upstream)
DEFECT.BOT-BRANCH-STALE-BASE.symptom=auto-branch.yml creates fix/{N}-{slug} when issue is filed; branch is anchored to issue-creation main; by the time work begins, main has moved
DEFECT.BOT-BRANCH-STALE-BASE.examples=#3309 fix/3309-checkpoint-type-human-verify-burns-token (was at e14ef535; main at 2e87c60a)
DEFECT.BOT-BRANCH-STALE-BASE.detect=git merge-base origin/<bot-branch> origin/main returns the bot branch tip — confirms the bot branch is an ancestor of main, just stale
DEFECT.BOT-BRANCH-STALE-BASE.fix-forward=git checkout --detach origin/main; do work; git checkout -b <same-branch-name>; force-push with --force-with-lease
DEFECT.SUPERSEDED-CONCURRENT-PRS.symptom=multiple in-flight PRs attack overlapping subsets of the same issue; the broadest one merges first; narrower siblings remain open with phantom conflicts
DEFECT.SUPERSEDED-CONCURRENT-PRS.examples=#3303 + #3307 superseded by #3306 (all addressing #3297/#3298 project_code prefix family)
DEFECT.SUPERSEDED-CONCURRENT-PRS.detect=after a fix lands on main, grep recently-merged PR title for shared keyword/issue; check open PRs touching same files; if open PRs are subsets of merged work they are superseded
DEFECT.SUPERSEDED-CONCURRENT-PRS.fix-forward=close superseded PRs via gh api PATCH state=closed; do not comment on self-authored PRs (k101); the link to the merged PR makes supersession discoverable in PR history
DEFECT.PROMPT-INJECTION-SCAN-COLLISION.symptom=custom XML element name in agent .md file matches scripts/scan-prompt-injection regex; legitimate agent vocabulary trips the security gate
DEFECT.PROMPT-INJECTION-SCAN-COLLISION.examples=#3309 added a bare 'human' element (angle-bracket-wrapped) for verify-block harvesting; tests/prompt-injection-scan.security.test.cjs flags angle-bracket-wrapped names matching system|assistant|human (open or close form)
DEFECT.PROMPT-INJECTION-SCAN-COLLISION.detect=any new bare <system|assistant|human|user> tag in agents/*.md
DEFECT.PROMPT-INJECTION-SCAN-COLLISION.fix-forward=hyphenate the tag (<human-check>, <assistant-prompt>) — scanner regex matches bare names only
DEFECT.INVENTORY-DRIFT.symptom=new file added under gsd-core/references/ or gsd-core/workflows/ without updating docs/INVENTORY.md row AND docs/INVENTORY-MANIFEST.json
DEFECT.INVENTORY-DRIFT.examples=#3309 planner-human-verify-mode.md (caught by tests/inventory-manifest-sync.test.cjs)
DEFECT.INVENTORY-DRIFT.detect=tests/inventory-manifest-sync.test.cjs fails with "New surfaces not in manifest"; tests/inventory-headings-countfree.test.cjs fails if a (N shipped) count is re-added to a heading
DEFECT.INVENTORY-DRIFT.fix-forward=update INVENTORY.md row entry; run node scripts/gen-inventory-manifest.cjs --write to regen INVENTORY-MANIFEST.json (all six families.* arrays are canonical — see RULESET.MANIFEST-CANONICAL-KEY)
DEFECT.AGENT-FILE-SIZE-CAP-BREACH.symptom=adding to agents/gsd-planner.md (or other large agent files) exceeds the 45K char extraction-evidence threshold
DEFECT.AGENT-FILE-SIZE-CAP-BREACH.state=gsd-planner.md is already 49,121 chars on main (over 45K); test fails on main; net-new content makes it strictly worse
DEFECT.AGENT-FILE-SIZE-CAP-BREACH.detect=tests/planner-decomposition.test.cjs ("planner is under 45K chars (proves mode sections were extracted)") and tests/reachability-check.test.cjs ("file stays under 50000 char limit")
DEFECT.AGENT-FILE-SIZE-CAP-BREACH.fix-forward=mirror MVP mode pattern — extract full rules to gsd-core/references/planner-<mode>.md, leave a slim Detection section in the agent file with @-reference to the new file
DEFECT.CHANGESET-PR-FIELD-DRIFT.symptom=.changeset/*.md frontmatter pr: value is the issue number, a guess made before PR opened, or a stale stacked-PR number
DEFECT.CHANGESET-PR-FIELD-DRIFT.examples=#3316 (pr:3312 was the issue), #3325 (pr:3319 was a guess); already covered in CONTEXT.md L94 + L186 but recurs every cycle
DEFECT.CHANGESET-PR-FIELD-DRIFT.detect=changeset pr: value mismatches the actual PR number returned by gh api POST /pulls
DEFECT.CHANGESET-PR-FIELD-DRIFT.fix-forward=author changeset with placeholder pr:0; immediately after gh api POST /pulls returns the number, edit changeset and amend or follow-up commit; never guess
DEFECT.WORKTREE-FETCH-SHA-DIVERGENCE.symptom=in a worktree, git fetch origin pull/N/head:pr-N produces commits with SHAs different from the actual remote PR head SHA; force-push rejected as non-fast-forward despite recent fetch
DEFECT.WORKTREE-FETCH-SHA-DIVERGENCE.examples=this session, branch fix/3309-... and pr-3316
DEFECT.WORKTREE-FETCH-SHA-DIVERGENCE.detect=git rev-parse HEAD~1 vs git rev-parse origin/<actual-branch-ref> — if they differ despite fetch the local copy was rewritten by some checkout-time hook
DEFECT.WORKTREE-FETCH-SHA-DIVERGENCE.fix-forward=git checkout --detach origin/<actual-remote-branch> directly; do work from detached HEAD; push HEAD:<remote-branch>
DEFECT.WINDOWS-FS-OPS.symptom=fs.renameSync / fs.copyFileSync hits EPERM/EBUSY on Windows when antivirus or another process holds a transient handle on the target
DEFECT.WINDOWS-FS-OPS.examples=c47c2c5d build-hooks rename → copy fallback, d2412271 install Windows persistent SDK shim
DEFECT.WINDOWS-FS-OPS.detect=any rename/copy in build/install path without try/catch fallback
DEFECT.WINDOWS-FS-OPS.fix-forward=catch EPERM/EBUSY/EACCES, fall back to copy + unlink with retry, surface degraded-mode message; never silently swallow
DEFECT.UNBOUNDED-SUBPROCESS.symptom=git/npm subprocess shelled out without timeout; CLI hangs indefinitely on stuck remote, large repo, or missing network
DEFECT.UNBOUNDED-SUBPROCESS.examples=a33cbe72 worktree fix bound git subprocesses with timeout
DEFECT.UNBOUNDED-SUBPROCESS.detect=execSync/execFileSync/spawnSync without timeout option in non-test code; especially git list-worktrees, git fetch, npm view
DEFECT.UNBOUNDED-SUBPROCESS.fix-forward=add timeout (5-30s for git, 60s for npm); on timeout return degraded result + structured warning rather than throw
DEFECT.PARSER-BRITTLE-MARKER-WHITELIST.symptom=human-output parser whitelists known markers (severity, status); silently drops unfamiliar markers as malformed
DEFECT.PARSER-BRITTLE-MARKER-WHITELIST.examples=ac518646/#3263 code-review SUMMARY parser rejected BL-/blocker variants
DEFECT.PARSER-BRITTLE-MARKER-WHITELIST.detect=any parser with hard-coded marker list; any parser that returns empty for non-matching input without warning
DEFECT.PARSER-BRITTLE-MARKER-WHITELIST.fix-forward=accept variants explicitly (case-insensitive, hyphen/space alternatives); on unknown marker emit a structured WARN with the original line so the human can fix the source
DEFECT.HALT-COST-PATTERN.symptom=architecturally-sound checkpoint pattern produces hidden token cost because subagent context is discarded across the pause and respawn
DEFECT.HALT-COST-PATTERN.examples=#3309 checkpoint:human-verify (mid-flight halt = full executor cold-start per round-trip; reporter measured "tens of thousands of tokens" per halt)
DEFECT.HALT-COST-PATTERN.detect=any subagent-spawning workflow with mid-flight pause-and-resume that does not preserve subagent context
DEFECT.HALT-COST-PATTERN.fix-forward=offer config flag for end-of-phase aggregation; if cost dominates make end-of-phase the default; route deferred items through existing verifier surface, do not invent new writer
DEFECT.HOOK-OVER-ENFORCEMENT.symptom=PreToolUse hook keeps blocking gh pr edit / gh issue edit even after all required files are read in the session
DEFECT.HOOK-OVER-ENFORCEMENT.examples=this session repeatedly hit "Refusing to run gh issue create|edit / gh pr create|edit" despite reading every listed file
DEFECT.HOOK-OVER-ENFORCEMENT.detect=hook re-fires on each invocation regardless of session-state read receipts
DEFECT.HOOK-OVER-ENFORCEMENT.fix-forward=use gh api -X PATCH repos/{owner}/{repo}/pulls/{N} or repos/{owner}/{repo}/issues/{N} directly — same effect, hook regex does not match
DEFECT.DEFAULT-FLIP-DOCUMENTATION.symptom=PR flips a config default but does not call out the migration semantics (when does the new default take effect; existing configs vs new configs; what the opt-back-in looks like)
DEFECT.DEFAULT-FLIP-DOCUMENTATION.examples=#3309 v2 default flip from mid-flight to end-of-phase
DEFECT.DEFAULT-FLIP-DOCUMENTATION.detect=any PR that changes a default value in CONFIG_DEFAULTS or buildNewProjectConfig; check that PR body Breaking Changes section explicitly covers (a) when the new default takes effect, (b) opt-back-in command, (c) effect on in-flight artifacts
DEFECT.DEFAULT-FLIP-DOCUMENTATION.fix-forward=template — "new default takes effect when .planning/config.json is rewritten (config-set, fresh project, regenerated config); existing artifacts continue to work; opt-back-in: gsd config-set <key> <old-value>"
DEFECT.SOURCE-GREP-IN-NEW-TESTS.symptom=new test file uses readFileSync + .includes() / .match() against source code (CONTEXT.md L82); contradicts the test rule lint script
DEFECT.SOURCE-GREP-IN-NEW-TESTS.detect=scripts/lint-no-source-grep.cjs (npm run lint:tests) fails with line-number-precise violation
DEFECT.SOURCE-GREP-IN-NEW-TESTS.fix-forward=replace with runGsdTools(...) behavioral test capturing JSON; if asserting agent .md content (which IS the runtime contract) add // allow-test-rule: source-text-is-the-product with one-line justification
DEFECT.GENERATIVE-PRIORITY=these defect classes share a common root: parallel implementations diverge silently because no parity test enforces equality at the test layer
DEFECT.GENERATIVE-FIX=for any new constant/array/parser shared between two parallel surfaces (two workflow surfaces, or a generated artifact and its hand-authored source), the same commit MUST add a parity assertion that fails when the two diverge
DEFECT.GENERATIVE-EXEMPLAR=tests/runtime-launcher-parity.test.cjs (asserts every workflow bash block uses the canonical gsd_run launcher — the in-repo pattern for enforcing equality across parallel surfaces)
DEFECT.FRONTMATTER-SCALAR-BROAD-GREP.symptom=a YAML-frontmatter scalar (e.g. VERIFICATION.md status) read with grep "^key:" over the WHOLE markdown report instead of the frontmatter block; a key: line in the body (code block, copied artifact, example) returns extra matches that concatenate after cut|tr into a value matching no expected token, so a valid state is misrouted
DEFECT.FRONTMATTER-SCALAR-BROAD-GREP.examples=#586/PR #650 ship.md verification gate — grep "^status:" also matched body status: lines, yielding passed+gaps_found+human_needed instead of passed and blocking a passed phase; the same broad-grep still lives in execute-phase.md (consolidation tracked by #651)
DEFECT.FRONTMATTER-SCALAR-BROAD-GREP.detect=grep "^<key>:" on a *.md whose result is compared to exact tokens, with no frontmatter scoping and no -m1; one body line beginning <key>: is enough to break it
DEFECT.FRONTMATTER-SCALAR-BROAD-GREP.fix-forward=scope to the leading frontmatter block and take the first match: sed -n '/^---$/,/^---$/p' "$f" | grep -m1 "^<key>:" | cut -d: -f2 | tr -d ' '; fix every parallel copy in the same change or consolidate behind one queryable seam (#651)
DEFECT.TEST-SHELL-PIPELINE-NONPORTABLE.symptom=a test that parses a workflow bash block out of a *.md and runs it via execFileSync('bash',...) breaks on Windows two ways: the fence regex uses a literal \n after the bash fence that will not match CRLF and trips windows-test-parity-guard (fenceRegexLiteralNewline); and git-bash exists so a bash-presence probe is true, but an os.tmpdir() Windows path (C:\...) is un-globbable in bash so the pipeline returns empty and assertions fail
DEFECT.TEST-SHELL-PIPELINE-NONPORTABLE.examples=#586/PR #650 tests/ship-586-verification-routing.test.cjs — the fence \n offender failed ubuntu-24/macos/coverage, then the Windows tmpdir-path glob failed full test (windows-latest,22) at fail 3; both were invisible to file-scoped gsd-test-both runs because the parity guard is only scanned by the full suite
DEFECT.TEST-SHELL-PIPELINE-NONPORTABLE.detect=test does readFileSync(md).match for a bash fence with literal \n, OR execFileSync('bash',...) gated only on a bash-presence probe; also verifying a new test with a file-scoped run instead of the full suite hides repo-wide static guards
DEFECT.TEST-SHELL-PIPELINE-NONPORTABLE.fix-forward=match the fence with \r?\n and normalize the captured block to LF; gate pipeline execution on process.platform !== 'win32' && hasBash since the extraction LOGIC is platform-independent and POSIX coverage suffices; run the full suite (or the parity/lint guards) before push when adding a test file
DEFECT.WINDOWS-TEST-PORTABILITY.symptom=local gsd-test runs Mac+Linux only (no Windows host); Windows-only test failures (chmod exec-bit not honored for PATH-executing extension-less scripts in Git Bash msys2; / vs \ path-separator in assertions; Git Bash msys2 shell semantics) surface ONLY in CI test (windows-latest,*) / full test (windows-latest,*) lanes, never locally
DEFECT.WINDOWS-TEST-PORTABILITY.examples=PR #1084 (chmod 0o755 + bare-command execution failed on windows lane); test files that assert path.join result without normalizing to forward slashes
DEFECT.WINDOWS-TEST-PORTABILITY.detect=npm run lint:windows-test-portability (tripwire: flags tests combining chmod exec-bit with sh/bash -c and no platform guard); watch CI windows matrix green before declaring a PR done
DEFECT.WINDOWS-TEST-PORTABILITY.fix-forward=gate platform-specific execution with if (process.platform !== 'win32'); normalize path expectations to forward slashes with .replace(/\\/g, '/'); invoke scripts via explicit interpreter (sh <path>) rather than relying on exec-bit; annotate // windows-portability-ok: <reason> when a bypass is intentional
DEFECT.WINDOWS-TEST-PORTABILITY.prevention=run lint:ci before opening a PR; treat the CI windows lane as the only true Windows signal — gsd-test (Mac/Linux only) cannot substitute for it
Shell Command Projection Module (expanded glossary entry, 2026-05-13)
Module owning all OS-facing I/O for the tool: runtime-aware command-text rendering (hook commands, PATH action lines, shim scripts), subprocess dispatch (run-git, run-npm, run-tool, probeTty), and platform file I/O (platformWriteSync, platformReadSync, platformEnsureDir). Single seam for platform-conditional logic — one place to fix any shell or file write regression across Windows, macOS, and Linux. Lives in gsd-core/bin/lib/shell-command-projection.cjs. See ADR-0009 (superseded "does not execute" constraint) and ADR-0010 (superseded File Operation Engine).
Invariants:
- Result shape: all run-* return
{ exitCode, stdout, stderr }; never throw on non-zero exit code. - Platform policy owned at the seam:
shell: process.platform === 'win32'lives only in run-npm; probeTty returnsnullon Windows. - Normalization policy: platformWriteSync owns full
normalizeMdfor.md; CRLF-to-LF + trailing newline for all others; callers must NOT pre-callnormalizeMd. _normalizeMdis re-implemented inline (not imported fromcore.cjs) to avoid circular dep.atomicWriteFileSync,safeReadFile,normalizeMdwere incore.cjsexports (retired in epic #1267); callers now import these from their respective leaf modules directly.
Migration plan: Phase 1 (#3465) seam additions complete; Phase 2 (#3466) targets 6 subprocess files; Phase 3 (#3467) targets 15 fs files (215 call sites); Phase 4 (#3468) removes compat exports.
Session log (chronological, append-only, one line per session)
Discipline: new operational lessons go into a predicate above. Each dated entry below is a one-line pointer at the predicates derived from that session — NOT a prose narrative. If you can't compress a session's lesson into a predicate, the lesson isn't sharp enough yet — keep grinding.
SESSION.2026-05-05=[PRED.k320..k331 introduced; DEFECT.SOURCE-GREP-IN-NEW-TESTS, DEFECT.CHANGESET-PR-FIELD-DRIFT, DEFECT.PHASE-DIR-PREFIX-DRIFT, DEFECT.PROMPT-INJECTION-SCAN-COLLISION; ADR-0002 thin-wrapper pattern findings folded into RULESET.WORKFLOW_*]
SESSION.2026-05-05.sdk-bridge=PR #3158 SDK Runtime Bridge — observability isolation rule; strict-mode dispatchMode reporting invariant; transport decision ordering (guard before event emission); folded into Dispatch Policy Module glossary
SESSION.2026-05-09=[8-PR triage wave, 7 merged + 1 subsumed; META.RULE.* introduced; WAVE.LESSON.* captured; k320/k322/k323/k326/k331 evidence; AI Ops Memory predicate format established]
SESSION.2026-05-10=[ai-ops memory consolidation; release-notes standard taxonomy + templates; RELEASE-NOTES.* predicates introduced]
SESSION.2026-05-13=[Shell Command Projection Module expansion (#3465-#3468); ADR-0009 superseded; new exports for subprocess dispatch and platform file I/O; phase-gated migration plan; PR #3464 three-gate invariant CI+CR+unresolved=0; PR #3470 stash-include-untracked rebase pattern]
SESSION.2026-05-14=[#3095/PR #3490 EXEC.CLASSIFY.* introduced (Anthropic/Copilot/Codex/Gemini cross-runtime rate-limit sentinel coverage); #3489/PR #3499 DEFECT.STATE-TRAMPLE.idempotency-oracle (STATE.md current_phase field is oracle for state.complete-phase); #3488/PR #3501 DAG resolver same-phase short-form depends_on (shortFormToId index added to sdk/src/query/phase.ts); #3491/PR #3502 DEFECT.NESTED-GIT-INIT (gitWorktreeInfoInternal helper); #3493/PR #3500 extractCurrentMilestone generic Phase Details continuation past planned-milestone siblings; #3503/PR #3504 DEFECT.PATH-SUBSTRING-CHECK (trailing-slash anchor for homedir checks); #3346/PR #3505 codex AoT TOML leaf-key via extractFlatHookEventName; #3506/PR #3507 label-scoped stale-bot sub-job pattern; multi-PR triage operational lessons folded into PROC.TRIAGE.*; #3508 DEFECT.AGENT-ISOLATION-SILENT-FAIL; gsd-test image-missing auto-build (locally-built image via embedded heredoc Dockerfile); refined PRED.k322 threshold to 3 PRs/<10min]
SESSION.2026-05-15=[#3537/PR #3538 DEFECT.PHASE-REGEX-FANOUT — phaseMarkdownRegexSource promoted to core.cjs and wired to 7 sites; parity-style regression test established as DEFECT.GENERATIVE-FIX exemplar; trek-e/gsd-test-runner#1 filed for DEFECT.GSD-TEST-MIRROR-POISONED — chown-back-before-exec legacy gap (poisoned holodeck mirror unstuck via authorized docker chown to remote 1000:1000); RULESET.PR-FLOW.* codified from project CLAUDE.md load-bearing rule; first dispatch under run-tests-before-create held cleanly (PR #3520 worker stopped on Docker exit 12 infra failure, orchestrator opened PR after unblock); CONTEXT.md refactored from 882 lines of mixed prose+predicates into ~500 lines of pure-predicate format with chronological session log]
SESSION.2026-05-15.parallel-fix-dispatch=[#3542/PR #3546 prohibit git stash family in executor agents (shared refs/stash across worktrees); #3541/PR #3547 non-TTY resolution for installer prompt-user actions (default remove for SDK build artifacts, keep for skills/gsd-*/SKILL.md); #3545 filed for gsd-test-summary concurrent /tmp output collision; new predicates DEFECT.HOOK-OVER-ENFORCEMENT.read-tool-tracking, DEFECT.GSD-TEST-CONCURRENT-OUTPUT-COLLISION, DEFECT.SUBAGENT-LONG-RUNNING-BG-STALL, DEFECT.AGENT-RETIRED-SLASH-SYNTAX-DRIFT, PROC.PARALLEL-FIX-DISPATCH; agent-trust-but-verify caught /gsd-update retired-syntax comment slip in #3541 implementation before PR open]
SESSION.2026-05-16=[multi-PR triage wave (#3577/3581/3640/3641/3642/3648/3649/3637/3639). Established global PreToolUse hook ~/.claude/hooks/test-memory-guard.sh denying new node/test spawns when sum(RSS of node|vitest|jest|...) >= 4 GiB on the 24 GB Mac OR when a same-runner process is already in argv[0] — hard deny via hookSpecificOutput.permissionDecision=deny. PR #3577 fix: revert config-ensure-section dispatch to CJS cmdConfigEnsureSection (SDK author wrote single-section semantics under a name whose legacy callers expect full-default config init); plus 3 SDK parity carve-outs (configNewProject defaults align with sdk/shared/config-defaults.manifest.json, return relative .planning/config.json path, drop quotes from Unknown config key, lead malformed-JSON error with "Failed to read config.json:"). PR #3649 fix: chunk node --test spawn at 28K argv ceiling (Windows CreateProcess lpCommandLine cap 32,767 was instantly aborting unchunked spawn of 546 paths). Chunking fix surfaced 14 pre-existing Windows-only test bugs (4010 pass / 14 fail; vs 0/0 before — entire suite was un-runnable on Windows). PRs #3639 + #3637 confirmed unable to stand alone (legitimately depend on Phase 6 scaffolding only present on feat/3575-enforcement-hardening) — user decision: cherry-pick into #3577 and close. Five other PRs each had ≤1 unresolved CR thread of the changeset-pr-number / null-vs-throw / implicit-Claude-runtime / docs-stale-guidance / hardcoded-tests-path family — all quick wins. New predicates: DEFECT.SDK-PORT-NAME-COLLISION, DEFECT.WINDOWS-ARGV-OVERFLOW, DEFECT.STACKED-PR-CANNOT-STAND-ALONE, DEFECT.CANARY-VERSION-LEAK, DEFECT.GSD-TEST-HOST-MID-RUN-DEATH, RULESET.HARNESS.test-memory-guard, RULESET.PR-FLOW.docker-before-push, RULESET.PR-FLOW.templates-mandatory]
DEFECT.NAME-COLLISION.symptom=a router migration rebinds CLI dispatch for a canonical command name to a handler with a different positional-arg shape; every legacy no-arg / wrong-arg caller then errors out at the new handler's own validation throw
DEFECT.NAME-COLLISION.examples=#3577 config-ensure-section (legacy = no-arg full-default init via ensureConfigFile→buildNewProjectConfig; the rebound configEnsureSection = single-section ensure requiring args[0]; all CLI callers pass no args; handler throws "Usage: config-ensure-section <section>")
DEFECT.NAME-COLLISION.detect=trace every CLI/test caller of the canonical name → if any caller's argv shape differs from the rebound handler's args[0] expectation, the migration broke the legacy contract
DEFECT.NAME-COLLISION.fix-forward=either (a) bind the dispatch to a handler whose body mirrors legacy semantics (e.g. configNewProject when no args), or (b) keep the dispatch case calling the original handler directly (precedent: 7d5dfa9d codex runtime carve-out). Whichever path, add a behavioral test that round-trips the legacy invocation shape to lock the contract
DEFECT.SDK-PORT-NAME-COLLISION.generative-tie=instance of DEFECT.GENERATIVE-PRIORITY — parity assertion at the test layer between CJS handler shape and SDK handler shape would have failed at PR open
DEFECT.WINDOWS-ARGV-OVERFLOW.symptom=execFileSync(node, ['--test', ...N paths]) succeeds on Linux/macOS, instantly exits with code 1 and no test output on Windows when N×avg(path_len) exceeds 32,767 chars (CreateProcess lpCommandLine cap)
DEFECT.WINDOWS-ARGV-OVERFLOW.examples=#3649 scripts/run-tests.cjs spawning 546 paths (~85 chars each ≈ 46 KB); Linux ARG_MAX 2 MB allows it, Windows aborts in ~70 ms with zero test output making the failure look like the runner itself crashed
DEFECT.WINDOWS-ARGV-OVERFLOW.detect=Windows CI job at "Run unit tests" exits with code 1 within seconds of starting, no node:test output between "run-tests: suite=… files=N: …" line and "Process completed with exit code 1"; same job on Linux/macOS runs full duration
DEFECT.WINDOWS-ARGV-OVERFLOW.fix-forward=chunk argv into batches whose total length stays under 28,000 chars (headroom under the 32,767 ceiling); run each chunk sequentially; aggregate exit codes (first non-zero wins). Expose RUN_TESTS_MAX_CMDLINE_CHARS env override so cross-platform regression tests can force chunking with short tmp paths
DEFECT.WINDOWS-ARGV-OVERFLOW.test-anchor=tests/run-tests-harness.test.cjs "Windows argv-overflow chunking (issue #3597)" — 30 long-named fixture files + RUN_TESTS_MAX_CMDLINE_CHARS=2000 → asserts run-tests: chunk N/M marker in stderr; pattern works on every platform
DEFECT.SHARED-ARTIFACT-MUTATION-IN-CONCURRENT-TEST.symptom=a test deletes/rewrites a SHARED REAL build artifact or fixture (e.g. gsd-core/bin/lib/*.cjs, the build tsbuildinfo) that other test files require; node --test runs files concurrently, so innocent concurrent tests intermittently fail with "Cannot find module" / ENOENT while the racy test itself passes (victim-not-culprit, leg-asymmetric red); placing mutable build state inside a copied/shipped tree (gsd-core/bin/) additionally races install-test fs.cpSync copies → copyfile ENOENT
DEFECT.SHARED-ARTIFACT-MUTATION-IN-CONCURRENT-TEST.examples=#996/88e30d53 — bug-969 hardening tests fs.unlinkSync'd + restored the real gsd-core/bin/lib/core.cjs and set tsBuildInfoFile inside gsd-core/bin/ → next red across the full-test matrix (macOS/Windows) + ubuntu-24 coverage leg, ~40-50 MODULE_NOT_FOUND/ENOENT per leg; reproduced locally on iteration 1; fixed #1001/#1002
DEFECT.SHARED-ARTIFACT-MUTATION-IN-CONCURRENT-TEST.detect=grep tests for fs.unlinkSync|rmSync|writeFileSync|renameSync|cpSync targeting paths resolved from the repo root (join(__dirname,'..',...)) under gsd-core/bin/lib or a shared committed fixture, instead of a mkdtempSync temp dir; any build helper (e.g. ensureBuiltArtifacts) invoked with real-tree paths during the concurrent test phase; any tsBuildInfoFile / build-cache path that lands inside a copied/shipped dir (gsd-core/bin/)
DEFECT.SHARED-ARTIFACT-MUTATION-IN-CONCURRENT-TEST.fix-forward=tests mutate ONLY isolated mkdtempSync copies — never delete/rewrite shared real build outputs while node --test runs files concurrently; parameterize build helpers to accept {root,srcDir,outDir,tsBuildInfoPath,tsconfigPath} overrides and point the test at a throwaway temp project (precedent: #1002 ensureBuiltArtifacts(overrides)); keep mutable build state (tsbuildinfo) OUTSIDE copied/shipped trees (repo root, gitignored) + best-effort self-heal of stale bin-local copies; this is the concrete instance of the RULESET.TESTS.delete-bad-tests real-race class
DEFECT.SHARED-ARTIFACT-MUTATION-IN-CONCURRENT-TEST.test-anchor=tests/bug-969-test-infra-flake-hardening.test.cjs (hermetic temp-project rewrite); regression gate = 10x concurrent run of that suite + tests/state.test.cjs + tests/install.test.cjs must be clean (reproduces on iter 1 when racy)
DEFECT.STACKED-PR-CANNOT-STAND-ALONE.symptom=patch PR was authored against scaffolding (handler files, lint scripts, generated modules) that exists only on an unmerged upstream feature branch; the PR's "base" on GitHub is the feature branch, not main; merging requires the upstream PR to land first
DEFECT.STACKED-PR-CANNOT-STAND-ALONE.examples=#3639 + #3637 both targeted base=feat/3575-enforcement-hardening (the Phase 6 PR #3577); #3639 modifies SDK-bridge calls in 6 family-router files that on main do NOT have any SDK-bridge call yet; #3637 patches scripts/lint-shared-module-handsync.cjs which does not exist on main at all
DEFECT.STACKED-PR-CANNOT-STAND-ALONE.detect=gh pr view <n> --json baseRefName shows non-main base; OR git rebase --onto origin/main <upstream-pr-branch> <patch-pr-branch> produces real (not whitespace) conflicts at files the patch claims to modify; OR git cat-file -e origin/main:<patch-target-file> errors with "does not exist in origin/main"
DEFECT.STACKED-PR-CANNOT-STAND-ALONE.fix-forward=user policy (this session, 2026-05-16): every PR must stand alone. Resolution = cherry-pick the patch's unique commits onto the upstream PR head, push to upstream PR branch, close patch PR with "subsumed by #<upstream>". Alternatives explicitly rejected: leaving stacked open ("no, fold them in") and closing-without-folding ("we want the fix")
DEFECT.STACKED-PR-CANNOT-STAND-ALONE.anti-pattern=blindly running git rebase --onto origin/main on the patch branch — produces "conflicts" that are really "the scaffolding doesn't exist yet"; resolving them means reinventing the upstream PR's contribution, which duplicates work and creates merge hazards. Recognize the shape early via cat-file probe before rebasing
DEFECT.CANARY-VERSION-LEAK.symptom=package.json version on main carries a -canary.<N> suffix that per release policy belongs to the dev branch only; nothing publishable depends on the version string at runtime, but every consumer of the version metadata (release flow, install banners, statusline) sees the dev-channel label
DEFECT.CANARY-VERSION-LEAK.examples=2026-05-16 audit found origin/main + origin/feat/3575-enforcement-hardening both at "version": "1.50.0-canary.0" in sdk/package.json AND root package.json; npm view @opengsd/gsd-sdk versions returned ["0.1.0"] only, dist-tag latest=0.1.0, @1.50.0-canary.0 404 — confirms the string is metadata-only, never published. git log -S '"version": "1.50.0-canary.0"' origin/main blamed commit 2d32ad82 fix(plan-phase)... (#3206), a fix PR that accidentally carried the version bump from a dev-branch base
DEFECT.CANARY-VERSION-LEAK.detect=jq -r .version package.json on origin/main shows a -canary suffix; OR npm view <pkg> dist-tags shows latest != main's version
DEFECT.CANARY-VERSION-LEAK.fix-forward=open a chore/* PR against main that resets the version strings to the canonical pre-canary stable; rebase open PRs to pick it up; gate at PR open with a CI check that rejects -canary versions on PRs targeting main
DEFECT.GSD-TEST-HOST-MID-RUN-DEATH.symptom=pick_host succeeds at probe time (ssh -o ConnectTimeout=3 -o BatchMode=yes "$h" true); subsequent ssh "$h" 'docker run ...' hangs indefinitely because the chosen host went unreachable between probe and exec; gsd-test-summary buffers stderr until the wrapper exits, so the operator sees no progress at all
DEFECT.GSD-TEST-HOST-MID-RUN-DEATH.examples=2026-05-16 redshirt probed up at 12:48 UTC, gsd-test-summary picked it, docker container spawned, then redshirt's ssh daemon stopped responding — banner-exchange timeout. Test stalled 20+ minutes with the wrapper's output file at 0 bytes
DEFECT.GSD-TEST-HOST-MID-RUN-DEATH.detect=gsd-test-summary's task output file at /private/tmp/claude-*/tasks/<id>.output stays 0 bytes for >5 min after launch; ps shows the test still alive; ssh -o ConnectTimeout=5 <probed-host> true now times out
DEFECT.GSD-TEST-HOST-MID-RUN-DEATH.fix-forward=TaskStop the wrapper; pkill -f gsd-test-summary + pkill -f "ssh <dead-host>"; re-run gsd-test-summary so pick_host re-randomizes from the live set (probe each ~/.config/gsd-test/hosts entry first to confirm). Upstream fix candidate: gsd-test should add a heartbeat read on the ssh-stdin channel and abort + retry on a different host after N silent seconds
DEFECT.GSD-TEST-HOST-MID-RUN-DEATH.related=DEFECT.GSD-TEST-MIRROR-POISONED (legacy bind-mount ownership); GSD-TEST-CONCURRENT-OUTPUT-COLLISION (file collision) — host-mid-run-death is the third independent gsd-test infra failure mode this month
RULESET.HARNESS.test-memory-guard=~/.claude/hooks/test-memory-guard.sh fires on every Bash PreToolUse; if argv[0]∈{node|vitest|jest|mocha|tsx|ts-node|tap|ava|playwright|cypress} OR matches (npm|pnpm|yarn|bun) (run )?(t|test|tests|vitest|jest); blocks via hookSpecificOutput.permissionDecision=deny when sum(RSS of running matching procs, excluding tsserver|*-mcp|claude|Electron|...) ≥ 4 GiB OR when argv[0] basename matches a running process's argv[0]. Exception: node --version|-v|--help|-h|-p|-e are trivial probes and skip the check. Designed for a 24 GB Mac where prior accidental fan-out exhausted RAM
RULESET.PR-FLOW.docker-before-push=before ANY git push of any fix to any PR, run gsd-test-summary (docker on the remote, mirrors ubuntu CI) and confirm exit 0. macOS-local node --test is NOT a substitute — many failures are platform-specific (path separators, case sensitivity, locale, fs semantics). Watchdog with Monitor on the output log; never set a sleep/timer and walk away. Source: user feedback 2026-05-16 — "we don't set a timer we actively watch and record results in real time as possible"
RULESET.PR-FLOW.templates-mandatory=every gh pr create|edit|gh issue create|edit MUST first invoke the gh-templates-first skill and Read (Read tool, not Bash cat — k321 read-tracking) the matching template in .github/. Apply ALL required sections; never write freeform bodies. Repo enforces this via gsd-pr-template-policy GitHub Action which flags any non-templated body — the bot allows the PR to stay open only because authors are contributors-or-higher, but the warning is a real complaint that must be cured. Source: user feedback 2026-05-16 (multi-message escalation) — "the whole reason i have that github action is because you fucking blow through and ignore using the templates"
Executor failure classification (#3095 / PR #3490)
EXEC.CLASSIFY.handler=gsd-core/bin/lib/agent-command-router.cjs:classifyAgentFailure (registered via command-aliases.cjs; mutation:false outputMode:json)
EXEC.CLASSIFY.workflow=gsd-core/workflows/execute-phase.md step 7; class-distinct prompts (quota-to-wait-for-reset; classify-handoff-bug-to-spot-check; unknown-to-continue/stop)
EXEC.CLASSIFY.classes={class:'quota-exceeded'|'classify-handoff-bug'|'unknown-failure', sentinel?, retryAfterSeconds?}
EXEC.CLASSIFY.sentinel-order=most specific first: 429 beats too-many-requests; quota beats resource_exhausted; case-insensitive; canonical sentinel value is lower-cased form
EXEC.CLASSIFY.cross-runtime=Anthropic/CC: usage limit|rate limit|quota|429|retry-after; Copilot CLI: rate_limit (stem); Codex CLI: 429|usage_limit_reached|too many requests; Gemini CLI: RESOURCE_EXHAUSTED|exceeded your
EXEC.CLASSIFY.precedence=quota sentinel wins over classifyHandoffIfNeeded bug when both appear
EXEC.CLASSIFY.retry-after-parser=\bretry[-_ ]after[:\s]+(\d+)\b avoids embedded-word false matches like noretry-after
EXEC.CLASSIFY.proactive-signal-not-usable=Anthropic exposes anthropic-ratelimit-* headers + Agent SDK RateLimitEvent; Claude Code subprocess does NOT forward to hooks/statusline today (upstream #33820, #22407, #32796)
DEFECT.GSD-TEST-MIRROR-POISONED.symptom=gsd-test-summary --both exits docker=23 (rsync partial transfer) with mkstemp Permission denied on remote mirror files; mirror has root-owned artifacts from prior cold runs
DEFECT.GSD-TEST-MIRROR-POISONED.detect=docker stderr shows rsync: [generator] delete_file: unlink(...) failed: Permission denied (13) OR [receiver] mkstemp ".gsd-*.<suffix>" failed
DEFECT.GSD-TEST-MIRROR-POISONED.root-cause=container ran without --user; build:hooks wrote into bind-mount as root; chown-back-before-exec patch closes forward path but not legacy hosts
DEFECT.GSD-TEST-MIRROR-POISONED.recovery=ssh <host> 'docker run --rm -v ~/gsd-mirror-gsd-core:/work gsd-test:node22 chown -R <remote-uid>:<remote-gid> /work'; remote-uid is the SSH user's uid on the remote (1000 on holodeck, NOT local Mac 501)
DEFECT.GSD-TEST-MIRROR-POISONED.upstream=trek-e/gsd-test-runner#1 — proposes self-healing init-time chown probe
DEFECT.HOOK-OVER-ENFORCEMENT.read-tool-tracking=gh-templates-first PreToolUse hook tracks Read tool invocations specifically; Bash cat/head of the same file does NOT satisfy the hook; future-self must use Read tool from the first contact with template files
DEFECT.GSD-TEST-CONCURRENT-OUTPUT-COLLISION.symptom=two simultaneous gsd-test-summary --both invocations (e.g. one per worktree) both crash with UnicodeDecodeError in parse_events_from_file; "local exit=1 docker exit=1" reported even though remote containers ran fine
DEFECT.GSD-TEST-CONCURRENT-OUTPUT-COLLISION.root-cause=gsd-test-summary lines 126-127 default LOCAL_OUT/DOCKER_OUT to fixed /tmp/gsd-test-{local,docker}.jsonl; concurrent line-buffered writers interleave bytes mid-multibyte → split UTF-8 sequence → decoder explodes on f.read()
DEFECT.GSD-TEST-CONCURRENT-OUTPUT-COLLISION.detect=two gsd-test-summary --both runs in flight; UnicodeDecodeError in parse_events_from_string traceback; /tmp/gsd-test-*.jsonl size mismatch vs total events emitted
DEFECT.GSD-TEST-CONCURRENT-OUTPUT-COLLISION.fix-forward=set per-invocation LOCAL_OUT=/tmp/gsd-test-<tag>-local.jsonl DOCKER_OUT=/tmp/gsd-test-<tag>-docker.jsonl env vars; or serialize the runs; upstream fix tracked in #3545 (default to tempfile.mkstemp + advisory flock)
DEFECT.GSD-TEST-CONCURRENT-OUTPUT-COLLISION.upstream=open-gsd/gsd-core#3545
DEFECT.SUBAGENT-LONG-RUNNING-BG-STALL.symptom=spawned sub-agent kicks off gsd-test-summary --both via Bash run_in_background, then stops on the harness "you will be notified" message; never receives the notification because cross-turn task-notifications are only delivered to the top-level orchestrator
DEFECT.SUBAGENT-LONG-RUNNING-BG-STALL.detect=sub-agent returns prematurely with text like "I should wait for the notification per CLAUDE.md" and incomplete work in its worktree (commits absent, push absent, PR absent)
DEFECT.SUBAGENT-LONG-RUNNING-BG-STALL.fix-forward=keep gsd-test-summary --both at the top-level orchestrator; sub-agents either run it foreground with timeout: 1500000 (25min) and block, OR delegate the test step back to the orchestrator (write commits + return); never have a sub-agent fire-and-await a backgrounded long task
DEFECT.SUBAGENT-LONG-RUNNING-BG-STALL.anchor=project CLAUDE.md "Top-level orchestrator (cross-turn notifications available) vs Sub-agent worker (no cross-turn notifications)" guidance — load-bearing for multi-worktree parallel fix dispatch
DEFECT.AGENT-RETIRED-SLASH-SYNTAX-DRIFT.symptom=sub-agent writes /gsd-<cmd> (legacy hyphen syntax) in code comments or doc strings while implementing a fix; lands as part of the implementation diff
DEFECT.AGENT-RETIRED-SLASH-SYNTAX-DRIFT.examples=#3541 implementation included a typical /gsd-update path comment in installer-migration-report.cjs; caught by tests/bug-2543-gsd-slash-namespace.test.cjs (#3443 invariant)
DEFECT.AGENT-RETIRED-SLASH-SYNTAX-DRIFT.detect=tests/bug-2543-gsd-slash-namespace.test.cjs prints "Found N retired /gsd-<cmd> reference(s) — use /gsd:<cmd> instead" with line-number-precise violations
DEFECT.AGENT-RETIRED-SLASH-SYNTAX-DRIFT.fix-forward=replace /gsd-<cmd> with /gsd:<cmd> at the cited file:line; healthy emergent property — project-wide invariant test catches drift agents would never self-correct
DEFECT.AGENT-RETIRED-SLASH-SYNTAX-DRIFT.lesson=agent-trust-but-verify is load-bearing — sub-agent reporting "done" is not a substitute for running the full suite; the invariant test surfaces drift even in doc-only changes
PROC.PARALLEL-FIX-DISPATCH.pattern=bot triage brief → worktree per branch → parallel sub-agents do rubber-duck/RCA/TDD implementation only → top-level orchestrator owns commit + gsd-test-summary --both + push + PR + changeset-pr-backfill
PROC.PARALLEL-FIX-DISPATCH.rationale=long-running test runs need cross-turn notifications (orchestrator-only); CONTRIBUTING.md gh-templates-first hook requires session-scoped Read calls sub-agents wouldn't otherwise make; sequencing test runs avoids GSD-TEST-CONCURRENT-OUTPUT-COLLISION
PROC.PARALLEL-FIX-DISPATCH.observed=#3541 + #3542 dispatched simultaneously this session; PRs #3546 #3547 opened green; one syntax slip caught by AGENT-RETIRED-SLASH-SYNTAX-DRIFT and fixed before second PR opened
DEFECT.HOOK-OVER-ENFORCEMENT.write-bypass=security_reminder_hook can block Write on substring match (e.g. a literal child-process call-expression token); workaround is heredoc to /tmp then mv into place, or use Edit instead — Edit hooks are more lenient than Write hooks
PROC.TRIAGE.routing-incoming=stale-bug-already-fixed to close as duplicate of originating issue + cite fix PR + first stable tag; release-publish-or-backport to ready-for-human; reporter-can-self-test to awaiting-retest
PROC.TRIAGE.comment-shape=lead with "duplicate of #NNNN, fixed by PR #MMMM, in v1.X.Y"; show current code snippet proving bug-surface gone; give @latest and @next upgrade commands; close
PROC.TRIAGE.no-duplicate-label=this repo has no duplicate label; framing lives in comment text + closing the issue
PR fix discipline — patterns observed 2026-05-23
Full detail in ~/.claude/skills/gsd-pr-fix-discipline/SKILL.md. AI agents MUST check this section before pushing to open-gsd/gsd-core.
INVENTORY / manifest drift
- Symptom:
tests/inventory-manifest-sync.test.cjsfails —"New surfaces not in manifest"; ortests/inventory-headings-countfree.test.cjsfails if a(N shipped)count was re-added to a heading - Affected this session: #154, #156, #143, #155, #169
- Fix: Add row to
docs/INVENTORY.md+node scripts/gen-inventory-manifest.cjs --write
Slash command two-tier confusion
- Symptom:
tests/bug-2543-gsd-slash-namespace.test.cjsortests/bug-3584-runtime-slash-emitters.test.cjsfails - Affected this session: #154 (three passes), #164 (added the authoritative matrix)
- Fix: Consult
## Slash-command formsection of this file before touching any/gsd-or/gsd:token — colon foragents//commands/, hyphen for runtime emitters
Concurrency cancel-in-progress masking real CI state
- Symptom:
gh pr checksshows failures but the latest commit SHA's run was cancelled before Tests even started - Affected this session: #154, #136
- Fix:
gh workflow run Tests --repo open-gsd/gsd-core --ref <branch>; verify withgh run list --branch <branch> --workflow Tests --limit 1 --json status,conclusion,headSha
Missing changeset fragment
- Symptom:
changeset-lintfails withfail_missing_fragment(~5s) - Affected this session: #156, #143, #164
- Fix:
node scripts/changeset/new.cjs --type <Type> --pr <N> --body "..."or applyno-changeloglabel for doc-only PRs
Cross-platform Windows / Node 24 hazards
- Symptom: Windows CI leg fails; Mac/Linux green — POSIX paths in
node -e, hardcoded.nvmrcfixtures, 2000ms wall-clock budget flakes,synckituncaught Worker exception - Affected this session: #157
- Fix: Use
./package.jsonnot$PWD/package.json; write.nvmrcdynamically inbefore()hook; use 5000ms budget; wrapgetExecuteForCjs()intry/catch
Sub-agent rubber-duck stall
- Symptom: Sub-agent returns a question list and halts; no commits or push in the worktree
- Affected this session: Multiple agents mid-session
- Fix: Every sub-agent brief must include:
Skill rubber-duck is BANNED in this sub-agent. Convert to internal monologue and proceed.
Stacked PR squash-merge breakage
- Symptom: After base PR squash-merges, stacked PR shows conflicts or wrong diff; GitHub auto-retarget fails
- Affected this session: #158 stacked on #156
- Fix:
git rebase --onto main <old-base> <stacked-branch>then force-push andgh pr edit --base main
tee pipe swallowing exit codes
- Symptom:
gsd-test-summary --both 2>&1 | tee /tmp/logreturns0even when Docker reports failures - Affected this session: Session-wide risk
- Fix: Run un-piped, or
set -o pipefailbefore the pipe
Auto-merge disabled
- Symptom:
gh pr merge --autoreturnsGraphQL: Auto merge is not allowed for this repository - Affected this session: All stacked PRs
- Fix: Merge manually by hand in dependency order once CI greens;
gh pr merge <N> --squash --repo open-gsd/gsd-core