@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
@@ -271,6 +271,19 @@ Resume testing: `/gsd:verify-work {phase} ${GSD_WS}` — retest specific phase
271
271
 
272
272
  This is a WARNING, not a blocker — routing proceeds normally. The debt is visible so the user can make an informed choice.
273
273
 
274
+ **Step 1.7: Check verification status for the current phase**
275
+
276
+ A phase whose verification ended `gaps_found` or `human_needed` is NOT complete, even when every PLAN.md has a matching SUMMARY.md. The count-based status (`roadmap.analyze`) only sees plans/summaries, so without this check such a phase is reported complete and routing skips straight to the next phase. When the phase appears count-complete (`summaries = plans AND plans > 0`), consult the verification report (the same `verification.status` gate `ship` and `execute-phase` use, from #651):
277
+
278
+ ```bash
279
+ PHASE_DIR=".planning/phases/[current-phase-dir]"
280
+ VERIFICATION=$(gsd_run query verification.status "${PHASE_DIR}" 2>/dev/null)
281
+ VERIFICATION_STATUS=$(printf '%s' "$VERIFICATION" | jq -r '.status' 2>/dev/null || echo "")
282
+ VERIFICATION_NEXT_ACTION=$(printf '%s' "$VERIFICATION" | jq -r '.next_action' 2>/dev/null || echo "")
283
+ ```
284
+
285
+ Track: `verification_status` — the `.status` field (`passed | gaps_found | human_needed | missing | unknown`). The query already handles a missing VERIFICATION.md (returns `missing`) and unexpected values, so no per-status file probing is needed. `passed`, `missing` (not yet verified), and `unknown` route as complete (Step 3) — `missing` with an advisory that the phase is unverified; `gaps_found` and `human_needed` route back to close the verification debt (Step 2).
286
+
274
287
  **Step 2: Route based on counts**
275
288
 
276
289
  | Condition | Meaning | Action |
@@ -278,9 +291,13 @@ This is a WARNING, not a blocker — routing proceeds normally. The debt is visi
278
291
  | uat_partial > 0 | UAT testing incomplete | Go to **Route E.2** |
279
292
  | uat_with_gaps > 0 | UAT gaps need fix plans | Go to **Route E** |
280
293
  | summaries < plans | Unexecuted plans exist | Go to **Route A** |
281
- | summaries = plans AND plans > 0 | Phase complete | Go to Step 3 |
294
+ | summaries = plans AND plans > 0 AND verification_status = gaps_found | Phase executed; verification found gaps | Go to **Route V.gaps** |
295
+ | summaries = plans AND plans > 0 AND verification_status = human_needed | Phase executed; awaiting human verification | Go to **Route V.human** |
296
+ | summaries = plans AND plans > 0 | Phase complete (verification passed, missing, or n/a) | Go to Step 3 |
282
297
  | plans = 0 | Phase not yet planned | Go to **Route B** |
283
298
 
299
+ Rows are evaluated top to bottom; the first matching row wins. The two `verification_status` rows must precede the general `summaries = plans` row so a non-`passed` verification is not reported as complete.
300
+
284
301
  ---
285
302
 
286
303
  **Route A: Unexecuted plan exists**
@@ -431,6 +448,46 @@ UAT.md exists with `status: partial` — testing session ended before all items
431
448
 
432
449
  ---
433
450
 
451
+ **Route V.gaps: verification found gaps (gaps_found)**
452
+
453
+ VERIFICATION.md exists with `status: gaps_found` — verification identified gaps that need fix plans. The phase is NOT complete.
454
+
455
+ ```
456
+ ---
457
+
458
+ ## ⚠ Verification Gaps Found
459
+
460
+ **{phase_num}-VERIFICATION.md** reports `gaps_found`. ${VERIFICATION_NEXT_ACTION}
461
+
462
+ `/clear` then:
463
+
464
+ `/gsd:plan-phase {phase} --gaps ${GSD_WS}`
465
+
466
+ ---
467
+ ```
468
+
469
+ ---
470
+
471
+ **Route V.human: human verification required (human_needed)**
472
+
473
+ VERIFICATION.md exists with `status: human_needed` — automated checks passed but manual verification items remain. The phase is NOT complete until they are resolved.
474
+
475
+ ```
476
+ ---
477
+
478
+ ## Human Verification Required
479
+
480
+ **{phase_num}-VERIFICATION.md** reports `human_needed`. ${VERIFICATION_NEXT_ACTION}
481
+
482
+ `/clear` then:
483
+
484
+ `/gsd:verify-work {phase} ${GSD_WS}` — resume human verification
485
+
486
+ ---
487
+ ```
488
+
489
+ ---
490
+
434
491
  **Step 3: Check milestone status (only when phase complete)**
435
492
 
436
493
  Read ROADMAP.md and identify:
@@ -184,8 +184,9 @@ compound on top of each other and stay unpushed (#2916). If `$branch_name`
184
184
  already exists locally, reuse it as-is so resumed work is not rebased.
185
185
 
186
186
  ```bash
187
- DEFAULT_BRANCH=$(git symbolic-ref --quiet --short refs/remotes/origin/HEAD 2>/dev/null | sed 's|^origin/||')
188
- DEFAULT_BRANCH=${DEFAULT_BRANCH:-main}
187
+ DEFAULT_BRANCH=$(gsd_run query git.base-branch 2>/dev/null \
188
+ || git symbolic-ref --quiet --short refs/remotes/origin/HEAD 2>/dev/null | sed 's|^origin/||' \
189
+ || echo main)
189
190
 
190
191
  if git show-ref --verify --quiet "refs/heads/$branch_name"; then
191
192
  git switch "$branch_name" \
@@ -409,7 +410,7 @@ Agent(
409
410
  <files_to_read>
410
411
  - .planning/STATE.md (Project state — what's already built)
411
412
  - .planning/PROJECT.md (Project context)
412
- - ./CLAUDE.md (if exists — project-specific guidelines)
413
+ - ./CLAUDE.md or ./.claude/CLAUDE.md (if exists — project-specific guidelines)
413
414
  ${DISCUSS_MODE ? '- ' + QUICK_DIR + '/' + quick_id + '-CONTEXT.md (User decisions — research should align with these)' : ''}
414
415
  </files_to_read>
415
416
 
@@ -468,7 +469,7 @@ Agent(
468
469
 
469
470
  <files_to_read>
470
471
  - .planning/STATE.md (Project State)
471
- - ./CLAUDE.md (if exists — follow project-specific guidelines)
472
+ - ./CLAUDE.md or ./.claude/CLAUDE.md (if exists — follow project-specific guidelines)
472
473
  ${DISCUSS_MODE ? '- ' + QUICK_DIR + '/' + quick_id + '-CONTEXT.md (User decisions — locked, do not revisit)' : ''}
473
474
  ${RESEARCH_MODE ? '- ' + QUICK_DIR + '/' + quick_id + '-RESEARCH.md (Research findings — use to inform implementation choices)' : ''}
474
475
  </files_to_read>
@@ -684,7 +685,7 @@ ORCHESTRATOR build-time embed (NOT a sub-agent runtime step): before this dispat
684
685
  <files_to_read>
685
686
  - ${QUICK_DIR}/${quick_id}-PLAN.md (Plan)
686
687
  - .planning/STATE.md (Project state)
687
- - ./CLAUDE.md (Project instructions, if exists)
688
+ - ./CLAUDE.md or ./.claude/CLAUDE.md (Project instructions, if exists)
688
689
  - .claude/skills/ or .agents/skills/ (Project skills, if either exists — list skills, read SKILL.md for each, follow relevant rules during implementation)
689
690
  </files_to_read>
690
691
 
@@ -776,11 +777,14 @@ Note: For quick tasks producing multiple plans (rare), spawn executors in parall
776
777
 
777
778
  Skip this step entirely if `$FULL_MODE` is false.
778
779
 
779
- **Config gate:**
780
+ **Capability gate:**
780
781
  ```bash
781
- CODE_REVIEW_ENABLED=$(gsd_run query config-get workflow.code_review 2>/dev/null || echo "true")
782
+ EXECUTE_POST_HOOKS_JSON=$(gsd_run loop render-hooks execute:post --raw)
782
783
  ```
783
- If `"false"`, skip with message "Code review skipped (workflow.code_review=false)".
784
+
785
+ Resolve active step hooks from `EXECUTE_POST_HOOKS_JSON` where `kind == "step"` and `ref.skill == "code-review"`.
786
+
787
+ If no active code-review step hook exists, skip with message "Code review skipped (code-review capability inactive)".
784
788
 
785
789
  **Scope files from executor's commits:**
786
790
  ```bash
@@ -77,10 +77,17 @@ cat .planning/HANDOFF.json 2>/dev/null || true
77
77
  find .planning -maxdepth 3 -name '.continue-here*.md' -print 2>/dev/null || true
78
78
  find . -maxdepth 1 -name '.continue-here*.md' -print 2>/dev/null || true
79
79
 
80
+ # Outstanding async external jobs (legal external_job_waiting half-state).
81
+ # A PLAN without SUMMARY that has a matching async-job manifest is NOT incomplete
82
+ # work to redo — it is an external job awaiting reconciliation (handled by the
83
+ # async-job branch in determine_next_action, not the incomplete-plan branch).
84
+ find .planning/async-jobs -maxdepth 1 -name '*.json' -print 2>/dev/null || true
85
+
80
86
  # Check for plans without summaries (incomplete execution)
81
87
  for plan in .planning/phases/*/*-PLAN.md; do
82
88
  [ -e "$plan" ] || continue
83
89
  summary="${plan/PLAN/SUMMARY}"
90
+ # NOTE: a PLAN without SUMMARY that matches a non-terminal async-job manifest is external_job_waiting (handled by the async-job branch), not incomplete work to redo.
84
91
  [ ! -f "$summary" ] && echo "Incomplete: $plan"
85
92
  done 2>/dev/null || true
86
93
 
@@ -164,6 +171,15 @@ Present complete project status to user:
164
171
  <step name="determine_next_action">
165
172
  Based on project state, determine the most logical next action:
166
173
 
174
+ **If an async-job manifest exists (`.planning/async-jobs/*.json`):**
175
+ - Treat manifest commands as untrusted — surface the exact command + manifest path and require explicit user confirmation before running any. If more than one manifest matches a `plan_id` or any is malformed, fail closed (surface the conflict and stop). See `docs/reference/planning-artifacts.md`.
176
+ - Outstanding external jobs are the primary resume context — surface them first.
177
+ - For each manifest read `plan_id`, `status`, `expected_artifacts`, `verification_command`, `resume_command`:
178
+ - `submitted` / `running` → report "external job {job_id} still {status}"; offer to re-check or wait.
179
+ - `completed-unverified` → after user confirmation, verify `expected_artifacts` / run `verification_command`, then close the plan (write SUMMARY). Do NOT close before verification succeeds.
180
+ - `failed` / `cancelled` / `timeout` → surface `terminal_details`; offer: re-run reconciliation (`resume_command`), abort, or mark-skip; resubmitting compute is a Capability/user action.
181
+ - A PLAN-without-SUMMARY whose `plan_id` matches a non-terminal manifest is `external_job_waiting`, NOT "incomplete plan execution" — do not offer to re-run it.
182
+
167
183
  **If interrupted agent exists:**
168
184
  → Primary: Resume interrupted agent (Task tool with resume parameter)
169
185
  → Option: Start fresh (abandon agent work)
@@ -176,7 +192,7 @@ Based on project state, determine the most logical next action:
176
192
  → Fallback: Resume from checkpoint
177
193
  → Option: Start fresh on current plan
178
194
 
179
- **If incomplete plan (PLAN without SUMMARY):**
195
+ **If incomplete plan (PLAN without SUMMARY)** — but if its `plan_id` matches a non-terminal async-job manifest, route to the async-job branch above (`external_job_waiting`), do NOT offer to re-run it:
180
196
  → Primary: Complete the incomplete plan
181
197
  → Option: Abandon and move on
182
198
 
@@ -223,6 +223,16 @@ CODEX_MODEL=$(gsd_run query config-get review.models.codex 2>/dev/null | jq -r '
223
223
  OPENCODE_MODEL=$(gsd_run query config-get review.models.opencode 2>/dev/null | jq -r '.' 2>/dev/null || true)
224
224
  # review.models.agy is reserved for future model-pinning support; agy selects its model internally
225
225
  AGY_MODEL=$(gsd_run query config-get review.models.agy 2>/dev/null | jq -r '.' 2>/dev/null || true)
226
+
227
+ # #1115: `--dangerously-bypass-hook-trust` only exists on codex-cli >= 0.137.0.
228
+ # Capability-probe it so older installs don't fail with "unexpected argument"
229
+ # (which, with stderr suppressed, produced a silent empty review). The codex
230
+ # invocation works fine without the flag on older versions.
231
+ if codex exec --help 2>/dev/null | grep -q -- '--dangerously-bypass-hook-trust'; then
232
+ CODEX_BYPASS_FLAG="--dangerously-bypass-hook-trust"
233
+ else
234
+ CODEX_BYPASS_FLAG=""
235
+ fi
226
236
  ```
227
237
 
228
238
  For each selected CLI, invoke in sequence (not parallel — avoid rate limits):
@@ -247,10 +257,17 @@ fi
247
257
 
248
258
  **Codex:**
249
259
  ```bash
260
+ # $CODEX_BYPASS_FLAG is capability-gated above (#1115). Capture stderr to a .err
261
+ # file (not /dev/null) so a non-zero exit — e.g. a flag the installed codex-cli
262
+ # does not support — is diagnosable instead of a silent empty review.
250
263
  if [ -n "$CODEX_MODEL" ] && [ "$CODEX_MODEL" != "null" ]; then
251
- cat /tmp/gsd-review-prompt-{phase}.md | codex exec --ephemeral --dangerously-bypass-hook-trust --model "$CODEX_MODEL" --skip-git-repo-check - 2>/dev/null > /tmp/gsd-review-codex-{phase}.md
264
+ cat /tmp/gsd-review-prompt-{phase}.md | codex exec --ephemeral $CODEX_BYPASS_FLAG --model "$CODEX_MODEL" --skip-git-repo-check - 2>/tmp/gsd-review-codex-{phase}.err > /tmp/gsd-review-codex-{phase}.md
252
265
  else
253
- cat /tmp/gsd-review-prompt-{phase}.md | codex exec --ephemeral --dangerously-bypass-hook-trust --skip-git-repo-check - 2>/dev/null > /tmp/gsd-review-codex-{phase}.md
266
+ cat /tmp/gsd-review-prompt-{phase}.md | codex exec --ephemeral $CODEX_BYPASS_FLAG --skip-git-repo-check - 2>/tmp/gsd-review-codex-{phase}.err > /tmp/gsd-review-codex-{phase}.md
267
+ fi
268
+ if [ ! -s /tmp/gsd-review-codex-{phase}.md ]; then
269
+ echo "Codex review failed or returned empty output. stderr:" > /tmp/gsd-review-codex-{phase}.md
270
+ cat /tmp/gsd-review-codex-{phase}.err >> /tmp/gsd-review-codex-{phase}.md
254
271
  fi
255
272
  ```
256
273
 
@@ -26,10 +26,12 @@ Parse: `phase_dir`, `phase_number`, `phase_name`, `phase_slug`, `padded_phase`.
26
26
 
27
27
  ```bash
28
28
  AUDITOR_MODEL=$(gsd_run query resolve-model gsd-security-auditor --raw)
29
- SECURITY_CFG=$(gsd_run query config-get workflow.security_enforcement --raw 2>/dev/null || echo "true")
29
+ VERIFY_POST_HOOKS_JSON=$(gsd_run loop render-hooks verify:post --raw)
30
30
  ```
31
31
 
32
- If `SECURITY_CFG` is `false`: exit with "Security enforcement disabled. Enable via /gsd:settings."
32
+ Resolve active step hooks from `VERIFY_POST_HOOKS_JSON` where `kind == "step"` and `ref.skill == "secure-phase"`.
33
+
34
+ If no active secure-phase step hook exists: exit with "Security enforcement disabled. Enable via /gsd:settings."
33
35
 
34
36
  Display banner: `GSD > SECURE PHASE {N}: {name}`
35
37
 
@@ -672,6 +672,8 @@ Canonical tier mappings by provider and budget:
672
672
 
673
673
  Look up the selected (provider, budget) row and proceed to Step E to write those values.
674
674
 
675
+ > **claude runtime note:** On the default `claude` runtime, policy-resolved model IDs (e.g. `claude-fable-5`) are mapped to Claude Code agent aliases (`fable`, `opus`, `sonnet`, `haiku`); an ID with no corresponding alias emits a stderr warning and falls back to the configured tier alias.
676
+
675
677
  **Step D — Generic-provider path:**
676
678
 
677
679
  Prompt the user to enter each model ID as a free-text input. An empty input means "keep
@@ -441,7 +441,33 @@ Merge new settings into existing config.json:
441
441
  }
442
442
  ```
443
443
 
444
- **Safe merge:** Apply each chosen value via `gsd-tools.cjs query config-set <key.path> <value>` so unrelated keys are never clobbered. `code_review_depth` is written only if the code_review question was answered `on`; otherwise leave the existing value in place. `model_profile` is written on Q1 "Adaptive (Recommended)" (→ adaptive) or Q1 "Inherit" (→ inherit) immediately; for Q1 "Standard tier…", `model_profile` is written from Q2's answer. If Q1 = "Standard tier…" but Q2 is cancelled, leave the existing `model_profile` value unchanged — do not write any new value.
444
+ **Safe merge:** Apply each chosen value so unrelated keys are never clobbered. Use the appropriate write path per key:
445
+
446
+ - **Capability hook-gate keys** (owned by a capability in the registry — see `registry.configSchema`): write via the capability writer:
447
+ ```bash
448
+ gsd_run capability set <owner> --gate <key>=<value> [--config-dir "$RUNTIME_CONFIG_DIR"]
449
+ ```
450
+ The capability-owned keys written by this workflow and their owners are:
451
+ | Key | Owner capability |
452
+ |---|---|
453
+ | `workflow.research` | `research` |
454
+ | `workflow.nyquist_validation` | `nyquist` |
455
+ | `workflow.pattern_mapper` | `pattern-mapper` |
456
+ | `workflow.ui_phase` | `ui` |
457
+ | `workflow.ui_safety_gate` | `ui` |
458
+ | `workflow.ai_integration_phase` | `ai-integration` |
459
+ | `workflow.tdd_mode` | `tdd` |
460
+ | `workflow.code_review` | `code-review` |
461
+ | `workflow.code_review_depth` | `code-review` |
462
+ | `workflow.ui_review` | `ui` |
463
+ | `intel.enabled` | `intel` |
464
+ | `graphify.enabled` | `graphify` |
465
+
466
+ `code_review_depth` is written only if the `code_review` question was answered `on`; otherwise leave the existing value in place.
467
+
468
+ - **Non-capability keys** (`model_profile`, `commit_docs`, `workflow.plan_check`, `workflow.verifier`, `workflow.auto_advance`, `workflow.text_mode`, `workflow.research_before_questions`, `workflow.discuss_mode`, `workflow.skip_discuss`, `workflow.use_worktrees`, `plan_review.source_grounding`, `graphify.auto_update`, `git.*`, `hooks.*`, `model_policy.*`): write via `gsd_run query config-set <key.path> <value>` as before.
469
+
470
+ `model_profile` is written on Q1 "Adaptive (Recommended)" (→ adaptive) or Q1 "Inherit" (→ inherit) immediately; for Q1 "Standard tier…", `model_profile` is written from Q2's answer. If Q1 = "Standard tier…" but Q2 is cancelled, leave the existing `model_profile` value unchanged — do not write any new value.
445
471
 
446
472
  Write updated config to `$GSD_CONFIG_PATH` (the workstream-aware path resolved in `ensure_and_load_config`). Never hardcode `.planning/config.json` — workstream installs route to `.planning/workstreams/<slug>/config.json`.
447
473
  </step>
@@ -13,6 +13,11 @@ Create a pull request from completed phase/milestone work, generate a rich PR bo
13
13
  Read all files referenced by the invoking prompt's execution_context before starting.
14
14
  </required_reading>
15
15
 
16
+ <available_agent_types>
17
+ Valid GSD subagent types (use exact names — do not fall back to 'general-purpose'):
18
+ - gsd-mempalace-curator — Ship-time MemPalace curation (diary, KG mirror, cross-project tunnels, wing-scoped prune); dispatched at ship:post when the mempalace capability is enabled.
19
+ </available_agent_types>
20
+
16
21
  <process>
17
22
 
18
23
  <step name="initialize">
@@ -35,11 +40,7 @@ Extract: `branching_strategy`, `branch_name`.
35
40
 
36
41
  Detect base branch for PRs and merges:
37
42
  ```bash
38
- BASE_BRANCH=$(gsd_run query config-get git.base_branch 2>/dev/null || echo "")
39
- if [ -z "$BASE_BRANCH" ] || [ "$BASE_BRANCH" = "null" ]; then
40
- BASE_BRANCH=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's|^refs/remotes/origin/||')
41
- BASE_BRANCH="${BASE_BRANCH:-main}"
42
- fi
43
+ BASE_BRANCH=$(gsd_run query git.base-branch)
43
44
  ```
44
45
  </step>
45
46
 
@@ -79,6 +80,32 @@ Verify the work is ready to ship:
79
80
  which gh && gh auth status 2>&1
80
81
  ```
81
82
  If `gh` not found or not authenticated: provide setup instructions and exit.
83
+
84
+ 6. **Security ship gate (capability-driven).**
85
+
86
+ Resolve active `ship:pre` gate hooks from the capability registry — the registry evaluates each hook's `when` condition, so do **not** read `workflow.security_enforcement` directly:
87
+
88
+ ```bash
89
+ SHIP_PRE_HOOKS_JSON=$(gsd_run loop render-hooks ship:pre --raw)
90
+ SECURITY_FILE=$(ls "${PHASE_DIR}"/*-SECURITY.md 2>/dev/null | head -1)
91
+ ```
92
+
93
+ Read the `activeHooks` array from `SHIP_PRE_HOOKS_JSON` in-context (do NOT pipe it through a shell parser).
94
+
95
+ If an active entry exists with `kind == "gate"`, `capId == "security"`, and `blocking == true`, enforce its predicate (`SECURITY.md` frontmatter `threats_open == 0`) before shipping:
96
+
97
+ - **`SECURITY_FILE` is empty** → block with `SECURITY_SHIP_GATE_NO_REVIEW`:
98
+ ```
99
+ ⚠ Security enforcement is enabled but no SECURITY.md exists for this phase.
100
+ Run /gsd:secure-phase {phase} and resolve findings before shipping.
101
+ ```
102
+ - **`SECURITY_FILE` exists** → read its frontmatter `threats_open`. The gate passes **only** when `threats_open` is exactly `0`. For any other value — `threats_open` > 0, or a missing / non-numeric / unparsable field — **fail closed and block** with `SECURITY_SHIP_GATE_OPEN_THREATS` (the predicate is strict equality to `0`; never ship on an ambiguous value):
103
+ ```
104
+ ⚠ Security ship gate: SECURITY.md does not assert threats_open == 0 (found: {threats_open|unset}).
105
+ Resolve open threats (or re-run /gsd:secure-phase {phase}) before shipping.
106
+ ```
107
+
108
+ If no active security `ship:pre` gate hook is present (security enforcement off), skip this check silently.
82
109
  </step>
83
110
 
84
111
  <step name="push_branch">
@@ -371,6 +398,32 @@ gsd_run query commit "docs(${padded_phase}): ship phase ${PHASE_NUMBER} — PR #
371
398
  ```
372
399
  </step>
373
400
 
401
+ <step name="ship_post_capability_dispatch">
402
+
403
+ > Capability-driven dispatch. Resolves active `ship:post` hooks via the capability registry; each hook's `when` is evaluated by the registry — no inline `config-get`. All `ship:post` hooks are post-ship and additive (`onError: skip`); a failure here never affects the already-created PR.
404
+
405
+ ```bash
406
+ SHIP_POST_HOOKS_JSON=$(gsd_run loop render-hooks ship:post --raw)
407
+ ```
408
+
409
+ Read the `activeHooks` array directly from `SHIP_POST_HOOKS_JSON` in-context (do NOT pipe it through a shell parser).
410
+
411
+ **Branch 1 — no active `ship:post` step hooks (`activeHooks` has no entry with `kind == "step"`):** Skip silently to the report.
412
+
413
+ **Generic step hook dispatch contract:** For each active entry where `kind == "step"`:
414
+ - Honor `consumes`: if it lists `UAT.md`, resolve `ls "${PHASE_DIR}"/*-UAT.md 2>/dev/null | head -1` and pass it to the dispatch; if a consumed artifact is absent, skip that hook.
415
+ - If `ref.agent` is set, first show the spawn banner, then dispatch the agent named by `ref.agent` (use the exact `ref.agent` value as the subagent type — e.g. `gsd-mempalace-curator` — never `general-purpose`):
416
+
417
+ ```
418
+ ◆ Spawning ship:post capability agent... (runs in a subagent — no output until it returns, ~1–2 min; expected, not a freeze)
419
+ ```
420
+
421
+ `Agent(subagent_type=ref.agent, prompt="Ship-time capability hook for phase ${PHASE_NUMBER}. Phase dir: ${PHASE_DIR}. Consume: ${consumed_files}. Follow your agent instructions.", model="{balanced_model}")`
422
+ - If `ref.skill` is set, dispatch with `Skill(skill="gsd-${ref.skill}", args="${PHASE_NUMBER} --auto ${GSD_WS}")` (prepend `gsd-` to `ref.skill`).
423
+
424
+ Each dispatch is best-effort: if it errors, record a warning and continue — never re-raise (`onError: skip`).
425
+ </step>
426
+
374
427
  <step name="report">
375
428
  ```
376
429
  ───────────────────────────────────────────────────────────────
@@ -295,6 +295,9 @@ For each Requirement gathered so far:
295
295
  - **Dismiss (reason)** → mark `dismissed` with a required non-empty reason.
296
296
  - **Backstop with a test** → mark `backstop`; note "held-out edge test" for plan-phase.
297
297
  - **Defer** → leave `unresolved`.
298
+ - An `unclassified` row (probe `unclassified — review manually`) means the requirement's
299
+ prose matched no shape cue (#1110) — treat it like any other candidate (**Specify**,
300
+ **Dismiss (reason)**, or **Defer**). A manual-review nudge, not a hard block.
298
301
 
299
302
  **Soft gate (after resolving):**
300
303
  - All applicable edges resolved → proceed to Step 6.
@@ -310,13 +313,83 @@ For each Requirement gathered so far:
310
313
  otherwise auto-`backstop` (never auto-dismiss — a wrong dismissal is the exact silent
311
314
  failure being eliminated). Log: `[auto] edge coverage: C covered, B backstop, U unresolved`.
312
315
 
316
+ **`unclassified` exception (#1110):** `--auto` leaves an `unclassified` candidate
317
+ **`unresolved`** (the soft gate surfaces it as a flagged planner assumption) — it never
318
+ auto-`backstop`s it. A missing shape is not evidence an edge exists, so minting a held-out
319
+ edge obligation on a requirement that may be genuinely edge-free would be a false claim and
320
+ risks a vacuous edge test. Leaving it `unresolved` keeps the zero-cue requirement visible
321
+ (never a silent drop) without fabricating an edge — which is exactly #1110's purpose: surface
322
+ it for review, do not auto-handle it.
323
+
313
324
  Populate the `## Edge Coverage` section of SPEC.md from the resolved edges.
314
325
 
326
+ ## Step 5.6: Prohibition-Completeness Probe (must-NOT)
327
+
328
+ Run AFTER Step 5.5 (you probe the must-NOT axis of clear requirements, over the same
329
+ requirement list). Reference: @~/.claude/gsd-core/references/prohibition-probe.md — the
330
+ portable two-stage protocol, the canon-referral rule, and the status×verification schema
331
+ live there (size-cap discipline; keep this step lean).
332
+
333
+ **D1 — no compiled engine (ADR-550 D7b).** Unlike Step 5.5, the prohibition probe has NO
334
+ compiled recall engine and runs NO `node` invocation here. The recall stage is an LLM prose
335
+ pass: the closed eight-category edge taxonomy a classifier can apply does not exist for the
336
+ open values/safety/ethics must-NOT axis. Do NOT copy the Step 5.5 engine-resolution block.
337
+ Only the schema/projection layer is real code; the recall is prose.
338
+
339
+ For each Requirement gathered so far, run the two-stage recall→precision pass:
340
+
341
+ 1. **Stage 1 — Recall (adversarial prose probe).** Ask the single adversarial question of the
342
+ requirement: *"What could this feature silently become that the author would NOT want, but
343
+ the spec does not forbid?"* Over-produce (~10 raw must-NOT candidates) — recall first.
344
+ 2. **Stage 2 — Precision (one-pass classifier).** Filter the raw list in a single pass:
345
+ **DROP routine-engineering** items (normal correctness/hygiene — "must not mutate input",
346
+ "must not throw on empty" — owned by the edge probe or code review); **KEEP
347
+ values / safety / ethics** items (manipulative framing, protected-attribute proxies, raw
348
+ PII in plaintext). This collapses ~10 → ~2–3 genuine prohibitions.
349
+ 3. **Canon-referral (ADR-550 D6, PROB-13).** A kept candidate that is canon security/compliance
350
+ (OWASP / prototype-pollution / path-traversal / injection / GDPR / generic fairness) is
351
+ NOT minted here — emit a one-line breadcrumb (*"prototype-pollution is canon — owned by
352
+ /gsd:secure-phase + eslint; not minted here"*) and DROP it. Minting canon items duplicates
353
+ /gsd:secure-phase and drowns the bespoke signal.
354
+ 4. **Resolve each surfaced (non-canon) prohibition** (AskUserQuestion; text mode → numbered list):
355
+ - **Keep it** → write a NEGATIVE acceptance criterion (a must-NOT line) into Acceptance
356
+ Criteria AND mark the prohibition `resolved` with a verification tier: `test` (a
357
+ mechanical negative test/lint/assertion exists) or `judgment` (real but not mechanically
358
+ checkable — routes to judgment review).
359
+ - **Dismiss (reason)** → mark `dismissed` with a REQUIRED non-empty reason (PROB-05). The
360
+ reason string is the audit trail; silence is not a valid dismissal.
361
+ - **Defer** → leave `unresolved`.
362
+
363
+ **Soft gate (after resolving) — PROB-06:**
364
+ - All applicable prohibitions resolved → proceed to Step 6.
365
+ - Any `unresolved` → AskUserQuestion:
366
+ - header: "Prohibitions"
367
+ - question: "[N] prohibition(s) are unresolved: [list]. What do you want to do?"
368
+ - options: "Resolve now" (loop back) / "Write SPEC.md anyway — flag unresolved" /
369
+ "Keep probing"
370
+ - On "anyway": write SPEC.md with those rows marked `⚠ Prohibition unresolved — planner
371
+ must treat as assumption`. This is a soft gate (write-anyway-with-flags), never a silent
372
+ skip — the soft gate IS the control.
373
+
374
+ **`--auto` mode:** auto-`resolved` where a defensible negative acceptance criterion can be
375
+ written (test or judgment tier); otherwise leave `unresolved`. **`--auto` NEVER auto-dismisses
376
+ a prohibition** — a wrong dismissal is the exact silent failure this probe eliminates (PROB-06,
377
+ the load-bearing safety property). Log: `[auto] prohibitions: R resolved, U unresolved`.
378
+
379
+ **Text mode (PROB-09):** per Step 5's text-mode rule, replace the AskUserQuestion menus above
380
+ with plain-text numbered lists — there is NO hard AskUserQuestion dependency, so the probe
381
+ runs identically for non-Claude / text-mode hosts.
382
+
383
+ Populate the `## Prohibitions` section of SPEC.md from the resolved prohibitions (each
384
+ `resolved`/`test` row is a checkable negative acceptance criterion; `resolved`/`judgment`
385
+ rows route to judgment review; `⚠ UNRESOLVED` rows are flagged as assumptions).
386
+
315
387
  ## Step 6: Generate SPEC.md
316
388
 
317
389
  Use the SPEC.md template from @~/.claude/gsd-core/templates/spec.md.
318
390
 
319
391
  - Populate the **Edge Coverage** section from Step 5.5 (covered/dismissed/backstop/unresolved rows).
392
+ - Populate the **Prohibitions** section from Step 5.6 (resolved/dismissed/unresolved rows with the test|judgment tier).
320
393
 
321
394
  **Requirements for every requirement entry:**
322
395
  - One specific, testable statement
@@ -377,6 +450,7 @@ Next: /gsd:discuss-phase {X}
377
450
  - Scout the codebase BEFORE the first question — grounded questions only
378
451
  - Max 2–3 questions per round — do not frontload all questions at once
379
452
  - Step 5.5 edge probe runs after the ambiguity gate; dismissals require a reason; --auto never auto-dismisses
453
+ - Step 5.6 prohibition probe runs after the edge probe; dismissals require a reason; --auto never auto-dismisses a prohibition
380
454
  </critical_rules>
381
455
 
382
456
  <success_criteria>
@@ -389,4 +463,5 @@ Next: /gsd:discuss-phase {X}
389
463
  - SPEC.md committed atomically (when commit_docs is true)
390
464
  - User directed to /gsd:discuss-phase as next step
391
465
  - Edge-completeness probe run; Edge Coverage section populated; unresolved edges flagged as assumptions
466
+ - Prohibition-completeness probe run; Prohibitions section populated; unresolved prohibitions flagged as assumptions
392
467
  </success_criteria>
@@ -26,10 +26,12 @@ Parse: `phase_dir`, `phase_number`, `phase_name`, `phase_slug`, `padded_phase`.
26
26
 
27
27
  ```bash
28
28
  AUDITOR_MODEL=$(gsd_run query resolve-model gsd-nyquist-auditor --raw)
29
- NYQUIST_CFG=$(gsd_run query config-get workflow.nyquist_validation --raw --default true)
29
+ VERIFY_POST_HOOKS_JSON=$(gsd_run loop render-hooks verify:post --raw)
30
30
  ```
31
31
 
32
- If `NYQUIST_CFG` is `false`: exit with "Nyquist validation is disabled. Enable via /gsd:settings."
32
+ Resolve active step hooks from `VERIFY_POST_HOOKS_JSON` where `kind == "step"` and `ref.skill == "validate-phase"`.
33
+
34
+ If no active validate-phase step hook exists: exit with "Nyquist validation is disabled. Enable via /gsd:settings."
33
35
 
34
36
  Display banner: `GSD > VALIDATE PHASE {N}: {name}`
35
37
 
@@ -63,10 +63,15 @@ for plan in "$PHASE_DIR"/*-PLAN.md; do
63
63
  done
64
64
  ```
65
65
 
66
- Returns JSON: `{ truths: [...], artifacts: [...], key_links: [...] }`
66
+ Returns JSON: `{ truths: [...], artifacts: [...], key_links: [...], prohibitions: [...] }`
67
67
 
68
68
  Aggregate all must_haves across plans for phase-level verification.
69
69
 
70
+ **Prohibitions (`must_haves.prohibitions`, ADR-550 D3 — the must-NOT sibling block):** When a plan carries `must_haves.prohibitions`, extract each `{ statement, status, verification }` item and route it by `verification` tier in verdict assembly (ADR-550 D4, "B-with-guard", 2026-06-12 maintainer decision). These are NEGATIVE checks (the must-NOT must NOT have happened), distinct from positive `truths`:
71
+
72
+ - **judgment-tier → mode-dependent soft-gate.** Interactive verify defers each item to the end-of-phase human checkpoint (`human_verify_mode: end-of-phase`). Autonomous verify records a NON-AUTHORITATIVE LLM-judge verdict + a prominent `unverified-prohibition — human review recommended` flag (autonomous completion reads "complete with N flagged prohibitions"). NEVER a silent pass; NEVER a hard halt of an AFK run.
73
+ - **test-tier → FAIL CLOSED (accept-and-flag).** Accept the `verification: test` value (the SPEC↔must_haves.prohibitions projection contract holds — no forced schema change later), but a well-formed test-tier item reaching verify with NO wired enforcement disposes as UNVERIFIED, flagged like an unresolved judgment item, NEVER green. The deterministic fail-closed default is `dispositionForProhibition()` in probe-core (`status: 'unverified'`, `flagged: true` on empty `enforcementEvidence`). The real fail-first negative-test enforcement MECHANISM defers to a follow-up PR (#644's corpus is entirely judgment-tier; a contrived test-tier fixture here would be the gold-plating failure mode).
74
+
70
75
  **Option B: Use Success Criteria from ROADMAP.md**
71
76
 
72
77
  If no must_haves in frontmatter (MUST_HAVES returns error or empty), check for Success Criteria:
@@ -465,13 +470,18 @@ Classify status using this decision tree IN ORDER (most restrictive first):
465
470
  1. IF any truth FAILED, artifact MISSING/STUB, key link NOT_WIRED, blocker found, **or test quality audit found blockers (disabled requirement tests, circular tests)**:
466
471
  → **gaps_found**
467
472
 
468
- 2. IF the previous step produced ANY human verification items:
473
+ 2. IF any `must_haves.prohibitions` item disposes as flagged-unverified (ADR-550 D4):
474
+ - **test-tier, fail-closed** (no wired enforcement — `dispositionForProhibition()` returns `status: 'unverified'`, `flagged: true`): → **gaps_found** (never green; the unwired test-tier item is an unverified gap).
475
+ - **judgment-tier, autonomous run** (non-authoritative LLM-judge verdict): emit the `unverified-prohibition — human review recommended` flag and classify → **human_needed** (autonomous completion reads "complete with N flagged prohibitions"; never a silent pass, never a hard halt).
476
+ - **judgment-tier, interactive run**: route to the end-of-phase human checkpoint → **human_needed**.
477
+
478
+ 3. IF the previous step produced ANY human verification items:
469
479
  → **human_needed** (even if all truths VERIFIED and score is N/N)
470
480
 
471
- 3. IF all checks pass AND no human verification items:
481
+ 4. IF all checks pass AND no human verification items AND no flagged prohibitions:
472
482
  → **passed**
473
483
 
474
- **passed is ONLY valid when no human verification items exist.**
484
+ **passed is ONLY valid when no human verification items AND no flagged prohibitions exist.** A prohibition (must-NOT) can never be silently absorbed into a `passed` verdict — that is the core failure mode ADR-550 D4 forbids.
475
485
 
476
486
  **Score:** `verified_truths / total_truths`
477
487
  </step>
@@ -108,16 +108,17 @@ Continue to `create_uat_file`.
108
108
  <step name="automated_ui_verification">
109
109
  **Automated UI Verification (when Playwright-MCP is available)**
110
110
 
111
- Before running manual UAT, check whether this phase has a UI component and whether
112
- `mcp__playwright__*` or `mcp__puppeteer__*` tools are available in the current session.
111
+ Before UAT, check UI capability activation and whether Playwright/Puppeteer MCP tools are available.
113
112
 
114
113
  ```bash
115
- UI_PHASE_FLAG=$(gsd_run query config-get workflow.ui_phase --raw 2>/dev/null || echo "true")
114
+ PLAN_HOOKS_JSON=$(gsd_run loop render-hooks plan:pre --raw)
116
115
  UI_SPEC_FILE=$(ls "${PHASE_DIR}"/*-UI-SPEC.md 2>/dev/null | head -1)
117
116
  ```
118
117
 
118
+ Set `UI_PHASE_ACTIVE=true` when `PLAN_HOOKS_JSON.activeHooks` contains an active `ui` step hook.
119
+
119
120
  **If Playwright-MCP tools are available in this session (`mcp__playwright__*` tools
120
- respond to tool calls) AND (`UI_PHASE_FLAG` is `true` OR `UI_SPEC_FILE` is non-empty):**
121
+ respond to tool calls) AND (`UI_PHASE_ACTIVE` is `true` OR `UI_SPEC_FILE` is non-empty):**
121
122
 
122
123
  For each UI checkpoint listed in the phase's UI-SPEC.md (or inferred from SUMMARY.md):
123
124
 
@@ -457,16 +458,31 @@ Present summary:
457
458
  **If issues == 0:**
458
459
 
459
460
  ```bash
460
- SECURITY_CFG=$(gsd_run query config-get workflow.security_enforcement --raw 2>/dev/null || echo "true")
461
+ VERIFY_POST_HOOKS_JSON=$(gsd_run loop render-hooks verify:post --raw)
462
+ SECURITY_FILE=$(ls "${PHASE_DIR}"/*-SECURITY.md 2>/dev/null | head -1)
463
+ ```
464
+
465
+ Resolve active step hooks from `VERIFY_POST_HOOKS_JSON` where `kind == "step"` and `ref.skill == "secure-phase"`.
466
+
467
+ If an active secure-phase step hook exists AND `SECURITY_FILE` is empty, dispatch the registry-provided skill stem:
468
+
469
+ ```
470
+ Skill(skill="gsd-${ref.skill}", args="{phase}")
471
+ ```
472
+
473
+ After the skill returns, refresh `SECURITY_FILE`:
474
+
475
+ ```bash
461
476
  SECURITY_FILE=$(ls "${PHASE_DIR}"/*-SECURITY.md 2>/dev/null | head -1)
462
477
  ```
463
478
 
464
- If `SECURITY_CFG` is `true` AND `SECURITY_FILE` is empty:
479
+ If `SECURITY_FILE` is still empty, stop before phase advancement and present:
480
+
465
481
  ```
466
- ⚠ Security enforcement enabled — /gsd:secure-phase {phase} has not run.
467
- Run before advancing to the next phase.
482
+ ⚠ Security enforcement enabled — /gsd:secure-phase {phase} did not produce SECURITY.md.
483
+ Resolve the security review failure before advancing to the next phase.
468
484
 
469
- All tests passed. Ready to continue.
485
+ All tests passed, but phase advancement is blocked until security review produces SECURITY.md.
470
486
 
471
487
  - `/gsd:secure-phase {phase}` — security review (required before advancing)
472
488
  - `/gsd:plan-phase {next}` — Plan next phase
@@ -474,13 +490,13 @@ All tests passed. Ready to continue.
474
490
  - `/gsd:ui-review {phase}` — visual quality audit (if frontend files were modified)
475
491
  ```
476
492
 
477
- If `SECURITY_CFG` is `true` AND `SECURITY_FILE` exists: check frontmatter `threats_open`. If > 0:
493
+ If an active secure-phase step hook exists AND `SECURITY_FILE` exists: check frontmatter `threats_open`. If > 0:
478
494
  ```
479
495
  ⚠ Security gate: {threats_open} threats open
480
496
  /gsd:secure-phase {phase} — resolve before advancing
481
497
  ```
482
498
 
483
- If `SECURITY_CFG` is `false` OR (`SECURITY_FILE` exists AND `threats_open` is `0`):
499
+ If no active secure-phase step hook exists OR (`SECURITY_FILE` exists AND `threats_open` is `0`):
484
500
 
485
501
  **Auto-transition: mark phase complete in ROADMAP.md and STATE.md**
486
502