@opengsd/gsd-core 1.20.0 → 1.21.0

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 (154) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.opencode/plugins/gsd-core.js +33 -2
  4. package/agents/gsd-executor.md +1 -1
  5. package/agents/gsd-plan-checker.md +1 -1
  6. package/agents/gsd-planner.md +3 -3
  7. package/agents/gsd-verifier.md +2 -2
  8. package/bin/install.js +265 -161
  9. package/commands/gsd/autonomous.md +1 -1
  10. package/commands/gsd/new-milestone.md +2 -2
  11. package/commands/gsd/new-project.md +2 -2
  12. package/gsd-core/bin/gsd-tools.cjs +554 -823
  13. package/gsd-core/bin/lib/capability-activation.cjs +8 -2
  14. package/gsd-core/bin/lib/capability-command-router.cjs +6 -2
  15. package/gsd-core/bin/lib/capability-ledger.cjs +1 -1
  16. package/gsd-core/bin/lib/capability-lock.cjs +19 -27
  17. package/gsd-core/bin/lib/capability-registry.cjs +74 -71
  18. package/gsd-core/bin/lib/capability-validator.cjs +1 -0
  19. package/gsd-core/bin/lib/capability-writer.cjs +7 -4
  20. package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +142 -62
  21. package/gsd-core/bin/lib/claude-orchestration.cjs +214 -122
  22. package/gsd-core/bin/lib/cli-surface.cjs +545 -0
  23. package/gsd-core/bin/lib/command-arg-projection.cjs +356 -103
  24. package/gsd-core/bin/lib/commands.cjs +99 -2
  25. package/gsd-core/bin/lib/config-file-read.cjs +85 -0
  26. package/gsd-core/bin/lib/config-loader.cjs +23 -15
  27. package/gsd-core/bin/lib/config.cjs +139 -101
  28. package/gsd-core/bin/lib/configuration.cjs +41 -2
  29. package/gsd-core/bin/lib/criterion-baseline.cjs +346 -0
  30. package/gsd-core/bin/lib/criterion-replay.cjs +380 -0
  31. package/gsd-core/bin/lib/estimate-cli.cjs +5 -1
  32. package/gsd-core/bin/lib/gap-checker.cjs +12 -1
  33. package/gsd-core/bin/lib/gate-evaluation-scope.cjs +593 -27
  34. package/gsd-core/bin/lib/gate-phase-context.cjs +16 -4
  35. package/gsd-core/bin/lib/graphify.cjs +19 -2
  36. package/gsd-core/bin/lib/gsd-tool-dir.cjs +134 -0
  37. package/gsd-core/bin/lib/health-diagnostic.cjs +5 -1
  38. package/gsd-core/bin/lib/init.cjs +20 -18
  39. package/gsd-core/bin/lib/install-effort-resolver.cjs +24 -4
  40. package/gsd-core/bin/lib/install-engine.cjs +20 -28
  41. package/gsd-core/bin/lib/install-fs-adapter.cjs +5 -1
  42. package/gsd-core/bin/lib/install-manifest-record.cjs +86 -0
  43. package/gsd-core/bin/lib/install-model-override-resolver.cjs +33 -3
  44. package/gsd-core/bin/lib/install-profiles.cjs +4 -2
  45. package/gsd-core/bin/lib/io.cjs +6 -0
  46. package/gsd-core/bin/lib/markdown-sectionizer.cjs +62 -31
  47. package/gsd-core/bin/lib/markdown-table.cjs +39 -1
  48. package/gsd-core/bin/lib/milestone.cjs +8 -6
  49. package/gsd-core/bin/lib/model-resolver.cjs +8 -4
  50. package/gsd-core/bin/lib/phase-entry-grammar.cjs +14 -0
  51. package/gsd-core/bin/lib/phase-id.cjs +26 -9
  52. package/gsd-core/bin/lib/phase-locator.cjs +8 -3
  53. package/gsd-core/bin/lib/phase.cjs +170 -45
  54. package/gsd-core/bin/lib/plan-dependency-graph.cjs +46 -23
  55. package/gsd-core/bin/lib/planning-snapshot.cjs +10 -6
  56. package/gsd-core/bin/lib/planning-workspace.cjs +20 -10
  57. package/gsd-core/bin/lib/profile-output.cjs +9 -6
  58. package/gsd-core/bin/lib/project-context.cjs +439 -0
  59. package/gsd-core/bin/lib/project-root.cjs +9 -230
  60. package/gsd-core/bin/lib/quick-batch-dispatch.cjs +1 -1
  61. package/gsd-core/bin/lib/report-parser.cjs +16 -0
  62. package/gsd-core/bin/lib/roadmap-parser.cjs +357 -50
  63. package/gsd-core/bin/lib/roadmap-upgrade.cjs +24 -5
  64. package/gsd-core/bin/lib/roadmap.cjs +2 -2
  65. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +317 -134
  66. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +81 -3
  67. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +6 -0
  68. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +279 -8
  69. package/gsd-core/bin/lib/runtime-slash.cjs +7 -2
  70. package/gsd-core/bin/lib/shell-command-projection.cjs +1010 -10
  71. package/gsd-core/bin/lib/stale-bake-guard.cjs +4 -1
  72. package/gsd-core/bin/lib/state-transition.cjs +103 -46
  73. package/gsd-core/bin/lib/state.cjs +448 -63
  74. package/gsd-core/bin/lib/surface.cjs +4 -7
  75. package/gsd-core/bin/lib/tdd-red-evidence.cjs +10 -3
  76. package/gsd-core/bin/lib/verify-command-router.cjs +6 -1
  77. package/gsd-core/bin/lib/verify.cjs +49 -6
  78. package/gsd-core/bin/lib/workstream-inventory.cjs +4 -4
  79. package/gsd-core/bin/lib/worktree-base-ref.cjs +32 -2
  80. package/gsd-core/bin/lib/worktree-safety.cjs +8 -111
  81. package/gsd-core/bin/shared/config-schema.manifest.json +1 -0
  82. package/gsd-core/references/autonomous-smart-discuss.md +99 -18
  83. package/gsd-core/references/checkpoints.md +1 -1
  84. package/gsd-core/references/planner-guidance.md +11 -0
  85. package/gsd-core/references/planner-human-verify-mode.md +1 -1
  86. package/gsd-core/references/planner-reversibility.md +2 -0
  87. package/gsd-core/references/planner-verify-command-grounding.md +22 -6
  88. package/gsd-core/workflows/autonomous.md +4 -4
  89. package/gsd-core/workflows/code-review-fix.md +5 -7
  90. package/gsd-core/workflows/execute-phase.md +21 -7
  91. package/gsd-core/workflows/plan-phase/steps/plan-pre-dispatch.md +101 -0
  92. package/gsd-core/workflows/plan-phase.md +18 -19
  93. package/hooks/dist/gsd-agent-isolation-guard.js +4 -3
  94. package/hooks/dist/gsd-config-reload.js +2 -2
  95. package/hooks/dist/gsd-context-monitor.js +2 -1
  96. package/hooks/dist/gsd-cursor-subagent-start.js +9 -6
  97. package/hooks/dist/gsd-read-injection-scanner.js +2 -2
  98. package/hooks/dist/gsd-secret-read-guard.js +141 -6
  99. package/hooks/dist/gsd-statusline.js +3 -2
  100. package/hooks/dist/gsd-workflow-guard.js +2 -1
  101. package/hooks/dist/lib/config-read.js +49 -0
  102. package/hooks/dist/lib/git-cmd.js +3 -1
  103. package/hooks/dist/lib/git-head-tree.js +474 -0
  104. package/hooks/dist/lib/isolation-sentinel.js +168 -13
  105. package/hooks/dist/lib/secret-read-exemptions.js +161 -0
  106. package/hooks/gsd-agent-isolation-guard.js +4 -3
  107. package/hooks/gsd-config-reload.js +2 -2
  108. package/hooks/gsd-context-monitor.js +2 -1
  109. package/hooks/gsd-cursor-subagent-start.js +9 -6
  110. package/hooks/gsd-read-injection-scanner.js +2 -2
  111. package/hooks/gsd-secret-read-guard.js +141 -6
  112. package/hooks/gsd-statusline.js +3 -2
  113. package/hooks/gsd-workflow-guard.js +2 -1
  114. package/hooks/lib/config-read.js +49 -0
  115. package/hooks/lib/git-cmd.js +3 -1
  116. package/hooks/lib/git-head-tree.js +474 -0
  117. package/hooks/lib/isolation-sentinel.js +168 -13
  118. package/hooks/lib/secret-read-exemptions.js +161 -0
  119. package/package.json +7 -2
  120. package/pi/package-agents/gsd-executor.md +1 -1
  121. package/pi/package-agents/gsd-plan-checker.md +1 -1
  122. package/pi/package-agents/gsd-planner.md +3 -3
  123. package/pi/package-agents/gsd-verifier.md +2 -2
  124. package/pi/package-agents/manifest.json +4 -4
  125. package/pi/package-skills/gsd-autonomous/SKILL.md +1 -1
  126. package/pi/package-skills/gsd-new-milestone/SKILL.md +1 -1
  127. package/pi/package-skills/gsd-new-project/SKILL.md +1 -1
  128. package/scripts/changeset/cli.cjs +2 -2
  129. package/scripts/changeset/lint.cjs +28 -3
  130. package/scripts/changeset/serialize.cjs +12 -2
  131. package/scripts/ci-compact-v8-coverage.cjs +204 -0
  132. package/scripts/ci-fix-next-rerun.cjs +149 -0
  133. package/scripts/ci-next-health.cjs +52 -9
  134. package/scripts/ci-test-scope.cjs +77 -71
  135. package/scripts/command-contract-helpers.cjs +56 -7
  136. package/scripts/docs-guard-registry.cjs +3 -0
  137. package/scripts/gen-cli-surface-manifest.cjs +287 -0
  138. package/scripts/gen-platform-conformance-tier.cjs +258 -19
  139. package/scripts/lib/macos-conformance-tier.generated.cjs +59 -47
  140. package/scripts/lib/platform-conformance-tier.generated.cjs +23 -64
  141. package/scripts/lib/shellcheck-fetch.cjs +71 -4
  142. package/scripts/lint-removed-but-needed.cjs +562 -50
  143. package/scripts/merge-commit-pr.cjs +186 -0
  144. package/scripts/release-ancestry.cjs +86 -0
  145. package/scripts/release-notes/changelog-release-notes.cjs +345 -0
  146. package/scripts/release-notes/discord-release-summary.cjs +82 -1
  147. package/scripts/release-pipeline-pr.cjs +92 -0
  148. package/scripts/require-issue-link-policy.cjs +18 -9
  149. package/scripts/sync-next-version.cjs +11 -2
  150. package/scripts/timeout-evidence-from-ci.cjs +964 -0
  151. package/skills/gsd-autonomous/SKILL.md +1 -1
  152. package/skills/gsd-new-milestone/SKILL.md +1 -1
  153. package/skills/gsd-new-project/SKILL.md +1 -1
  154. package/vscode/package.json +1 -1
@@ -19,19 +19,35 @@ guessing one.
19
19
  **Run a count/match gate before it ships (#4927).** A gate you author — or one a `RESEARCH.md`
20
20
  hands you — that counts or matches text (`grep -c`, `grep -q`, `rg`, `test -n "$(grep …)"`) is a
21
21
  claim about the tree, and two defects are invisible to reading and take one command to expose. You
22
- have a shell when you author the plan, so run each such gate against the **current tree** and write
23
- the pre-change result next to it as its baseline (`# baseline at HEAD: 0`):
22
+ have a shell when you author the plan, so run each such gate against the **current tree** and record
23
+ what it printed, and what it must print after the change, as a **baseline notation** next to the
24
+ gate (#4309):
25
+
26
+ ```
27
+ [HEAD=<value> EXPECT<op><value> [@<sha>] [GUARD MUTATION: <ref>] [UNPINNED]]
28
+ value ∈ <integer> | absent | present | error:<exit-code> op ∈ == != >= > <= <
29
+ ```
30
+
31
+ In an `<acceptance_criteria>` line write it inline: ``- `grep -c "## Quickstart" README.md` -ge 1 [HEAD=0 EXPECT>=1 @<sha>]``.
32
+ In an `<automated>` command put it in a trailing shell comment: `grep -c "## Quickstart" README.md # [HEAD=0 EXPECT>=1]`.
33
+ `HEAD` is the value the command printed, whichever counting flag produced it. A missing file is
34
+ `error:2` (what `grep` exits with), never `0`. `@<sha>` is the commit you measured at; write
35
+ `UNPINNED` instead when you did not record one. `verify plan-structure` reads the notation and
36
+ rejects one whose comparison already holds (B1). Do not write the old free-form
37
+ `# baseline at HEAD: N` comment: nothing can check it, and `verify plan-structure` warns on it (B2).
24
38
 
25
39
  - **It already passes at HEAD.** `grep -c "Agent" .planning/PROJECT.md` returned `6` before any work
26
- existed, because the word sat in an unrelated row — the gate measures nothing and passes whether
27
- or not the work happens. Fix the gate: anchor it to the new heading, key, or line the task
28
- creates, or assert a delta from the recorded baseline.
40
+ existed, because the word sat in an unrelated row. `[HEAD=6 EXPECT>=1]` is already true, so the
41
+ gate measures nothing and passes whether or not the work happens. Fix the gate: anchor it to the
42
+ new heading, key, or line the task creates, or tighten `EXPECT` to the delta (`[HEAD=6 EXPECT>=7]`).
43
+ Only a gate that is meant to hold before and after (a guard against regression) may stay
44
+ already-true, and then it must name the mutation that turns it red: `GUARD MUTATION: <ref>`.
29
45
  - **Its literal can never match the deliverable.** `grep` is case-sensitive, so `grep -c "quickstart"`
30
46
  fails on a correct `## Quickstart`. Match the real casing of the text the task writes, or use
31
47
  `-i` when casing is not part of the contract.
32
48
 
33
49
  A gate that is meant to prove a *negative* is checked the other way: it must pass after the work
34
- and its baseline must show the thing was there to remove. Only read-only commands are safe to run
50
+ and its baseline must show the thing was there to remove (`[HEAD=3 EXPECT==0]`). Only read-only commands are safe to run
35
51
  this way — run `grep`/`rg`/`test` gates; do not execute a gate that builds, installs, or writes.
36
52
 
37
53
  **Root-relative, never absolute (#4767).** The paths in your prompt's `<required_reading>` are
@@ -2,7 +2,7 @@
2
2
 
3
3
  <purpose>
4
4
 
5
- Drive milestone phases autonomously — all remaining phases, a range via `--from N`/`--to N`, or a single phase via `--only N`. For each incomplete phase: discuss → plan → execute using Skill() flat invocations. When `--converge` or `--cross-ai` is set, route the planning step through plan-review convergence before execution. Pauses only for explicit user decisions (grey area acceptance, blockers, validation requests). Re-reads ROADMAP.md after each phase to catch dynamically inserted phases.
5
+ Drive milestone phases autonomously — all remaining phases, a range via `--from N`/`--to N`, or a single phase via `--only N`. For each incomplete phase: discuss → plan → execute using Skill() flat invocations. When `--converge` or `--cross-ai` is set, route the planning step through plan-review convergence before execution. Pauses only for explicit user decisions (grey areas that need the user, blockers, validation requests). Re-reads ROADMAP.md after each phase to catch dynamically inserted phases.
6
6
 
7
7
  </purpose>
8
8
 
@@ -311,7 +311,7 @@ The discuss step in `--auto` mode MUST NOT loop. If CONTEXT.md already exists af
311
311
  Skill(skill="gsd-discuss-phase", args="${PHASE_NUM}")
312
312
  ```
313
313
 
314
- **If `INTERACTIVE` is NOT set:** Execute the smart_discuss step for this phase (batch table proposals, auto-optimized).
314
+ **If `INTERACTIVE` is NOT set:** Execute the smart_discuss step for this phase (classified grey-area questions).
315
315
 
316
316
  After discuss completes (either mode), verify context was written:
317
317
 
@@ -573,7 +573,7 @@ Display the review result summary and score from UI-REVIEW.md if produced. Conti
573
573
 
574
574
  > Full instructions are in `gsd-core/references/autonomous-smart-discuss.md`. Read that file now and follow it exactly.
575
575
 
576
- Smart discuss is an autonomous-optimized variant of `gsd-discuss-phase`. It proposes grey area answers in batch tables — the user accepts or overrides per area — and writes an identical CONTEXT.md to what discuss-phase produces.
576
+ Smart discuss is an autonomous-optimized variant of `gsd-discuss-phase`. It auto-decides reversible technical questions (logged), asks the rest (preference questions neutrally), and writes CONTEXT.md and DISCUSSION-LOG.md.
577
577
 
578
578
  **Inputs:** `PHASE_NUM` from execute_phase.
579
579
 
@@ -802,7 +802,7 @@ When any phase operation fails or a blocker is detected, present 3 options via A
802
802
 
803
803
  <success_criteria>
804
804
  - [ ] All incomplete phases executed in order (smart discuss → ui-phase → plan → execute → ui-review each)
805
- - [ ] Smart discuss proposes grey area answers in tables, user accepts or overrides per area
805
+ - [ ] Smart discuss auto-decides reversible technical questions and asks the rest
806
806
  - [ ] Progress banners displayed between phases
807
807
  - [ ] Execute-phase invoked with --no-transition (autonomous manages transitions)
808
808
  - [ ] Post-execution verification reads VERIFICATION.md and routes on status
@@ -86,21 +86,19 @@ FIX_REPORT_PATH="${PHASE_DIR}/${PADDED_PHASE}-REVIEW-FIX.md"
86
86
  </step>
87
87
 
88
88
  <step name="check_config_gate">
89
- Check if code review is active via the capability registry:
89
+ Check if code review is active via `workflow.code_review` (the capability's on/off toggle — independent of `workflow.code_review_point`, the loop-point selector; a manual invocation must work regardless of which automatic point is currently configured):
90
90
 
91
91
  ```bash
92
- EXECUTE_POST_HOOKS_JSON=$(gsd_run loop render-hooks execute:post --raw)
92
+ CODE_REVIEW_ENABLED=$(gsd_run query config-get workflow.code_review --raw 2>/dev/null || echo "true")
93
93
  ```
94
94
 
95
- Resolve active step hooks from `EXECUTE_POST_HOOKS_JSON` where `kind == "step"` and `ref.skill == "code-review"`.
96
-
97
- If no active code-review step hook exists:
95
+ If `CODE_REVIEW_ENABLED` is not `"true"`:
98
96
  ```
99
97
  Code review fix skipped (code-review capability inactive)
100
98
  ```
101
99
  Exit workflow.
102
100
 
103
- Default is active through the Capability Registry schema — only skip when the registry resolves no active code-review step hook. This check runs AFTER phase validation so invalid phase errors are shown first.
101
+ Default is active (`workflow.code_review` schema default is `true`) — only skip when explicitly disabled. This check runs AFTER phase validation so invalid phase errors are shown first.
104
102
 
105
103
  Note: This reuses the code-review capability activation rather than introducing a separate code-review-fix capability. Rationale: fixes are meaningless without review, so a single activation boundary makes sense. If independent control is needed later, a separate key can be added in v2.
106
104
  </step>
@@ -666,7 +664,7 @@ echo "════════════════════════
666
664
 
667
665
  <success_criteria>
668
666
  - [ ] Phase validated before config gate check
669
- - [ ] Capability gate checked (execute:post code-review hook)
667
+ - [ ] Capability gate checked (`workflow.code_review` config key)
670
668
  - [ ] REVIEW.md existence verified (error if missing)
671
669
  - [ ] REVIEW.md status checked (skip if clean/skipped)
672
670
  - [ ] Agent spawned with correct config (review_path, fix_scope, fix_report_path)
@@ -335,12 +335,6 @@ All subsequent commits go to this branch. User handles merging.
335
335
  From init JSON: `phase_dir`, `plan_count`, `incomplete_count`.
336
336
 
337
337
  Report: "Found {plan_count} plans in {phase_dir} ({incomplete_count} incomplete)"
338
-
339
- **Update STATE.md for phase start:**
340
- ```bash
341
- gsd_run query state.begin-phase --phase "${PHASE_NUMBER}" --name "${PHASE_NAME}" --plans "${PLAN_COUNT}"
342
- ```
343
- This updates Status, Last Activity, Current focus, Current Position, and plan counts in STATE.md so frontmatter and body text reflect the active phase immediately.
344
338
  </step>
345
339
 
346
340
  <step name="discover_and_group_plans">
@@ -432,6 +426,12 @@ Report:
432
426
  | 1 | 01-01, 01-02 | {from plan objectives, 3-8 words} |
433
427
  | 2 | 01-03 | ... |
434
428
  ```
429
+
430
+ **Update STATE.md for phase start — only now that at least one plan will be dispatched (#5843).** Every all-filtered arm above has already exited or jumped to `aggregate_results` / `update_roadmap`; reaching here means the post-filter list is non-empty. A run that dispatches nothing must leave STATE.md Current Position, Status and Last activity unchanged.
431
+ ```bash
432
+ gsd_run query state.begin-phase --phase "${PHASE_NUMBER}" --name "${PHASE_NAME}" --plans "${PLAN_COUNT}"
433
+ ```
434
+ This updates Status, Last Activity, Current focus, Current Position, and plan counts in STATE.md so frontmatter and body text reflect the active phase immediately.
435
435
  </step>
436
436
 
437
437
  <step name="cross_ai_delegation">
@@ -1184,7 +1184,21 @@ Route on `$STATUS`: if `passed`, proceed to update_roadmap. Otherwise keep the p
1184
1184
 
1185
1185
  **Step A: Persist human verification items as UAT file.**
1186
1186
 
1187
- Create `{phase_dir}/{phase_num}-UAT.md` using UAT template format:
1187
+ Merge or skip — never blindly create (#5845). A `{phase_num}-UAT.md` may already hold a human's recorded results (from `/gsd:verify-work`, or from an earlier run before gap-closure) that cannot be re-derived. Check first:
1188
+
1189
+ ```bash
1190
+ UAT_FILE="{phase_dir}/{phase_num}-UAT.md"
1191
+ if test -f "$UAT_FILE"; then echo "UAT_EXISTS"; else echo "UAT_ABSENT"; fi
1192
+ ```
1193
+
1194
+ **If `UAT_EXISTS`:** read the file and merge in place. Never reset, overwrite, or replace it, and do not rename it aside (`phase uat-passed` reads `{phase_num}-UAT.round1.md` in the phase directory as a live blocker).
1195
+ - Keep every recorded result exactly as written, including `result:` values other than `[pending]`, `source:`/`coverage_id:` lines, `## Gaps`, and `## Deferred Follow-Ups`. Keep `status` and `started` unchanged (`/gsd:verify-work` resume recomputes the status from the rows).
1196
+ - For each `human_verification` item in VERIFICATION.md, compare its description to the existing `### N. {description}` test entries. Items not already present (matched by description, case-insensitive with whitespace collapsed) are appended under `## Tests` as the next numbered entry with `expected:` from VERIFICATION.md and `result: [pending]`. Items already present are left untouched.
1197
+ - Recompute the `## Summary` counts (`total`, `passed`, `issues`, `pending`, `skipped`, `blocked`) from the merged test list; set `updated` to now.
1198
+ - If nothing to append (every item already present), do not write the file and do not commit; continue to Step B.
1199
+ - Otherwise commit the merged file with the command below.
1200
+
1201
+ **If `UAT_ABSENT`:** create `{phase_dir}/{phase_num}-UAT.md` using UAT template format:
1188
1202
 
1189
1203
  ```markdown
1190
1204
  ---
@@ -0,0 +1,101 @@
1
+ # Plan:Pre Generic Step and Gate Dispatch (#5982)
2
+
3
+ Implements @gsd-core/references/loop-hook-dispatch.md for `plan:pre`. Read `activeHooks` from `PLAN_PRE_HOOKS_JSON` in-context. This runs in **manual and auto-chain mode alike**, before the planner is spawned — a resolved hook is dispatched, never silently dropped.
4
+
5
+ **Dedicated sections.** These `capId`s own a `plan:pre` step or gate that a named section of `plan-phase.md` already runs with its own mode-specific behavior. Skip their `step` and `gate` entries here so none runs twice; dispatch **every other** active `step` and `gate`, first-party or third-party, in array order. (Contributions are injected at §5.55 and §8, not here.)
6
+
7
+ | capId | Section | What that section owns |
8
+ |---|---|---|
9
+ | `research` | §5 | the research step, behind §5's research decision |
10
+ | `drift` | §4.6 | the context-drift gate (§4.6) and the codebase-drift gate (§5.65), both non-blocking |
11
+ | `ui` | §5.6 | the `ui-phase` step and the `ui.plan-gate` gate (Branches 2–6) |
12
+ | `ai-integration` | §5.6 | the AI-integration step (pipeline: skill; manual: recommendation) |
13
+ | `pattern-mapper` | §7.8 | the pattern-mapper agent step, which needs `RESEARCH_PATH` |
14
+ | `intel` | §7.9 | the `intel api-surface` command step, which sets `API_SURFACE_PATH` |
15
+
16
+ If no active `step` or `gate` remains after that, `PLAN_PRE_PRODUCTS` stays empty; return to §5.6. Otherwise make sure the phase values the dispatch lines carry are set — §1 already parsed them from `INIT`, so each is only filled when empty:
17
+
18
+ ```bash
19
+ _GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; GSD_TOOLS="${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}"; _gsd_at() { for _p; do if [ -f "$_p" ]; then GSD_TOOLS="$_p"; return 0; fi; done; return 1; }; _gsd_id_ok() { case "$("$1" runtime-identity --raw 2>/dev/null || true)" in '{"packageName":"@opengsd/gsd-core"'*'}') return 0;; *) return 1;; esac; }; _gsd_homes() { set -- "${CLAUDE_CONFIG_DIR:-$HOME/.claude}" "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}" "$HOME/.gemini/antigravity-ide" "$HOME/.gemini/antigravity-cli" "${AUGMENT_CONFIG_DIR:-$HOME/.augment}" "${CLINE_CONFIG_DIR:-$HOME/.cline}" "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}" "${CODEX_HOME:-$HOME/.codex}" "${COPILOT_CONFIG_DIR:-${COPILOT_HOME:-$HOME/.copilot}}" "${CURSOR_CONFIG_DIR:-$HOME/.cursor}" "${HERMES_HOME:-$HOME/.hermes}" "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}" "${KIMI_CONFIG_DIR:-$HOME/.config/agents}" "$HOME/.agents" "${KIMI_CODE_HOME:-$HOME/.kimi-code}" "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}" "${PI_CODING_AGENT_DIR:-$HOME/.pi/agent}" "${QWEN_CONFIG_DIR:-$HOME/.qwen}" "${TRAE_CONFIG_DIR:-$HOME/.trae}" "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}" "${ZCODE_CONFIG_DIR:-$HOME/.zcode}" "${GROK_AGENTS_HOME:-$HOME/.agents}"; for _h; do _gsd_at "$_h/gsd-core/bin/${_GSD_SHIM_NAME}" && return 0; done; return 1; }; if _gsd_at "${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}" "${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" "${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}"; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif _gsd_homes; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif unset -f gsd_run; _G="$(command -v gsd_run)"; [ -n "$_G" ] && _gsd_id_ok "$_G"; then GSD_TOOLS="$_G"; gsd_run() { "$GSD_TOOLS" "$@"; }; else echo "ERROR: gsd-tools.cjs not found at $GSD_TOOLS and no identity-proving gsd_run is on PATH. Run: npx -y @opengsd/gsd-core@latest --claude --local" >&2; exit 1; fi; GSD_IDENTITY_STATUS=unverified; _gsd_id_ok gsd_run && GSD_IDENTITY_STATUS=ok; export GSD_IDENTITY_STATUS; [ "$GSD_IDENTITY_STATUS" = ok ] || echo "WARNING: \"$GSD_TOOLS\" did not prove it is @opengsd/gsd-core - it is either a different package or an @opengsd/gsd-core older than the runtime-identity verb. See docs/how-to/diagnose-a-foreign-gsd-tools.md" >&2; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${GSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${GSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi
20
+ IFS= read -rd '' GSD_ARGS <<'GSD_ARGS_END' || true
21
+ $ARGUMENTS
22
+ GSD_ARGS_END
23
+ GSD_ARGS=${GSD_ARGS%$'\n'}
24
+ GSD_WS=$(echo " $GSD_ARGS" | sed -nE 's/.* --ws +([A-Za-z0-9][A-Za-z0-9._-]*).*/--ws \1/p' | head -n 1)
25
+ PHASE_NUMBER="${PHASE_NUMBER:-$(gsd_run query init.plan-phase ${GSD_WS:+--ws=${GSD_WS##* }} "$PHASE" --pick phase_number 2>/dev/null)}"
26
+ PHASE_DIR="${PHASE_DIR:-$(gsd_run query init.plan-phase ${GSD_WS:+--ws=${GSD_WS##* }} "$PHASE" --pick phase_dir 2>/dev/null)}"
27
+ PHASE_DIR="${PHASE_DIR:-$(gsd_run query init.plan-phase ${GSD_WS:+--ws=${GSD_WS##* }} "$PHASE" --pick expected_phase_dir 2>/dev/null)}"
28
+ PHASE_REQ_IDS="${PHASE_REQ_IDS:-$(gsd_run query init.plan-phase ${GSD_WS:+--ws=${GSD_WS##* }} "$PHASE" --pick phase_req_ids 2>/dev/null)}"
29
+ PHASE_REQ_IDS="${PHASE_REQ_IDS:-TBD}"
30
+ AUTO_CHAIN=$(gsd_run query check auto-mode --pick auto_chain_active 2>/dev/null)
31
+ AUTO_CHAIN="${AUTO_CHAIN:-false}"
32
+ echo "PHASE_DIR=${PHASE_DIR}"
33
+ ```
34
+
35
+ ## Steps
36
+
37
+ For each remaining active entry where `kind == "step"` (exactly one of `ref.skill`, `ref.agent`, `ref.command` is set):
38
+
39
+ - **`ref.skill`** → `Skill(skill="gsd-${ref.skill}", args="${PHASE} ${GSD_WS}")`, appending `--auto` to `args` only when `AUTO_CHAIN` is `true`. Prepend `gsd-` to `ref.skill`.
40
+ - **`ref.agent`** → print `◆ Spawning ${ref.agent}... (runs in a subagent — no output until it returns; expected, not a freeze)`, then `Agent(prompt=filled_hook_fragment, subagent_type=ref.agent, model="{researcher_model}")` with the hook's `fragment.inline` as the prompt body, phase fields filled. Omit `model=` when `researcher_model` is `"inherit"` or empty (#2517).
41
+ - **`ref.command`** → validate it IN-CONTEXT before any shell use: the value must match `^[a-z][a-z0-9-]*( [a-z][a-z0-9-]*)*$`. Check it yourself — never by pasting it into a shell command to be tested there. A value that fails is a malformed manifest: record a warning naming the `capId`, skip it, continue. Only a value that passed is run:
42
+
43
+ ```bash
44
+ # shellcheck disable=SC2086 # the validated command is a family and subcommand: splitting it into two words is intended
45
+ gsd_run ${ref.command} --phase "${PHASE_NUMBER}" --raw
46
+ ```
47
+
48
+ Wait for each result before the next hook. A step is advisory: on error record a warning naming the `capId` and continue, unless `onError == "halt"` — then surface the error and stop the plan-phase workflow.
49
+
50
+ ## Gates
51
+
52
+ For each remaining active entry where `kind == "gate"`, in array order. ⚠ **Validate `check` before shell use** (third-party manifest input; `loop-hook-dispatch.md` § `gate`): replace every `.` in `check.query` with `-`, check that normalized value against `^[a-z][a-z0-9]*(-[a-z0-9]+)*( [a-z][a-z0-9]*(-[a-z0-9]+)*)*$` in-context, and write the dot-normalized query wherever `${hook.check.query}` appears below. Serialize `check.predicate` to compact one-line JSON and pass it as a **single argv element** through the quoted heredoc below — never re-quote it into a shell string, where an apostrophe would close the literal. A malformed check is routed per `onError` in step 1, never run.
53
+
54
+ ```bash
55
+ # named-query gate (the validated query may be several words; splitting it is intended):
56
+ # shellcheck disable=SC2086
57
+ GATE_RESULT=$(gsd_run check ${hook.check.query} "${PHASE_DIR}" "${PHASE_REQ_IDS}" --raw) && CHECK_EXIT=0 || CHECK_EXIT=$?
58
+ ```
59
+ OR, for a `predicate` gate (ADR-2008):
60
+ ```bash
61
+ IFS= read -rd '' PREDICATE_JSON <<'GSD_PREDICATE_END' || true
62
+ <hook.check.predicate as compact one-line JSON>
63
+ GSD_PREDICATE_END
64
+ PREDICATE_JSON=${PREDICATE_JSON%$'\n'}
65
+ GATE_RESULT=$(gsd_run check predicate --predicate "$PREDICATE_JSON" --phase-dir "${PHASE_DIR}" --phase-number "${PHASE_NUMBER}" --phase-req-ids "${PHASE_REQ_IDS}" --raw) && CHECK_EXIT=0 || CHECK_EXIT=$?
66
+ ```
67
+
68
+ A gate carrying neither — including an `agentVerdict` check, which has no runner at `plan:pre` — cannot be evaluated here: record a warning naming the `capId` and treat it as a check-command failure in step 1, **never** as a silent pass.
69
+
70
+ **Step 1 — did the check command fail?** (malformed check, non-zero `CHECK_EXIT` including `69` UNAVAILABLE, empty output, or unparseable JSON — its `block` is not a verdict):
71
+ - `onError == "halt"` → surface `⚠ plan:pre gate check failed ({hook.capId})` with the error and stop the plan-phase workflow.
72
+ - otherwise → record a warning naming the `capId` and continue to the next gate.
73
+
74
+ **Step 2 — read `GATE_RESULT.block`:**
75
+ - `blocking == true` and `block == true` → output `⚠ Planning blocked by capability gate ({hook.capId}): {message}` (`check.message`, plus the result's own `message` when it has one) and **EXIT the plan-phase workflow. Do not spawn the planner.**
76
+ - `blocking == false` → advisory: surface the message when `block` is `true`, and continue.
77
+ - `block == false` → continue silently.
78
+
79
+ ## Products for the planner (#5960)
80
+
81
+ A step dispatched above hands its declared `produces` files to the planner, so a capability's plan:pre output (for example MemPalace's `MEMORY-RECALL.md`) is read, and only when its step was active this run. Validate each `produces` entry IN-CONTEXT against `^[A-Za-z0-9][A-Za-z0-9._-]*$` (a bare file name: no `/`, no leading `.`); record a warning naming the `capId` and drop a name that fails. Then put the remaining names of the dispatched steps, space-separated, in place of the placeholder:
82
+
83
+ ```bash
84
+ # Plan:pre products (#5960)
85
+ IFS= read -rd '' _PHASE_DIR_IN <<'GSD_PHASE_DIR_END' || true
86
+ <PHASE_DIR from the phase-value block above>
87
+ GSD_PHASE_DIR_END
88
+ PHASE_DIR="${PHASE_DIR:-${_PHASE_DIR_IN%$'\n'}}"
89
+ PLAN_PRE_PRODUCES='<validated produces names of the steps dispatched above, space-separated>'
90
+ [ -n "${PLAN_PRE_PRODUCES}" ] && [ -z "${PHASE_DIR}" ] && echo "WARNING: plan:pre products not collected — PHASE_DIR is empty" >&2
91
+ PLAN_PRE_PRODUCTS=""
92
+ # shellcheck disable=SC2046 # validated bare file names: splitting the substitution is intended
93
+ for _f in $(printf '%s' "$PLAN_PRE_PRODUCES"); do
94
+ if [ -n "${PHASE_DIR}" ] && [ -f "${PHASE_DIR}/${_f}" ]; then PLAN_PRE_PRODUCTS="${PLAN_PRE_PRODUCTS:+${PLAN_PRE_PRODUCTS}, }${PHASE_DIR}/${_f}"; fi
95
+ done
96
+ echo "PLAN_PRE_PRODUCTS=${PLAN_PRE_PRODUCTS}"
97
+ ```
98
+
99
+ `PLAN_PRE_PRODUCTS` is listed in the §8 planner `<required_reading>`; it stays empty when no dispatched step wrote its file. A dedicated-section capability's output (RESEARCH.md, PATTERNS.md, UI-SPEC.md, ...) reaches the planner through its own section, not this list. Shell variables do not survive between tool calls, so fill the `PHASE_DIR` value the phase-value block printed into the quoted heredoc as well.
100
+
101
+ When every remaining hook has been handled, return to §5.6.
@@ -99,7 +99,7 @@ CONTEXT_WINDOW=$(gsd_run query config-get context_window --raw 2>/dev/null || ec
99
99
  MVP_MODE_CFG=$(gsd_run query config-get workflow.mvp_mode --raw 2>/dev/null || echo "false")
100
100
  ```
101
101
 
102
- When the tdd capability's `workflow.tdd_mode` is active (resolved via the plan:pre render-hooks), the planner agent is instructed to apply `type: tdd` to eligible tasks using heuristics from `gsd-core/references/tdd.md`. The TDD guidance is injected via the tdd capability's contribution hook at §5.6; no inline config-get is needed.
102
+ When the tdd capability's `workflow.tdd_mode` is active (resolved via the plan:pre render-hooks), the planner agent is instructed to apply `type: tdd` to eligible tasks using heuristics from `gsd-core/references/tdd.md`. The TDD guidance is injected via the tdd capability's contribution hook at §8; no inline config-get is needed.
103
103
 
104
104
  When `CONTEXT_WINDOW >= 500000`, the planner prompt includes the 3 most recent prior-phase CONTEXT.md/SUMMARY.md files plus any phases in the current phase's `Depends on:` field (explicit deps load regardless of recency).
105
105
 
@@ -357,9 +357,9 @@ If `section_manifest` is `null` or `"research-only-modifiers"` is in its `includ
357
357
 
358
358
  ### 5.1. Standard Research Decision
359
359
 
360
- **Skip if** `RESEARCH_ONLY=true` (the research-only mode in 5.0 already determined the path: spawn or exit). Without this guard, an LLM following the workflow could fall through into "use existing, skip to step 6" → planner spawn, violating the research-only contract. **CR #3045 finding: this gate makes the early-exit unreachable from any non-research-only branch.**
360
+ **Skip if** `RESEARCH_ONLY=true` (the research-only mode in 5.0 already determined the path: spawn or exit). Without this guard, an LLM following the workflow could fall through into "use existing, continue to step 5.5" → planner spawn, violating the research-only contract. **CR #3045 finding: this gate makes the early-exit unreachable from any non-research-only branch.**
361
361
 
362
- **If `has_research` is true (from init) AND no `--research` flag:** Use existing, skip to step 6.
362
+ **If `has_research` is true (from init) AND no `--research` flag:** Use existing, continue to step 5.5.
363
363
 
364
364
  **If RESEARCH.md missing OR `--research` flag:**
365
365
 
@@ -391,7 +391,7 @@ AskUserQuestion([
391
391
  ])
392
392
  ```
393
393
 
394
- If user selects "Skip research": skip to step 6.
394
+ If user selects "Skip research": continue to step 5.5.
395
395
 
396
396
  **If `--auto` and `research_enabled` is false:** Skip research silently (preserves automated behavior).
397
397
 
@@ -437,7 +437,7 @@ Agent(
437
437
 
438
438
  ### Handle Researcher Return
439
439
 
440
- - **`## RESEARCH COMPLETE`:** Display confirmation, continue to step 6
440
+ - **`## RESEARCH COMPLETE`:** Display confirmation, continue to step 5.5
441
441
  - **`## RESEARCH BLOCKED`:** Display blocker, offer: 1) Provide context, 2) Skip research, 3) Abort
442
442
 
443
443
  <!-- gsd:section id="research-only-early-exit" when="flag:--research-phase" -->
@@ -448,14 +448,14 @@ If `section_manifest` is `null` or `"research-only-early-exit"` is in its `inclu
448
448
 
449
449
  Skip if `nyquist_validation_enabled` is false OR `research_enabled` is false.
450
450
 
451
- If `research_enabled` is false and `nyquist_validation_enabled` is true: warn "Nyquist validation enabled but research disabled — VALIDATION.md cannot be created without RESEARCH.md. Plans will lack validation requirements (Dimension 8)." Continue to step 6.
451
+ If `research_enabled` is false and `nyquist_validation_enabled` is true: warn "Nyquist validation enabled but research disabled — VALIDATION.md cannot be created without RESEARCH.md. Plans will lack validation requirements (Dimension 8)." Continue to step 5.55.
452
452
 
453
453
  **But Nyquist is not applicable for this run** when all of the following are true:
454
454
  - `research_enabled` is false
455
455
  - `has_research` is false
456
456
  - no `--research` flag was provided
457
457
 
458
- In that case: **skip validation-strategy creation entirely**. Do **not** expect `RESEARCH.md` or `VALIDATION.md` for this run, and continue to Step 6.
458
+ In that case: **skip validation-strategy creation entirely**. Do **not** expect `RESEARCH.md` or `VALIDATION.md` for this run, and continue to step 5.55.
459
459
 
460
460
  ```bash
461
461
  grep -l "## Validation Architecture" "${PHASE_DIR}"/*-RESEARCH.md 2>/dev/null || true
@@ -506,7 +506,7 @@ Continue to step 5.6. Security config is passed to the planner in step 8.
506
506
 
507
507
  ## 5.6. Plan:Pre Capability Dispatch and UI Design Contract Gate
508
508
 
509
- > Capability-driven dispatch. Resolves active `plan:pre` hooks via the capability registry; each hook's `when` condition is evaluated by the registry — no inline config-get needed. This section handles skill-based planning preflights such as `ai-integration`, agent-backed hooks through `ref.agent`, and the UI gate whose deterministic check comes from `check.query`.
509
+ > Capability-driven dispatch. Resolves active `plan:pre` hooks via the capability registry; each hook's `when` condition is evaluated by the registry — no inline config-get needed. This section runs the generic step/gate dispatch, the `ai-integration` preflight, and the UI gate whose deterministic check comes from `check.query`.
510
510
  >
511
511
  > **Config semantics (cutover fix):** `workflow.ui_phase` gates UI-SPEC *generation* (step); `workflow.ui_safety_gate` gates the *planning block* (gate). Both-on = identical to OLD §5.6. Intended change: `{ui_phase:true, ui_safety_gate:false}` now auto-generates in pipelines but does NOT block manual planning (each key controls exactly what its description says).
512
512
 
@@ -517,12 +517,9 @@ HOOKS_JSON="$PLAN_PRE_HOOKS_JSON"
517
517
 
518
518
  Read the `activeHooks` array directly from `PLAN_PRE_HOOKS_JSON` / `HOOKS_JSON` (in-context — do NOT invoke a shell pipeline).
519
519
 
520
- **Branch 1 — all plan:pre hooks inactive (`activeHooks` is empty or absent):** Skip to step 6.
520
+ **Branch 1 — all plan:pre hooks inactive (`activeHooks` is empty or absent):** Skip to step 6; `PLAN_PRE_PRODUCTS` stays empty.
521
521
 
522
- **Generic step hook dispatch contract:** For each active entry where `kind == "step"`:
523
- - If `ref.skill` is set, dispatch with `Skill(skill="gsd-${ref.skill}", args="${PHASE} --auto ${GSD_WS}")` when pipeline mode allows auto-chaining. Prepend `gsd-` to `ref.skill` — `ui-phase` → `gsd-ui-phase`.
524
- - If `ref.agent` is set, dispatch with `Agent(prompt=filled_hook_fragment, subagent_type=ref.agent, model="{researcher_model}")`. Use the hook's `fragment.inline` as the prompt body and fill phase fields before spawning.
525
- - The `research` hook is handled by §5.1's research decision. The `pattern-mapper` hook is handled by §7.8 after `RESEARCH_PATH` is known. Future plan:pre agent hooks use the same `ref.agent` fragment contract.
522
+ **Generic dispatch (#5982):** dispatch every active `kind == "step"` hook (`ref.skill`, `ref.agent`, `ref.command`) and evaluate every active `kind == "gate"` hook (`check.query`, `check.predicate`) per @gsd-core/references/loop-hook-dispatch.md, in manual and auto-chain mode: read and execute `gsd-core/workflows/plan-phase/steps/plan-pre-dispatch.md` — except the capabilities its table assigns to a dedicated section (research, drift, ui, ai-integration, pattern-mapper, intel). A blocking gate that blocks exits the workflow.
526
523
 
527
524
  **AI integration capability:** If the active `ai-integration` step hook is present, `AI_SPEC_PATH` is empty, and the phase goal contains AI keywords (`agent`, `llm`, `rag`, `chatbot`, `embedding`, `langchain`, `llamaindex`, `crewai`, `langgraph`, `openai`, `anthropic`, `vector`, `llm eval`), then:
528
525
  - In pipeline / `--auto` mode, invoke the hook's `ref.skill` via `Skill(skill="gsd-${ref.skill}", args="${PHASE} --auto ${GSD_WS}")`.
@@ -536,7 +533,7 @@ GATE=$(gsd_run check ui-plan-gate "${PHASE}" --raw) && GATE_EXIT=0 || GATE_EXIT=
536
533
 
537
534
  A non-zero `GATE_EXIT` is a command failure, including `69` (`UNAVAILABLE`: the gate could not read its evidence, so its `frontend: false` is not an answer): surface it and stop; never fall through to Branch 2. Otherwise read `frontend`, `hasUiSpec`, and `block` from `GATE`.
538
535
 
539
- **Branch 2 — no frontend indicators (`frontend` is `false`):** Skip silently to step 6.
536
+ **Branch 2 — no frontend indicators (`frontend` is `false`):** Skip silently to step 5.65.
540
537
 
541
538
  **Branch 3 — UI-SPEC already exists (`hasUiSpec` is `true`):**
542
539
 
@@ -545,9 +542,9 @@ UI_SPEC_FILE=$(ls "${PHASE_DIR}"/*-UI-SPEC.md 2>/dev/null | head -1)
545
542
  UI_SPEC_PATH="${UI_SPEC_FILE}"
546
543
  ```
547
544
 
548
- Display: `Using UI design contract: ${UI_SPEC_PATH}`. Continue to step 6.
545
+ Display: `Using UI design contract: ${UI_SPEC_PATH}`. Continue to step 5.65.
549
546
 
550
- **Branch 4 — `--skip-ui` in `$ARGUMENTS`:** Skip silently to step 6.
547
+ **Branch 4 — `--skip-ui` in `$ARGUMENTS`:** Skip silently to step 5.65.
551
548
 
552
549
  **Branches 5 & 6 — frontend detected, UI-SPEC missing, no `--skip-ui`.**
553
550
 
@@ -558,7 +555,7 @@ AUTO_CHAIN=$(gsd_run query check auto-mode --pick auto_chain_active 2>/dev/null)
558
555
  AUTO_CHAIN="${AUTO_CHAIN:-false}"
559
556
  ```
560
557
 
561
- **Branch 5 — `AUTO_CHAIN` is `true` (pipeline / `--auto`):** Fire each active UI **step** hook — runs independently of whether a gate is active (covers `{ui_phase:true,ui_safety_gate:false}`). For each entry in `activeHooks` (in array order) where `kind == "step"` and `ref.skill` is set:
558
+ **Branch 5 — `AUTO_CHAIN` is `true` (pipeline / `--auto`):** Fire each active UI **step** hook — runs independently of whether a gate is active (covers `{ui_phase:true,ui_safety_gate:false}`). For each entry in `activeHooks` (in array order) where `kind == "step"`, `capId == "ui"` and `ref.skill` is set:
562
559
 
563
560
  ```
564
561
  Skill(skill="gsd-${ref.skill}", args="${PHASE} --auto ${GSD_WS}")
@@ -571,9 +568,9 @@ UI_SPEC_FILE=$(ls "${PHASE_DIR}"/*-UI-SPEC.md 2>/dev/null | head -1)
571
568
  UI_SPEC_PATH="${UI_SPEC_FILE}"
572
569
  ```
573
570
 
574
- Continue to step 6.
571
+ Continue to step 5.65.
575
572
 
576
- **Branch 6 — `AUTO_CHAIN` is `false` (manual): generic gate handling.** For each entry in `activeHooks` where `kind == "gate"` and `blocking` is `true`: if `block:true` (from `GATE`), output the block below and **EXIT the plan-phase workflow**. If no active blocking gate (e.g. `workflow.ui_safety_gate` is off), continue to step 6 — no block.
573
+ **Branch 6 — `AUTO_CHAIN` is `false` (manual): UI gate handling.** For each entry in `activeHooks` where `kind == "gate"`, `capId == "ui"` and `blocking` is `true`: if `block:true` (from `GATE`), output the block below and **EXIT the plan-phase workflow**. If no active blocking gate (e.g. `workflow.ui_safety_gate` is off), continue to step 5.65 — no block.
577
574
 
578
575
  Output this markdown directly (not as a code block):
579
576
 
@@ -637,6 +634,7 @@ SKETCH_FINDINGS_PATH=$(ls ./.claude/skills/sketch-findings-*/SKILL.md 2>/dev/nul
637
634
  # on non-AI phases; gating it there silently starves the planner of the SPEC (#550 review).
638
635
  # init resolves the plain phase SPEC (never the -AI-SPEC.md / -UI-SPEC.md variants) through the one shared predicate.
639
636
  PHASE_DIR_FOR_SPEC=$(_gsd_field "$INIT" phase_dir)
637
+ PHASE_DIR_FOR_SPEC="${PHASE_DIR_FOR_SPEC:-$(_gsd_field "$INIT" expected_phase_dir)}" # §2 may create it after INIT (#5960)
640
638
  SPEC_PATH=$(_gsd_field "$INIT" spec_path)
641
639
  SPEC_FILE="${SPEC_PATH}"
642
640
  # Resolve the phase UI-SPEC separately (the glob above excludes -UI-SPEC.md); it carries the
@@ -780,6 +778,7 @@ Planner prompt:
780
778
  - {SPIKE_FINDINGS_PATH} (Spike Findings — validated patterns, constraints, landmines from experiments, if exists)
781
779
  - {SKETCH_FINDINGS_PATH} (Sketch Findings — validated design decisions, CSS patterns, visual direction, if exists)
782
780
  - {API_SURFACE_PATH} (API Surface — HINT ONLY, when intel capability is active; see <intel_surface_hint> below)
781
+ - {PLAN_PRE_PRODUCTS} (plan:pre capability outputs from this run, e.g. MemPalace's MEMORY-RECALL.md — prior decisions, patterns, surprises; if any)
783
782
  ${CONTEXT_WINDOW >= 500000 ? `
784
783
  **Cross-phase context (1M model enrichment):**
785
784
  - CONTEXT.md files from the 3 most recent completed phases (locked decisions — maintain consistency)
@@ -74,6 +74,7 @@ const os = require('os');
74
74
  const { readSentinel, VALID_ISOLATION, extractDispatchIdentifiers, sentinelAppliesToDispatch, agedNoneGovernsDispatch, buildSentinelDiscard } = require('./lib/isolation-sentinel.js');
75
75
  const { REASON_CODE, describeSentinelDiscard } = require('./lib/isolation-deny-reason.js');
76
76
  const { HOOK_ON_CRASH, allow, deny, crash } = require('./lib/hook-exit.js');
77
+ const { readConfigFileBounded } = require('./lib/config-read.js');
77
78
 
78
79
  // Required at module top, alongside the other ./lib requires — NOT behind
79
80
  // ensureRuntimeBuild() below. Terminating on a parse/timeout failure must
@@ -208,7 +209,7 @@ function resolveRuntimeIdentity(cwd, configPath, resolveRuntimeNameFromCandidate
208
209
  // A throw here (corrupt JSON, EISDIR, permission error) propagates to the
209
210
  // caller's catch as a resolution failure — this function only decides
210
211
  // "confident vs not", never resolution failure.
211
- const raw = fs.readFileSync(configPath, 'utf-8');
212
+ const raw = readConfigFileBounded(configPath, { required: true });
212
213
  const parsed = JSON.parse(raw);
213
214
  if (parsed && typeof parsed === 'object' && 'runtime' in parsed) {
214
215
  const configRuntime = resolveRuntimeNameFromCandidates(parsed.runtime);
@@ -331,7 +332,7 @@ function resolveRegistryIsolation(cwd, configPath) {
331
332
  }
332
333
  if (!ladderAnswered) {
333
334
  try {
334
- const raw = fs.readFileSync(configPath, 'utf-8');
335
+ const raw = readConfigFileBounded(configPath, { required: true });
335
336
  const parsedCfg = JSON.parse(raw);
336
337
  if (parsedCfg && typeof parsedCfg === 'object' && parsedCfg.workflow &&
337
338
  typeof parsedCfg.workflow === 'object' && parsedCfg.workflow.use_worktrees === false) {
@@ -422,7 +423,7 @@ function resolveIsolationState(cwd, { clock = Date, dispatchIds = null } = {}) {
422
423
  return { gsdProject: false, isolation: null, harnessFlag: null, error: null, sentinelDiscarded: null };
423
424
  }
424
425
 
425
- const sentinel = readSentinel(cwd, { clock });
426
+ const sentinel = readSentinel(cwd, { clock, dispatchIds });
426
427
  // Hoisted so the "sentinel was present/fresh but did not apply" case below
427
428
  // (#4594 row 15 — Postel's-Law finding) can distinguish itself from
428
429
  // "absent"/"stale" without re-deriving applicability.
@@ -18,9 +18,9 @@
18
18
  // Enabled for all Claude Code installs. This hook is always-on — it is a
19
19
  // no-op when .planning/config.json is absent (ENOENT → exit 0).
20
20
 
21
- const fs = require('fs');
22
21
  const path = require('path');
23
22
  const { HOOK_ON_CRASH, allow, crash } = require('./lib/hook-exit.js');
23
+ const { readConfigFileBounded } = require('./lib/config-read.js');
24
24
 
25
25
  // This hook only injects an advisory config-reload summary; a crash mid-parse
26
26
  // must not block the session or the FileChanged event that triggered it — the
@@ -70,7 +70,7 @@ process.stdin.on('end', () => {
70
70
  // Read the updated config file
71
71
  let config;
72
72
  try {
73
- const raw = fs.readFileSync(filePath, 'utf8');
73
+ const raw = readConfigFileBounded(filePath, { required: true });
74
74
  config = JSON.parse(raw);
75
75
  } catch (e) {
76
76
  if (e && e.code === 'ENOENT') allow(undefined);
@@ -28,6 +28,7 @@ const os = require('os');
28
28
  const path = require('path');
29
29
  const { spawn } = require('child_process');
30
30
  const { HOOK_ON_CRASH, allow, crash } = require('./lib/hook-exit.js');
31
+ const { readConfigFileBounded } = require('./lib/config-read.js');
31
32
 
32
33
  // This hook only injects an advisory context-usage warning; it never blocks
33
34
  // the tool call it rides in on. A crash here (e.g. a malformed bridge file)
@@ -334,7 +335,7 @@ const handleStdinEnd = () => {
334
335
  let thresholds = { warning: WARNING_THRESHOLD, critical: CRITICAL_THRESHOLD };
335
336
  try {
336
337
  const configPath = path.join(cwd, '.planning', 'config.json');
337
- const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
338
+ const config = JSON.parse(readConfigFileBounded(configPath, { required: true }));
338
339
  if (config.hooks?.context_warnings === false) {
339
340
  allow(undefined);
340
341
  }
@@ -65,6 +65,7 @@ const fs = require('fs');
65
65
  const path = require('path');
66
66
  const os = require('os');
67
67
  const { allow } = require('./lib/hook-exit.js');
68
+ const { readConfigFileBounded } = require('./lib/config-read.js');
68
69
 
69
70
  // Workspace resolution is shared across the Cursor hooks (#2587) — see
70
71
  // hooks/lib/cursor-workspace.js. Staged next to these scripts by
@@ -133,12 +134,12 @@ function realpathOrNull(p, realpathFn) {
133
134
  * i.e. `~/.cursor/worktrees` by default — `getGlobalConfigDir('cursor')`
134
135
  * honors the `CURSOR_CONFIG_DIR` env override and `~` expansion for free).
135
136
  * That is made NECESSARY AND SUFFICIENT below. Do NOT reinstate
136
- * `resolveWorktreeLinkage`'s `linked_worktree_root` mode as an alternative
137
+ * a linked-worktree result of `resolveProjectContext` as an alternative
137
138
  * OR'd proof of isolation — that is precisely the bypass finding 3 closed;
138
139
  * a future "simplification" that merges it back in re-opens unconsented
139
140
  * writes to the human's active checkout.
140
141
  *
141
- * `resolveWorktreeLinkage` is still called, but ONLY as a diagnostic: it
142
+ * `resolveProjectContext` is still called, but ONLY as a diagnostic: its provenance
142
143
  * distinguishes "confidently not isolated" from "git could not answer
143
144
  * (timeout) — cannot determine" so the eventual deny reason stays
144
145
  * actionable. Its result never flips `isolated`.
@@ -195,8 +196,10 @@ function resolveIsolationEvidence(root, { realpath = fs.realpathSync } = {}) {
195
196
  let linkageReason = null;
196
197
  try {
197
198
  // Sibling data/policy module, staged alongside this hook at install time.
198
- const { resolveWorktreeLinkage } = require('../gsd-core/bin/lib/worktree-safety.cjs');
199
- linkageReason = resolveWorktreeLinkage(root).reason;
199
+ const { resolveProjectContext } = require('../gsd-core/bin/lib/project-context.cjs');
200
+ const gitProvenance = resolveProjectContext(root).provenance;
201
+ // A degraded context (git timed out or could not run) is "cannot determine".
202
+ linkageReason = gitProvenance.degraded ? 'git_timed_out' : gitProvenance.reason;
200
203
  } catch {
201
204
  linkageReason = null;
202
205
  }
@@ -421,7 +424,7 @@ function resolveFallbackIsolation(root, configPath) {
421
424
  const { runtimes } = require('../gsd-core/bin/lib/capability-registry.cjs');
422
425
 
423
426
  let runtimeId = resolveRuntimeNameFromCandidates(process.env.GSD_RUNTIME);
424
- const rawConfig = fs.readFileSync(configPath, 'utf-8');
427
+ const rawConfig = readConfigFileBounded(configPath, { required: true });
425
428
  const parsedConfig = JSON.parse(rawConfig);
426
429
  if (!runtimeId && parsedConfig && typeof parsedConfig === 'object' && 'runtime' in parsedConfig) {
427
430
  runtimeId = resolveRuntimeNameFromCandidates(parsedConfig.runtime) || null;
@@ -512,7 +515,7 @@ function evaluateRootIsolation(root, subagentType, { clock = Date, dispatchIds =
512
515
  // Hoisted (readSentinel never throws) so the "present, fresh, but did not
513
516
  // apply" case (#4594 row 15) can be reported on every deny path below
514
517
  // instead of silently discarded.
515
- const sentinel = readSentinel(root, { clock });
518
+ const sentinel = readSentinel(root, { clock, dispatchIds });
516
519
  const applies = sentinelAppliesToDispatch(sentinel, dispatchIds);
517
520
  const sentinelDiscarded = buildSentinelDiscard(sentinel, dispatchIds);
518
521
 
@@ -22,8 +22,8 @@
22
22
  // hook source files — these legitimately contain injection-like strings.
23
23
 
24
24
  const path = require('path');
25
- const fs = require('fs');
26
25
  const { HOOK_ON_CRASH, allow, crash } = require('./lib/hook-exit.js');
26
+ const { readConfigFileBounded } = require('./lib/config-read.js');
27
27
 
28
28
  // This is a PostToolUse advisory scanner over content the tool call already
29
29
  // returned; a crash while scanning must not retroactively block that Read/
@@ -343,7 +343,7 @@ process.stdin.on('end', () => {
343
343
  try {
344
344
  const cfgBase = data.cwd || process.cwd();
345
345
  const cfgPath = path.join(cfgBase, '.planning', 'config.json');
346
- const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
346
+ const cfg = JSON.parse(readConfigFileBounded(cfgPath, { required: true }));
347
347
  blocking = cfg.security?.injection_blocking === true;
348
348
  } catch { /* no config ⇒ advisory */ }
349
349
  }