@opengsd/gsd-core 1.5.0-rc.2 → 1.5.0-rc.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/agents/gsd-advisor-researcher.md +1 -1
  3. package/agents/gsd-assumptions-analyzer.md +1 -1
  4. package/agents/gsd-code-fixer.md +1 -1
  5. package/agents/gsd-code-reviewer.md +1 -1
  6. package/agents/gsd-codebase-mapper.md +1 -1
  7. package/agents/gsd-debugger.md +1 -1
  8. package/agents/gsd-doc-writer.md +1 -1
  9. package/agents/gsd-eval-auditor.md +1 -1
  10. package/agents/gsd-executor.md +1 -1
  11. package/agents/gsd-integration-checker.md +1 -1
  12. package/agents/gsd-mempalace-curator.md +47 -0
  13. package/agents/gsd-nyquist-auditor.md +1 -0
  14. package/agents/gsd-phase-researcher.md +1 -1
  15. package/agents/gsd-plan-checker.md +1 -1
  16. package/agents/gsd-planner.md +1 -1
  17. package/agents/gsd-project-researcher.md +1 -1
  18. package/agents/gsd-research-synthesizer.md +1 -1
  19. package/agents/gsd-roadmapper.md +55 -2
  20. package/agents/gsd-security-auditor.md +1 -0
  21. package/agents/gsd-ui-auditor.md +1 -1
  22. package/agents/gsd-ui-checker.md +1 -1
  23. package/agents/gsd-ui-researcher.md +1 -1
  24. package/agents/gsd-verifier.md +13 -2
  25. package/bin/install.js +61 -64
  26. package/commands/gsd/mempalace-capture.md +71 -0
  27. package/commands/gsd/mempalace-recall.md +102 -0
  28. package/commands/gsd/ns-context.md +4 -2
  29. package/commands/gsd/progress.md +2 -1
  30. package/gemini-extension.json +1 -1
  31. package/gsd-core/bin/gsd-tools.cjs +277 -95
  32. package/gsd-core/bin/lib/active-workstream-store.cjs +6 -0
  33. package/gsd-core/bin/lib/capability-activation.cjs +86 -0
  34. package/gsd-core/bin/lib/capability-registry.cjs +1468 -11
  35. package/gsd-core/bin/lib/capability-state.cjs +128 -21
  36. package/gsd-core/bin/lib/capability-writer.cjs +354 -0
  37. package/gsd-core/bin/lib/check-command-router.cjs +328 -1
  38. package/gsd-core/bin/lib/clusters.cjs +2 -0
  39. package/gsd-core/bin/lib/command-roster.cjs +19 -0
  40. package/gsd-core/bin/lib/commands.cjs +33 -10
  41. package/gsd-core/bin/lib/config-loader.cjs +7 -8
  42. package/gsd-core/bin/lib/config-schema.cjs +32 -3
  43. package/gsd-core/bin/lib/config.cjs +81 -26
  44. package/gsd-core/bin/lib/core.cjs +5 -2
  45. package/gsd-core/bin/lib/edge-probe.cjs +25 -2
  46. package/gsd-core/bin/lib/frontmatter.cjs +53 -1
  47. package/gsd-core/bin/lib/git-base-branch.cjs +194 -0
  48. package/gsd-core/bin/lib/init.cjs +36 -11
  49. package/gsd-core/bin/lib/install-profiles.cjs +57 -1
  50. package/gsd-core/bin/lib/installer-migration-report.cjs +1 -0
  51. package/gsd-core/bin/lib/loop-resolver.cjs +157 -16
  52. package/gsd-core/bin/lib/model-resolver.cjs +47 -5
  53. package/gsd-core/bin/lib/phase.cjs +99 -23
  54. package/gsd-core/bin/lib/plan-drift-guard.cjs +117 -0
  55. package/gsd-core/bin/lib/probe-core.cjs +117 -1
  56. package/gsd-core/bin/lib/profile-output.cjs +45 -4
  57. package/gsd-core/bin/lib/profile-pipeline-command-router.cjs +138 -0
  58. package/gsd-core/bin/lib/roadmap-parser.cjs +13 -3
  59. package/gsd-core/bin/lib/roadmap.cjs +97 -7
  60. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +1946 -0
  61. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +54 -30
  62. package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +27 -19
  63. package/gsd-core/bin/lib/runtime-homes.cjs +26 -20
  64. package/gsd-core/bin/lib/state-command-router.cjs +15 -3
  65. package/gsd-core/bin/lib/state-document.cjs +46 -1
  66. package/gsd-core/bin/lib/state.cjs +461 -94
  67. package/gsd-core/bin/lib/verify.cjs +92 -8
  68. package/gsd-core/bin/lib/worktree-safety.cjs +2 -1
  69. package/gsd-core/bin/shared/config-defaults.manifest.json +1 -2
  70. package/gsd-core/bin/shared/config-schema.manifest.json +0 -18
  71. package/gsd-core/bin/shared/model-catalog.json +1 -0
  72. package/gsd-core/references/edge-probe.md +11 -0
  73. package/gsd-core/references/loop-hook-dispatch.md +61 -0
  74. package/gsd-core/references/prohibition-probe-fixtures/01-streak-reminder/expected.json +14 -0
  75. package/gsd-core/references/prohibition-probe-fixtures/02-clean-utility/expected.json +4 -0
  76. package/gsd-core/references/prohibition-probe-fixtures/03-multi-prohibition/expected.json +32 -0
  77. package/gsd-core/references/prohibition-probe.md +248 -0
  78. package/gsd-core/templates/config.json +1 -1
  79. package/gsd-core/templates/spec.md +14 -0
  80. package/gsd-core/workflows/audit-milestone.md +5 -3
  81. package/gsd-core/workflows/autonomous.md +10 -5
  82. package/gsd-core/workflows/code-review-fix.md +9 -7
  83. package/gsd-core/workflows/code-review.md +8 -6
  84. package/gsd-core/workflows/complete-milestone.md +1 -5
  85. package/gsd-core/workflows/discuss-phase.md +14 -0
  86. package/gsd-core/workflows/execute-phase.md +86 -146
  87. package/gsd-core/workflows/execute-plan.md +21 -6
  88. package/gsd-core/workflows/help/modes/full.md +7 -1
  89. package/gsd-core/workflows/new-project.md +3 -3
  90. package/gsd-core/workflows/next.md +50 -2
  91. package/gsd-core/workflows/pause-work.md +7 -1
  92. package/gsd-core/workflows/plan-phase.md +91 -221
  93. package/gsd-core/workflows/plan-review-convergence.md +14 -4
  94. package/gsd-core/workflows/pr-branch.md +4 -2
  95. package/gsd-core/workflows/profile-user.md +3 -1
  96. package/gsd-core/workflows/progress.md +58 -1
  97. package/gsd-core/workflows/quick.md +12 -8
  98. package/gsd-core/workflows/resume-project.md +17 -1
  99. package/gsd-core/workflows/review.md +19 -2
  100. package/gsd-core/workflows/secure-phase.md +4 -2
  101. package/gsd-core/workflows/settings-advanced.md +2 -0
  102. package/gsd-core/workflows/settings.md +27 -1
  103. package/gsd-core/workflows/ship.md +58 -5
  104. package/gsd-core/workflows/spec-phase.md +75 -0
  105. package/gsd-core/workflows/validate-phase.md +4 -2
  106. package/gsd-core/workflows/verify-phase.md +14 -4
  107. package/gsd-core/workflows/verify-work.md +27 -11
  108. package/hooks/dist/gsd-ensure-canonical-path.js +305 -0
  109. package/hooks/dist/gsd-statusline.js +1 -1
  110. package/hooks/dist/managed-hooks-registry.cjs +1 -0
  111. package/hooks/gsd-ensure-canonical-path.js +305 -0
  112. package/hooks/gsd-statusline.js +1 -1
  113. package/hooks/hooks.json +1 -0
  114. package/hooks/managed-hooks-registry.cjs +1 -0
  115. package/package.json +5 -4
  116. package/scripts/affected-tests-lib.cjs +16 -4
  117. package/scripts/build-hooks.js +7 -0
  118. package/scripts/changeset/new.cjs +17 -3
  119. package/scripts/fix-slash-commands.cjs +15 -3
  120. package/scripts/gen-capability-registry.cjs +373 -49
  121. package/scripts/gen-inventory-manifest.cjs +1 -4
  122. package/scripts/gen-loop-host-contract.cjs +55 -0
  123. package/scripts/issue-version-gate.cjs +140 -0
  124. package/scripts/lint-allow-test-rule-refs.allowlist.json +327 -0
  125. package/scripts/lint-allow-test-rule-refs.cjs +162 -0
  126. package/scripts/lint-test-file-count.allowlist.json +14 -0
  127. package/scripts/mutation-matrix.cjs +108 -7
  128. package/scripts/pr-target-policy.cjs +63 -0
  129. package/scripts/release-tarball-smoke.cjs +7 -1
  130. package/scripts/research-profiles.cjs +5 -5
  131. package/scripts/run-tests.cjs +178 -17
@@ -0,0 +1,71 @@
1
+ ---
2
+ name: gsd:mempalace-capture
3
+ description: "File a phase artifact into MemPalace; mirror decision facts into its temporal KG"
4
+ argument-hint: "[CONTEXT.md|PLAN.md|SUMMARY.md]"
5
+ allowed-tools:
6
+ - Read
7
+ - Bash
8
+ requires: [config]
9
+ ---
10
+
11
+ **STOP -- DO NOT READ THIS FILE. You are already reading it. This prompt was injected into your context by the command system. Using the Read tool on this file wastes tokens. Begin executing Step 0 immediately.**
12
+
13
+ ## Step 0 -- Banner
14
+
15
+ **Before ANY tool calls**, display this banner:
16
+
17
+ ```
18
+ GSD > MEMPALACE CAPTURE
19
+ ```
20
+
21
+ Then proceed to Step 1.
22
+
23
+ ## Step 1 -- Config Gate
24
+
25
+ Check whether the MemPalace capability is enabled by reading `.planning/config.json` directly with the Read tool.
26
+
27
+ 1. Read `.planning/config.json` with the Read tool.
28
+ 2. If the file does not exist, or `config.mempalace` is absent, or `config.mempalace.enabled !== true`, or `config.mempalace.capture_artifacts !== true`: display the disabled message and **STOP**.
29
+ 3. Otherwise proceed to Step 2.
30
+
31
+ **Disabled message:**
32
+
33
+ ```
34
+ GSD > MEMPALACE CAPTURE
35
+
36
+ MemPalace capture is disabled (mempalace.enabled / mempalace.capture_artifacts).
37
+ Nothing was filed; the loop proceeds normally.
38
+ ```
39
+
40
+ This step is `onError: skip` at `discuss:post` / `plan:post` / `verify:post` -- capture never fails a phase.
41
+
42
+ ## Step 2 -- Resolve target
43
+
44
+ 1. **Artifact.** Take the artifact from `$ARGUMENTS`. If absent, infer from the loop point: `discuss:post` → `CONTEXT.md`, `plan:post` → `PLAN.md`, `verify:post` → `SUMMARY.md`.
45
+ 2. **Room.** Map artifact → room:
46
+ - `CONTEXT.md` → `decisions`
47
+ - `PLAN.md` → `planning`
48
+ - `SUMMARY.md` → `milestones`
49
+ (Confirmed problem→fix pairs go to `problems` — see the `capture-problems` fragment used at `execute:wave:post`.)
50
+ 3. **Wing.** `config.mempalace.wing` if non-empty, else `config.project_code`, else the repo directory name.
51
+ 4. **Mode / transport.** Read `config.mempalace.memory_mode`. Prefer MCP (`mempalace_*`) when your MemPalace MCP server is registered and your runtime permits those tools; otherwise use the `mempalace` CLI (covered by this skill's `Bash` allow-tool), as in `mempalace-recall`.
52
+
53
+ ## Step 3 -- File verbatim (idempotent)
54
+
55
+ On any error or timeout, stop and let the phase continue -- capture is best-effort.
56
+
57
+ 1. **Dedup first.** Interactive: `mempalace_check_duplicate` on the artifact's deterministic drawer id. Headless: rely on `mempalace mine`'s content-hash idempotency.
58
+ 2. **Add the drawer (verbatim).** File the exact artifact text into `room: <room>` of `wing: <wing>` with provenance (`source_file`, phase id). Interactive: `mempalace_add_drawer`. Headless: `mempalace mine <path> --wing <wing> --room <room>`.
59
+ 3. **Mirror KG facts** when `config.mempalace.mirror_kg` is true: extract decision/delivery facts and `mempalace_kg_add` them with `valid_from` = the phase date (e.g. `(<project>, decided, <decision>)` from CONTEXT; `(<phase>, delivered, <capability>)` from SUMMARY). Only `augment` is currently wired, so these are an *additive* mirror of `.planning/graphs/`. (`kg_backend`/`replace` are forward-declared and behave as `augment` today.)
60
+ 4. Re-running a phase MUST NOT create duplicate drawers (deterministic ids + `check_duplicate`).
61
+
62
+ ## Step 4 -- Report
63
+
64
+ Print a one-line summary: `Filed <artifact> → <wing>/<room> (<n> KG facts)` or `MemPalace unavailable — capture skipped`.
65
+
66
+ ## Anti-Patterns
67
+
68
+ 1. DO NOT let any MemPalace error fail the step -- capture is `onError: skip`.
69
+ 2. DO NOT write lossy summaries -- store the verbatim artifact text (AAAK compression is a separate, optional index).
70
+ 3. DO NOT prune or delete drawers here -- pruning (`sync --apply`) is the curator agent's job at `ship:post`, wing-scoped only.
71
+ 4. DO NOT skip the config gate or the dedup check.
@@ -0,0 +1,102 @@
1
+ ---
2
+ name: gsd:mempalace-recall
3
+ description: "Recall decisions, patterns, and surprises from MemPalace before planning"
4
+ argument-hint: "[phase-slug]"
5
+ allowed-tools:
6
+ - Read
7
+ - Write
8
+ - Bash
9
+ requires: [config]
10
+ ---
11
+
12
+ **STOP -- DO NOT READ THIS FILE. You are already reading it. This prompt was injected into your context by the command system. Using the Read tool on this file wastes tokens. Begin executing Step 0 immediately.**
13
+
14
+ ## Step 0 -- Banner
15
+
16
+ **Before ANY tool calls**, display this banner:
17
+
18
+ ```
19
+ GSD > MEMPALACE RECALL
20
+ ```
21
+
22
+ Then proceed to Step 1.
23
+
24
+ ## Step 1 -- Config Gate
25
+
26
+ Check whether the MemPalace capability is enabled by reading `.planning/config.json` directly with the Read tool.
27
+
28
+ **DO NOT use `gsd-tools config get-value`** -- it hard-exits on missing keys.
29
+
30
+ 1. Read `.planning/config.json` with the Read tool.
31
+ 2. If the file does not exist: write the "unavailable" stub (Step 4) and **STOP**.
32
+ 3. Parse the JSON. Proceed to Step 2 only if `config.mempalace && config.mempalace.enabled === true` **and** `config.mempalace.recall_on_plan !== false`. Otherwise display the disabled message and **STOP** (`recall_on_plan: false` turns plan-time recall off while leaving the rest of the capability enabled).
33
+
34
+ **Disabled message:**
35
+
36
+ ```
37
+ GSD > MEMPALACE RECALL
38
+
39
+ MemPalace memory is disabled. To activate:
40
+
41
+ node <runtime-home>/gsd-core/bin/gsd-tools.cjs config-set mempalace.enabled true
42
+
43
+ Recall is opt-in; the loop proceeds normally without it.
44
+ ```
45
+
46
+ This step is `onError: skip` at `plan:pre` -- recall never blocks planning.
47
+
48
+ ## Step 2 -- Resolve wing, mode, and transport
49
+
50
+ 1. **Wing.** Use `config.mempalace.wing` if non-empty; otherwise derive from `config.project_code`; otherwise fall back to the repository directory name.
51
+ 2. **Mode.** Read `config.mempalace.memory_mode` (`augment` | `kg_backend` | `replace`, default `augment`). Only `augment` is wired today, so recall always treats the palace as additive; `kg_backend`/`replace` are forward-declared and behave as `augment`.
52
+ 3. **Transport.** Prefer the **MCP tools** (`mempalace_*`) in interactive runs *when your MemPalace MCP server is registered and your runtime permits those tools*. Otherwise — headless/cron/autonomous runs, or runtimes that don't grant the MemPalace MCP tools — use the **CLI** (`mempalace wake-up`, `mempalace search`), which this skill's `Bash` allow-tool always covers. If neither is reachable, go to Step 4.
53
+ 4. **Topic.** Read the phase `CONTEXT.md` (the consumed artifact). Derive a short search query from its title, goal, and key decisions.
54
+
55
+ ## Step 3 -- Retrieve (read-only)
56
+
57
+ All calls in this step are side-effect-free. On any error or timeout, stop retrieving and write whatever was gathered (or the stub) -- never raise.
58
+
59
+ 1. **Wake up** (cheap, ~600--900 tokens):
60
+ - Interactive: read the wing identity/summary, then `mempalace_search`.
61
+ - Headless: `mempalace wake-up --wing <wing>`.
62
+ 2. **Targeted search:**
63
+ - Interactive: `mempalace_search(query=<topic>, wing=<wing>)`.
64
+ - Headless: `mempalace search "<topic>" --wing <wing>`.
65
+ 3. **Knowledge-graph facts** (when `config.mempalace.mirror_kg` is true): `mempalace_kg_query` / `mempalace_kg_timeline` for decisions relevant to the topic and their validity windows. Only `augment` is currently wired, so the palace KG *supplements* GSD's native `.planning/graphs/` — do not treat it as the sole source. (`kg_backend`/`replace` are forward-declared and behave as `augment` today.)
66
+ 4. **Dedup** the returned drawers/facts; keep the top results.
67
+
68
+ ## Step 4 -- Write MEMORY-RECALL.md
69
+
70
+ Write `MEMORY-RECALL.md` in the current phase directory. The planner consumes it.
71
+
72
+ When recall succeeded, structure it as:
73
+
74
+ ```markdown
75
+ # Memory Recall (MemPalace)
76
+
77
+ _Wing: <wing> · Mode: <mode> · Transport: <mcp|cli>_
78
+
79
+ ## Prior decisions
80
+ - <decision> — <provenance: drawer id / kg fact, valid_from>
81
+
82
+ ## Patterns
83
+ - <pattern> — <provenance>
84
+
85
+ ## Surprises / gotchas
86
+ - <surprise> — <provenance>
87
+ ```
88
+
89
+ When MemPalace is unreachable, write the stub and continue:
90
+
91
+ ```markdown
92
+ # Memory Recall (MemPalace)
93
+
94
+ _MemPalace unavailable at recall time — proceeding without recalled memory._
95
+ ```
96
+
97
+ ## Anti-Patterns
98
+
99
+ 1. DO NOT let any MemPalace error fail the step -- recall is `onError: skip`.
100
+ 2. DO NOT write to the palace from this skill -- recall is read-only; capture is a separate skill.
101
+ 3. DO NOT paste raw search output into the file -- distil to decisions/patterns/surprises with provenance.
102
+ 4. DO NOT skip the config gate.
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: gsd-context
3
- description: "codebase intelligence | map graphify docs learnings"
3
+ description: "codebase intel | map graphify docs learnings mempalace"
4
4
  argument-hint: ""
5
5
  allowed-tools:
6
6
  - Read
7
7
  - Skill
8
- requires: [map-codebase, graphify, docs-update, extract-learnings]
8
+ requires: [map-codebase, graphify, docs-update, extract-learnings, mempalace-recall, mempalace-capture]
9
9
  ---
10
10
 
11
11
  Route to the appropriate codebase-intelligence skill based on the user's intent.
@@ -19,5 +19,7 @@ Route to the appropriate codebase-intelligence skill based on the user's intent.
19
19
  | Generate a knowledge graph | gsd-graphify |
20
20
  | Update project documentation | gsd-docs-update |
21
21
  | Extract learnings from a completed phase | gsd-extract-learnings |
22
+ | Recall prior decisions and patterns before planning | gsd-mempalace-recall |
23
+ | File a phase artifact into MemPalace | gsd-mempalace-capture |
22
24
 
23
25
  Invoke the matched skill directly using the Skill tool.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: gsd:progress
3
3
  description: Check progress, advance workflow, or dispatch freeform intent — the unified GSD situational command
4
- argument-hint: "[--forensic | --next | --do \"task description\"]"
4
+ argument-hint: "[--forensic | --next [--auto] [--converge] | --do \"task description\"]"
5
5
  effort: low
6
6
  allowed-tools:
7
7
  - Read
@@ -25,6 +25,7 @@ Three modes:
25
25
  <flags>
26
26
  - **--next**: Detect current project state and automatically invoke the next logical GSD workflow step. Scans all prior phases for incomplete work before routing. `--next --force` bypasses safety gates.
27
27
  - **--next --auto**: Like `--next`, but after the determined step completes, automatically re-invokes `/gsd:progress --next --auto` to continue chaining steps until completion or a blocking decision. Enables hands-free plan→execute→verify→complete progression.
28
+ - **--next --converge**: When the next action is planning (Route 3), route it through the plan-review **convergence** loop instead of the standard planner. Requires `workflow.plan_review_convergence=true` (enable with `gsd config-set workflow.plan_review_convergence true`). `--cross-ai` is an alias. Reviewer flags (`--codex`, `--gemini`, `--claude`, `--opencode`, `--ollama`, `--lm-studio`, `--llama-cpp`, `--all`) and `--max-cycles N` are forwarded to the convergence loop.
28
29
  - **--do "..."**: Smart dispatcher — match freeform intent to the best GSD command using routing rules, confirm the match, then hand off.
29
30
  - **--forensic**: Run 6-check integrity audit after the standard progress report.
30
31
  - **(no flag)**: Standard progress check + intelligent routing (Routes A through F).
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gsd-core",
3
- "version": "1.5.0-rc.2",
3
+ "version": "1.5.0-rc.4",
4
4
  "description": "GSD Core — a meta-prompting, context engineering, and spec-driven development system for AI coding agents. Loads gsd's operating context into every Gemini CLI session.",
5
5
  "contextFileName": "GEMINI.md"
6
6
  }
@@ -58,6 +58,18 @@
58
58
  * [--name <name>]
59
59
  * [--archive-phases] Move phase dirs to milestones/vX.Y-phases/
60
60
  *
61
+ * User Story Validation:
62
+ * user-story validate --story "..." Validate "As a / I want to / so that" format
63
+ * Returns JSON { valid, errors[], slots: {role,capability,outcome} | null }
64
+ * --pick valid Emit bare boolean (for workflow boolean checks)
65
+ *
66
+ * Drift Guard (ADR-22):
67
+ * drift-guard authority Resolve effective source-grounding authority
68
+ * (reads plan_review.source_grounding_authority + intel.enabled from config)
69
+ * drift-guard severity --status <S> Classify a symbol verdict into { severity, hardBlock }
70
+ * [--authority <A>] Status: VERIFIED|MISSING|AMBIGUOUS|UNCHECKABLE
71
+ * Authority: grep|intel|treesitter|lsp|scip (default: config-resolved)
72
+ *
61
73
  * Validation:
62
74
  * validate consistency Check phase numbering, disk/roadmap sync
63
75
  * validate health [--repair] Check .planning/ integrity, optionally repair
@@ -201,8 +213,6 @@ const milestone = require('./lib/milestone.cjs');
201
213
  const commands = require('./lib/commands.cjs');
202
214
  const init = require('./lib/init.cjs');
203
215
  const frontmatter = require('./lib/frontmatter.cjs');
204
- const profilePipeline = require('./lib/profile-pipeline.cjs');
205
- const profileOutput = require('./lib/profile-output.cjs');
206
216
  const workstream = require('./lib/workstream.cjs');
207
217
  const docs = require('./lib/docs.cjs');
208
218
  const learnings = require('./lib/learnings.cjs');
@@ -214,6 +224,7 @@ const verification = require('./lib/verification.cjs');
214
224
  const { routeInitCommand } = require('./lib/init-command-router.cjs');
215
225
  const loopResolver = require('./lib/loop-resolver.cjs');
216
226
  const capabilityState = require('./lib/capability-state.cjs');
227
+ const capabilityWriter = require('./lib/capability-writer.cjs');
217
228
  const { routePhaseCommand } = require('./lib/phase-command-router.cjs');
218
229
  const { routePhasesCommand } = require('./lib/phases-command-router.cjs');
219
230
  const { routeValidateCommand } = require('./lib/validate-command-router.cjs');
@@ -222,6 +233,8 @@ const { routeAgentCommand } = require('./lib/agent-command-router.cjs');
222
233
  const { routeCheckCommand } = require('./lib/check-command-router.cjs');
223
234
  const { routeTaskCommand } = require('./lib/task-command-router.cjs');
224
235
  const { parseNamedArgs, parseMultiwordArg } = require('./lib/command-arg-projection.cjs');
236
+ const { cmdGitBaseBranch } = require('./lib/git-base-branch.cjs');
237
+ const { getEffectiveAuthority, classifyDriftSeverity } = require('./lib/plan-drift-guard.cjs');
225
238
 
226
239
  // ─── Bridge collapsed (Phase 4) ────────────────────────────────────────────────
227
240
  // Non-family commands now run through their CJS handlers directly. Keep the
@@ -505,12 +518,12 @@ async function main() {
505
518
  const TOP_LEVEL_USAGE = 'Usage: gsd-tools <command> [args] [--raw] [--pick <field>] [--cwd <path>] [--ws <name>] [--json-errors]\n' +
506
519
  'Commands: agent, agent-skills, audit-open, audit-uat, check, check-commit, commit, commit-to-subrepo, ' +
507
520
  'config-ensure-section, config-get, config-new-project, config-path, config-set, migrate-config, ' +
508
- 'current-timestamp, detect-custom-files, docs-init, effort, extract-messages, find-phase, ' +
521
+ 'current-timestamp, detect-custom-files, docs-init, drift-guard, effort, extract-messages, find-phase, ' +
509
522
  'from-gsd2, frontmatter, gap-analysis, generate-claude-md, generate-claude-profile, ' +
510
523
  'generate-dev-preferences, generate-slug, graphify, history-digest, init, intel, ' +
511
- 'capability, classify-confidence, learnings, list-todos, loop, milestone, package-legitimacy, phase, phase-plan-index, phases, profile-questionnaire, ' +
524
+ 'capability, classify-confidence, git, learnings, list-todos, loop, milestone, package-legitimacy, phase, phase-plan-index, phases, profile-questionnaire, ' +
512
525
  'profile-sample, progress, prompt-budget, requirements, research-plan, research-store, resolve-granularity, resolve-model, roadmap, scaffold, state, ' +
513
- 'task, template, validate, verify, verify-path-exists, verify-summary, workstream, worktree\n\n' +
526
+ 'task, template, user-story, validate, verify, verify-path-exists, verify-summary, workstream, worktree\n\n' +
514
527
  'Global flags:\n' +
515
528
  ' --raw Emit raw output without post-processing\n' +
516
529
  ' --pick <field> Extract a single field from JSON output (dot/bracket notation)\n' +
@@ -559,6 +572,7 @@ async function main() {
559
572
  'verify-summary', 'template', 'frontmatter', 'detect-custom-files',
560
573
  'worktree', 'prompt-budget',
561
574
  'research-store', 'research-plan', 'package-legitimacy', 'classify-confidence',
575
+ 'user-story', // pure string validation — no .planning/ access needed
562
576
  ]);
563
577
  if (!SKIP_ROOT_RESOLUTION.has(command)) {
564
578
  cwd = findProjectRoot(cwd);
@@ -1236,7 +1250,39 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
1236
1250
  // loop render-hooks <point>
1237
1251
  const loopSubcommand = args[1];
1238
1252
  if (loopSubcommand === 'render-hooks') {
1239
- loopResolver.cmdLoopRenderHooks(cwd, args[2], raw, {});
1253
+ let loopConfigDir = null;
1254
+ const configDirEqArg = args.find(arg => arg.startsWith('--config-dir='));
1255
+ const configDirIdx = args.indexOf('--config-dir');
1256
+ if (configDirEqArg) {
1257
+ const value = configDirEqArg.slice('--config-dir='.length).trim();
1258
+ if (!value) error('Missing value for --config-dir', core.ERROR_REASON ? core.ERROR_REASON.USAGE : undefined);
1259
+ loopConfigDir = value;
1260
+ } else if (configDirIdx !== -1) {
1261
+ const value = args[configDirIdx + 1];
1262
+ if (!value || value.startsWith('--')) {
1263
+ error('Missing value for --config-dir', core.ERROR_REASON ? core.ERROR_REASON.USAGE : undefined);
1264
+ }
1265
+ loopConfigDir = value;
1266
+ }
1267
+ // --active-cap <capId>: parse and validate before delegating
1268
+ let loopActiveCap = undefined;
1269
+ const activeCapEqArg = args.find(arg => arg.startsWith('--active-cap='));
1270
+ const activeCapIdx = args.indexOf('--active-cap');
1271
+ if (activeCapEqArg) {
1272
+ const value = activeCapEqArg.slice('--active-cap='.length).trim();
1273
+ if (!value) error('Missing value for --active-cap (e.g. --active-cap tdd)', core.ERROR_REASON ? core.ERROR_REASON.USAGE : undefined);
1274
+ loopActiveCap = value;
1275
+ } else if (activeCapIdx !== -1) {
1276
+ const value = args[activeCapIdx + 1];
1277
+ if (!value || value.startsWith('--')) {
1278
+ error('Missing value for --active-cap (e.g. --active-cap tdd)', core.ERROR_REASON ? core.ERROR_REASON.USAGE : undefined);
1279
+ }
1280
+ loopActiveCap = value;
1281
+ }
1282
+ loopResolver.cmdLoopRenderHooks(cwd, args[2], raw, {
1283
+ configDir: loopConfigDir ? path.resolve(loopConfigDir) : undefined,
1284
+ activeCap: loopActiveCap,
1285
+ });
1240
1286
  } else {
1241
1287
  error(
1242
1288
  `Unknown loop subcommand: ${loopSubcommand}. Available: render-hooks`,
@@ -1267,9 +1313,85 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
1267
1313
  }
1268
1314
  const resolvedConfigDir = configDir ? path.resolve(configDir) : null;
1269
1315
  capabilityState.cmdCapabilityState(cwd, resolvedConfigDir, raw, {});
1316
+ } else if (capSubcommand === 'set') {
1317
+ // capability set <id> [--on|--off|--enable|--disable] [--gate <key>=<bool>]... [--config-dir <dir>] [--runtime <r>] [--scope <s>]
1318
+ const capId = args[2];
1319
+ if (!capId || capId.startsWith('--')) {
1320
+ error('Missing capability id for: capability set <id>', core.ERROR_REASON ? core.ERROR_REASON.USAGE : undefined);
1321
+ }
1322
+ // Parse --config-dir
1323
+ const setConfigDirIdx = args.indexOf('--config-dir');
1324
+ let setConfigDir = null;
1325
+ if (setConfigDirIdx !== -1) {
1326
+ const setConfigDirVal = args[setConfigDirIdx + 1];
1327
+ if (!setConfigDirVal || setConfigDirVal.startsWith('--')) {
1328
+ error('Missing value for --config-dir', core.ERROR_REASON ? core.ERROR_REASON.USAGE : undefined);
1329
+ }
1330
+ setConfigDir = setConfigDirVal;
1331
+ }
1332
+ const resolvedSetConfigDir = setConfigDir ? path.resolve(setConfigDir) : null;
1333
+ // Parse --on/--enable and --off/--disable (mutually exclusive)
1334
+ const hasOn = args.includes('--on') || args.includes('--enable');
1335
+ const hasOff = args.includes('--off') || args.includes('--disable');
1336
+ if (hasOn && hasOff) {
1337
+ error('Conflicting flags: --on/--enable and --off/--disable cannot both be present', core.ERROR_REASON ? core.ERROR_REASON.USAGE : undefined);
1338
+ }
1339
+ let setEnabled;
1340
+ if (hasOn) {
1341
+ setEnabled = true;
1342
+ } else if (hasOff) {
1343
+ setEnabled = false;
1344
+ }
1345
+ // Parse --gate <key>=<bool> (repeatable)
1346
+ const setGates = {};
1347
+ for (let gi = 0; gi < args.length; gi++) {
1348
+ if (args[gi] === '--gate') {
1349
+ const gateVal = args[gi + 1];
1350
+ if (!gateVal || gateVal.startsWith('--')) {
1351
+ error('Missing value for --gate (expected <key>=<true|false>)', core.ERROR_REASON ? core.ERROR_REASON.USAGE : undefined);
1352
+ }
1353
+ const eqIdx = gateVal.indexOf('=');
1354
+ if (eqIdx === -1) {
1355
+ error(`Malformed --gate value "${gateVal}": expected <key>=<true|false>`, core.ERROR_REASON ? core.ERROR_REASON.USAGE : undefined);
1356
+ }
1357
+ const gateKey = gateVal.slice(0, eqIdx);
1358
+ const gateBoolStr = gateVal.slice(eqIdx + 1);
1359
+ if (gateBoolStr !== 'true' && gateBoolStr !== 'false') {
1360
+ error(`Malformed --gate value "${gateVal}": bool must be true or false`, core.ERROR_REASON ? core.ERROR_REASON.USAGE : undefined);
1361
+ }
1362
+ setGates[gateKey] = gateBoolStr === 'true';
1363
+ gi++; // skip consumed value
1364
+ }
1365
+ }
1366
+ // Parse --runtime and --scope (validate that values are present and not flags)
1367
+ const runtimeIdx = args.indexOf('--runtime');
1368
+ let setRuntime;
1369
+ if (runtimeIdx !== -1) {
1370
+ const runtimeVal = args[runtimeIdx + 1];
1371
+ if (!runtimeVal || runtimeVal.startsWith('--')) {
1372
+ error('Missing value for --runtime', core.ERROR_REASON ? core.ERROR_REASON.USAGE : undefined);
1373
+ }
1374
+ setRuntime = runtimeVal;
1375
+ }
1376
+ const scopeIdx = args.indexOf('--scope');
1377
+ let setScope;
1378
+ if (scopeIdx !== -1) {
1379
+ const scopeVal = args[scopeIdx + 1];
1380
+ if (!scopeVal || scopeVal.startsWith('--')) {
1381
+ error('Missing value for --scope', core.ERROR_REASON ? core.ERROR_REASON.USAGE : undefined);
1382
+ }
1383
+ setScope = scopeVal;
1384
+ }
1385
+ capabilityWriter.cmdCapabilitySet(
1386
+ cwd,
1387
+ resolvedSetConfigDir,
1388
+ capId,
1389
+ { enabled: setEnabled, gates: Object.keys(setGates).length > 0 ? setGates : undefined, runtime: setRuntime, scope: setScope },
1390
+ raw,
1391
+ );
1270
1392
  } else {
1271
1393
  error(
1272
- `Unknown capability subcommand: ${capSubcommand}. Available: state`,
1394
+ `Unknown capability subcommand: ${capSubcommand}. Available: state, set`,
1273
1395
  core.ERROR_REASON ? core.ERROR_REASON.SDK_UNKNOWN_COMMAND : undefined,
1274
1396
  );
1275
1397
  }
@@ -1305,94 +1427,6 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
1305
1427
  break;
1306
1428
  }
1307
1429
 
1308
- // ─── Profiling Pipeline ────────────────────────────────────────────────
1309
-
1310
- case 'scan-sessions': {
1311
- const pathIdx = args.indexOf('--path');
1312
- const sessionsPath = pathIdx !== -1 ? args[pathIdx + 1] : null;
1313
- const verboseFlag = args.includes('--verbose');
1314
- const jsonFlag = args.includes('--json');
1315
- await profilePipeline.cmdScanSessions(sessionsPath, { verbose: verboseFlag, json: jsonFlag }, raw);
1316
- break;
1317
- }
1318
-
1319
- case 'extract-messages': {
1320
- const sessionIdx = args.indexOf('--session');
1321
- const sessionId = sessionIdx !== -1 ? args[sessionIdx + 1] : null;
1322
- const limitIdx = args.indexOf('--limit');
1323
- const limit = limitIdx !== -1 ? parseInt(args[limitIdx + 1], 10) : null;
1324
- const pathIdx = args.indexOf('--path');
1325
- const sessionsPath = pathIdx !== -1 ? args[pathIdx + 1] : null;
1326
- const projectArg = args[1];
1327
- if (!projectArg || projectArg.startsWith('--')) {
1328
- error('Usage: gsd-tools extract-messages <project> [--session <id>] [--limit N] [--path <dir>]\nRun scan-sessions first to see available projects.', ERROR_REASON.USAGE);
1329
- }
1330
- await profilePipeline.cmdExtractMessages(projectArg, { sessionId, limit }, raw, sessionsPath);
1331
- break;
1332
- }
1333
-
1334
- case 'profile-sample': {
1335
- const pathIdx = args.indexOf('--path');
1336
- const sessionsPath = pathIdx !== -1 ? args[pathIdx + 1] : null;
1337
- const limitIdx = args.indexOf('--limit');
1338
- const limit = limitIdx !== -1 ? parseInt(args[limitIdx + 1], 10) : 150;
1339
- const maxPerIdx = args.indexOf('--max-per-project');
1340
- const maxPerProject = maxPerIdx !== -1 ? parseInt(args[maxPerIdx + 1], 10) : null;
1341
- const maxCharsIdx = args.indexOf('--max-chars');
1342
- const maxChars = maxCharsIdx !== -1 ? parseInt(args[maxCharsIdx + 1], 10) : 500;
1343
- await profilePipeline.cmdProfileSample(sessionsPath, { limit, maxPerProject, maxChars }, raw);
1344
- break;
1345
- }
1346
-
1347
- // ─── Profile Output ──────────────────────────────────────────────────
1348
-
1349
- case 'write-profile': {
1350
- const inputIdx = args.indexOf('--input');
1351
- const inputPath = inputIdx !== -1 ? args[inputIdx + 1] : null;
1352
- if (!inputPath) error('--input <analysis-json-path> is required', ERROR_REASON.USAGE);
1353
- const outputIdx = args.indexOf('--output');
1354
- const outputPath = outputIdx !== -1 ? args[outputIdx + 1] : null;
1355
- profileOutput.cmdWriteProfile(cwd, { input: inputPath, output: outputPath }, raw);
1356
- break;
1357
- }
1358
-
1359
- case 'profile-questionnaire': {
1360
- const answersIdx = args.indexOf('--answers');
1361
- const answers = answersIdx !== -1 ? args[answersIdx + 1] : null;
1362
- profileOutput.cmdProfileQuestionnaire({ answers }, raw);
1363
- break;
1364
- }
1365
-
1366
- case 'generate-dev-preferences': {
1367
- const analysisIdx = args.indexOf('--analysis');
1368
- const analysisPath = analysisIdx !== -1 ? args[analysisIdx + 1] : null;
1369
- const outputIdx = args.indexOf('--output');
1370
- const outputPath = outputIdx !== -1 ? args[outputIdx + 1] : null;
1371
- const stackIdx = args.indexOf('--stack');
1372
- const stack = stackIdx !== -1 ? args[stackIdx + 1] : null;
1373
- profileOutput.cmdGenerateDevPreferences(cwd, { analysis: analysisPath, output: outputPath, stack }, raw);
1374
- break;
1375
- }
1376
-
1377
- case 'generate-claude-profile': {
1378
- const analysisIdx = args.indexOf('--analysis');
1379
- const analysisPath = analysisIdx !== -1 ? args[analysisIdx + 1] : null;
1380
- const outputIdx = args.indexOf('--output');
1381
- const outputPath = outputIdx !== -1 ? args[outputIdx + 1] : null;
1382
- const globalFlag = args.includes('--global');
1383
- profileOutput.cmdGenerateClaudeProfile(cwd, { analysis: analysisPath, output: outputPath, global: globalFlag }, raw);
1384
- break;
1385
- }
1386
-
1387
- case 'generate-claude-md': {
1388
- const outputIdx = args.indexOf('--output');
1389
- const outputPath = outputIdx !== -1 ? args[outputIdx + 1] : null;
1390
- const autoFlag = args.includes('--auto');
1391
- const forceFlag = args.includes('--force');
1392
- profileOutput.cmdGenerateClaudeMd(cwd, { output: outputPath, auto: autoFlag, force: forceFlag }, raw);
1393
- break;
1394
- }
1395
-
1396
1430
  case 'workstream': {
1397
1431
  const subcommand = args[1];
1398
1432
  if (subcommand === 'create') {
@@ -1984,6 +2018,154 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
1984
2018
  break;
1985
2019
  }
1986
2020
 
2021
+ // ─── User Story Validation (bug #1145) ────────────────────────────────────
2022
+ //
2023
+ // Invocation shapes (from mvp-phase.md and verify-work.md):
2024
+ // gsd_run query user-story.validate --story "$USER_STORY"
2025
+ // gsd_run query user-story.validate --story "$PHASE_GOAL" --pick valid
2026
+ //
2027
+ // Returns JSON: { valid: boolean, errors: string[], slots: { role, capability, outcome } | null }
2028
+ // - valid: true only when the story fully matches the canonical format
2029
+ // - errors: per-slot diagnostic strings (empty on success)
2030
+ // - slots: extracted role/capability/outcome on success; null on failure
2031
+ //
2032
+ // Canonical format (user-story-template.md):
2033
+ // "As a [user role], I want to [capability], so that [outcome]."
2034
+ // Each slot must be non-empty and contain non-whitespace content.
2035
+ //
2036
+ // No .planning/ access needed — pure string validation.
2037
+
2038
+ // #1146: single base-branch resolver for all forking workflows.
2039
+ // Workflows call `gsd_run query git.base-branch` (dotted form normalised to
2040
+ // command='git', args=['git','base-branch']).
2041
+ case 'git': {
2042
+ const subcommand = args[1];
2043
+ if (subcommand !== 'base-branch') {
2044
+ error(
2045
+ `Unknown git subcommand: ${subcommand || '(none)'}. Available: base-branch`,
2046
+ ERROR_REASON.SDK_UNKNOWN_COMMAND,
2047
+ );
2048
+ break;
2049
+ }
2050
+ cmdGitBaseBranch(cwd, args.slice(2));
2051
+ break;
2052
+ }
2053
+
2054
+ case 'user-story': {
2055
+ const subcommand = args[1];
2056
+ if (subcommand !== 'validate') {
2057
+ error(`Unknown user-story subcommand: ${subcommand || '(none)'}. Available: validate`, ERROR_REASON.SDK_UNKNOWN_COMMAND);
2058
+ break;
2059
+ }
2060
+
2061
+ const storyIdx = args.indexOf('--story');
2062
+ const story = (storyIdx !== -1 && args[storyIdx + 1] && !args[storyIdx + 1].startsWith('--'))
2063
+ ? args[storyIdx + 1]
2064
+ : '';
2065
+
2066
+ // Canonical extraction regex — requires non-whitespace content in each slot
2067
+ // (\S.*? ensures the slot isn't whitespace-only).
2068
+ // Named groups: role / capability / outcome.
2069
+ const USER_STORY_RE = /^As a (\S.*?), I want to (\S.*?), so that (\S.*?)\.$/;
2070
+
2071
+ const errors = [];
2072
+ const trimmed = story.trim();
2073
+ let slots = null;
2074
+
2075
+ if (!trimmed) {
2076
+ errors.push('Story is empty. Required format: "As a [role], I want to [capability], so that [outcome]."');
2077
+ } else {
2078
+ // Per-clause guards produce targeted, actionable error messages before
2079
+ // attempting the full regex. Guards are ordered: role → capability → outcome → period.
2080
+ if (!/^As a \S/i.test(trimmed)) {
2081
+ errors.push('Story must start with "As a [user role]," (role must be non-empty).');
2082
+ }
2083
+ if (!/, I want to \S/i.test(trimmed)) {
2084
+ errors.push('Story must include ", I want to [capability]," (capability must be non-empty).');
2085
+ }
2086
+ if (!/, so that \S/i.test(trimmed)) {
2087
+ errors.push('Story must include ", so that [outcome]." (outcome must be non-empty).');
2088
+ }
2089
+ if (!trimmed.endsWith('.')) {
2090
+ errors.push('Story must end with a period (.).');
2091
+ }
2092
+ // Full-regex check only when per-clause guards all passed — avoids
2093
+ // redundant "format mismatch" noise on top of specific error messages.
2094
+ if (errors.length === 0) {
2095
+ const m = USER_STORY_RE.exec(trimmed);
2096
+ if (!m) {
2097
+ errors.push('Story does not match the canonical format: "As a [role], I want to [capability], so that [outcome]."');
2098
+ } else {
2099
+ slots = { role: m[1], capability: m[2], outcome: m[3] };
2100
+ }
2101
+ }
2102
+ }
2103
+
2104
+ core.output({ valid: errors.length === 0, errors, slots }, raw);
2105
+ break;
2106
+ }
2107
+
2108
+ case 'drift-guard': {
2109
+ // ADR-22: deterministic authority resolution + severity classification.
2110
+ // Subcommands:
2111
+ // drift-guard authority → effective authority string
2112
+ // drift-guard severity --status <S> [--authority <A>] → {severity, hardBlock}
2113
+ const subcommand = args[1];
2114
+
2115
+ // Read config.json directly for both plan_review.source_grounding_authority
2116
+ // and intel.enabled. Neither key is in the config-loader.cjs whitelist that
2117
+ // core.loadConfig() returns; plan_review is only in config.cjs's private
2118
+ // buildConfig(), and intel is a federated capability config key.
2119
+ let configuredAuthority = 'grep';
2120
+ let intelEnabled = false;
2121
+ try {
2122
+ const { planningDir } = require('./lib/planning-workspace.cjs');
2123
+ const cfgPath = require('path').join(planningDir(cwd), 'config.json');
2124
+ if (require('fs').existsSync(cfgPath)) {
2125
+ const rawCfg = JSON.parse(require('fs').readFileSync(cfgPath, 'utf-8'));
2126
+ if (rawCfg && rawCfg.plan_review && rawCfg.plan_review.source_grounding_authority) {
2127
+ configuredAuthority = String(rawCfg.plan_review.source_grounding_authority);
2128
+ }
2129
+ if (rawCfg && rawCfg.intel && rawCfg.intel.enabled === true) {
2130
+ intelEnabled = true;
2131
+ }
2132
+ }
2133
+ } catch {
2134
+ // not fatal — defaults apply
2135
+ }
2136
+
2137
+ const effectiveAuthority = getEffectiveAuthority(configuredAuthority, intelEnabled);
2138
+
2139
+ if (subcommand === 'authority') {
2140
+ // Pass rawValue as 3rd arg so --raw returns unquoted string (not JSON)
2141
+ core.output(effectiveAuthority, raw, effectiveAuthority);
2142
+ break;
2143
+ }
2144
+
2145
+ if (subcommand === 'severity') {
2146
+ const statusIdx = args.indexOf('--status');
2147
+ const statusVal = statusIdx !== -1 ? args[statusIdx + 1] : undefined;
2148
+ if (!statusVal || statusVal.startsWith('--')) {
2149
+ error('drift-guard severity requires --status <VERIFIED|MISSING|AMBIGUOUS|UNCHECKABLE>', ERROR_REASON.SDK_UNKNOWN_COMMAND);
2150
+ break;
2151
+ }
2152
+ const authIdx = args.indexOf('--authority');
2153
+ const authVal = authIdx !== -1 ? args[authIdx + 1] : undefined;
2154
+ const authorityForClassify = (authVal && !authVal.startsWith('--'))
2155
+ ? authVal
2156
+ : effectiveAuthority;
2157
+ const result = classifyDriftSeverity({ status: statusVal, authority: authorityForClassify });
2158
+ core.output(result, raw);
2159
+ break;
2160
+ }
2161
+
2162
+ error(
2163
+ `Unknown drift-guard subcommand: ${subcommand || '(none)'}. Available: authority, severity`,
2164
+ ERROR_REASON.SDK_UNKNOWN_COMMAND,
2165
+ );
2166
+ break;
2167
+ }
2168
+
1987
2169
  default: {
1988
2170
  // ADR-959: try capability-registry dispatch before emitting the unknown-command error.
1989
2171
  // An unmigrated command still hits its hardcoded `case` above — untouched.