@opengsd/gsd-core 1.9.0 → 1.10.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 (223) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +2 -3
  3. package/.opencode/plugins/gsd-core.js +8 -1
  4. package/agents/gsd-code-fixer.md +131 -34
  5. package/agents/gsd-debugger.md +12 -246
  6. package/agents/gsd-executor.md +7 -5
  7. package/agents/gsd-integration-checker.md +3 -0
  8. package/agents/gsd-plan-checker.md +9 -0
  9. package/agents/gsd-planner.md +5 -8
  10. package/agents/gsd-roadmapper.md +21 -3
  11. package/agents/gsd-verifier.md +14 -70
  12. package/bin/install.js +503 -341
  13. package/commands/gsd/mempalace-capture.md +1 -1
  14. package/commands/gsd/new-milestone.md +1 -1
  15. package/commands/gsd/plan-phase.md +1 -1
  16. package/gsd-core/bin/gsd-tools.cjs +607 -63
  17. package/gsd-core/bin/lib/active-workstream-store.cjs +25 -0
  18. package/gsd-core/bin/lib/agent-install-check.cjs +38 -6
  19. package/gsd-core/bin/lib/api-coverage.cjs +120 -0
  20. package/gsd-core/bin/lib/audit.cjs +89 -1
  21. package/gsd-core/bin/lib/broken-windows.cjs +36 -6
  22. package/gsd-core/bin/lib/capability-registry.cjs +96 -110
  23. package/gsd-core/bin/lib/capability-validator.cjs +12 -2
  24. package/gsd-core/bin/lib/check-command-router.cjs +43 -1
  25. package/gsd-core/bin/lib/command-aliases.cjs +72 -0
  26. package/gsd-core/bin/lib/commands.cjs +26 -25
  27. package/gsd-core/bin/lib/commonjs-marker.cjs +136 -0
  28. package/gsd-core/bin/lib/config-loader.cjs +1 -0
  29. package/gsd-core/bin/lib/config.cjs +12 -1
  30. package/gsd-core/bin/lib/context-composer.cjs +278 -0
  31. package/gsd-core/bin/lib/context-predicates.cjs +506 -0
  32. package/gsd-core/bin/lib/core-utils.cjs +91 -12
  33. package/gsd-core/bin/lib/docs.cjs +3 -2
  34. package/gsd-core/bin/lib/external-job.cjs +19 -4
  35. package/gsd-core/bin/lib/frontmatter.cjs +84 -12
  36. package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +57 -6
  37. package/gsd-core/bin/lib/git-base-branch.cjs +58 -15
  38. package/gsd-core/bin/lib/graphify.cjs +142 -27
  39. package/gsd-core/bin/lib/gsd2-import.cjs +27 -4
  40. package/gsd-core/bin/lib/host-integration.cjs +13 -1
  41. package/gsd-core/bin/lib/init-command-router.cjs +83 -8
  42. package/gsd-core/bin/lib/init.cjs +1021 -57
  43. package/gsd-core/bin/lib/install-engine.cjs +64 -10
  44. package/gsd-core/bin/lib/install-profiles.cjs +27 -1
  45. package/gsd-core/bin/lib/installer-migration-authoring.cjs +3 -1
  46. package/gsd-core/bin/lib/installer-migration-report.cjs +4 -0
  47. package/gsd-core/bin/lib/installer-migrations/007-retire-config-root-commonjs-marker.cjs +149 -0
  48. package/gsd-core/bin/lib/installer-migrations/008-cursor-retire-commands-surface.cjs +55 -0
  49. package/gsd-core/bin/lib/installer-migrations/009-pi-retire-reserved-hooks-dir.cjs +199 -0
  50. package/gsd-core/bin/lib/installer-migrations.cjs +87 -1
  51. package/gsd-core/bin/lib/io.cjs +28 -3
  52. package/gsd-core/bin/lib/markdown-sectionizer.cjs +6 -0
  53. package/gsd-core/bin/lib/mcp-catalog.cjs +518 -0
  54. package/gsd-core/bin/lib/mcp-server.cjs +135 -3
  55. package/gsd-core/bin/lib/milestone.cjs +106 -51
  56. package/gsd-core/bin/lib/phase-id.cjs +63 -0
  57. package/gsd-core/bin/lib/phase-locator.cjs +138 -45
  58. package/gsd-core/bin/lib/phase.cjs +260 -25
  59. package/gsd-core/bin/lib/plan-dependency-graph.cjs +232 -0
  60. package/gsd-core/bin/lib/planning-workspace.cjs +4 -0
  61. package/gsd-core/bin/lib/project-root.cjs +48 -0
  62. package/gsd-core/bin/lib/prompt-budget.cjs +128 -165
  63. package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +80 -0
  64. package/gsd-core/bin/lib/review-lane-descriptor.cjs +99 -0
  65. package/gsd-core/bin/lib/review-lane-runner.cjs +30 -6
  66. package/gsd-core/bin/lib/roadmap-command-router.cjs +42 -9
  67. package/gsd-core/bin/lib/roadmap-parser.cjs +100 -18
  68. package/gsd-core/bin/lib/roadmap.cjs +37 -7
  69. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +195 -62
  70. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +15 -3
  71. package/gsd-core/bin/lib/runtime-homes.cjs +154 -41
  72. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +105 -41
  73. package/gsd-core/bin/lib/section-manifest.cjs +209 -0
  74. package/gsd-core/bin/lib/shell-command-projection.cjs +113 -27
  75. package/gsd-core/bin/lib/smart-entry.cjs +12 -0
  76. package/gsd-core/bin/lib/state-transition.cjs +73 -8
  77. package/gsd-core/bin/lib/state.cjs +151 -62
  78. package/gsd-core/bin/lib/surface.cjs +12 -1
  79. package/gsd-core/bin/lib/uat-predicate.cjs +11 -1
  80. package/gsd-core/bin/lib/uat.cjs +320 -21
  81. package/gsd-core/bin/lib/unusable-input.cjs +9 -0
  82. package/gsd-core/bin/lib/verification.cjs +29 -12
  83. package/gsd-core/bin/lib/verify.cjs +29 -5
  84. package/gsd-core/bin/lib/workflow-fragments.cjs +557 -0
  85. package/gsd-core/bin/lib/workstream-inventory-builder.cjs +181 -18
  86. package/gsd-core/bin/lib/workstream-inventory.cjs +519 -27
  87. package/gsd-core/bin/lib/workstream.cjs +6 -0
  88. package/gsd-core/bin/lib/worktree-base-ref.cjs +50 -6
  89. package/gsd-core/bin/lib/worktree-safety.cjs +276 -118
  90. package/gsd-core/bin/shared/config-schema.manifest.json +2 -0
  91. package/gsd-core/references/artifact-types.md +10 -3
  92. package/gsd-core/references/autonomous-ui-design-contract.md +42 -0
  93. package/gsd-core/references/debugger-techniques.md +255 -0
  94. package/gsd-core/references/research-documentation-lookup.md +5 -3
  95. package/gsd-core/references/specless-probe-fallback.md +7 -6
  96. package/gsd-core/references/verifier-wiring-patterns.md +100 -0
  97. package/gsd-core/references/worktree-branch-check.md +2 -2
  98. package/gsd-core/templates/summary-complex.md +2 -0
  99. package/gsd-core/templates/summary-minimal.md +2 -0
  100. package/gsd-core/templates/summary-standard.md +2 -0
  101. package/gsd-core/templates/summary.md +2 -0
  102. package/gsd-core/workflows/audit-milestone.md +3 -0
  103. package/gsd-core/workflows/autonomous/steps/converge-banner.md +1 -0
  104. package/gsd-core/workflows/autonomous/steps/converge-dispatch-bg.md +11 -0
  105. package/gsd-core/workflows/autonomous/steps/converge-dispatch-inline.md +7 -0
  106. package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +21 -0
  107. package/gsd-core/workflows/autonomous/steps/converge-loop.md +7 -0
  108. package/gsd-core/workflows/autonomous.md +32 -69
  109. package/gsd-core/workflows/code-review/steps/dispatch-fix.md +39 -0
  110. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +83 -0
  111. package/gsd-core/workflows/code-review.md +42 -145
  112. package/gsd-core/workflows/complete-milestone/steps/git-tag.md +29 -0
  113. package/gsd-core/workflows/complete-milestone.md +23 -81
  114. package/gsd-core/workflows/debug.md +9 -12
  115. package/gsd-core/workflows/diagnose-issues.md +22 -0
  116. package/gsd-core/workflows/discovery-phase.md +4 -4
  117. package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +15 -0
  118. package/gsd-core/workflows/discuss-phase-assumptions.md +5 -16
  119. package/gsd-core/workflows/docs-update/steps/dispatch-monorepo-packages.md +51 -0
  120. package/gsd-core/workflows/docs-update.md +8 -51
  121. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +34 -2
  122. package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +50 -0
  123. package/gsd-core/workflows/execute-phase/steps/partial-wave.md +31 -0
  124. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +19 -0
  125. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +42 -0
  126. package/gsd-core/workflows/execute-phase/steps/regression-gate.md +43 -37
  127. package/gsd-core/workflows/execute-phase.md +65 -137
  128. package/gsd-core/workflows/execute-plan.md +1 -1
  129. package/gsd-core/workflows/help/modes/full.md +6 -1
  130. package/gsd-core/workflows/ingest-docs.md +2 -1
  131. package/gsd-core/workflows/new-milestone/steps/project-md-milestone-write.md +16 -0
  132. package/gsd-core/workflows/new-milestone/steps/reset-phase-safety.md +19 -0
  133. package/gsd-core/workflows/new-milestone.md +21 -38
  134. package/gsd-core/workflows/new-project/steps/auto-mode-config.md +176 -0
  135. package/gsd-core/workflows/new-project/steps/auto-mode-detection.md +32 -0
  136. package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +18 -0
  137. package/gsd-core/workflows/new-project.md +13 -226
  138. package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +15 -0
  139. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +110 -0
  140. package/gsd-core/workflows/plan-phase/steps/prd-express-gate.md +8 -0
  141. package/gsd-core/workflows/plan-phase/steps/research-only-early-exit.md +17 -0
  142. package/gsd-core/workflows/plan-phase/steps/research-only-modifiers.md +16 -0
  143. package/gsd-core/workflows/plan-phase/steps/reviews-prerequisite.md +17 -0
  144. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +149 -0
  145. package/gsd-core/workflows/plan-phase.md +49 -193
  146. package/gsd-core/workflows/progress/steps/forensic-audit.md +125 -0
  147. package/gsd-core/workflows/progress/steps/mvp-display.md +18 -0
  148. package/gsd-core/workflows/progress.md +11 -153
  149. package/gsd-core/workflows/quick/steps/discussion-phase.md +124 -0
  150. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +111 -0
  151. package/gsd-core/workflows/quick/steps/quick-verification.md +46 -0
  152. package/gsd-core/workflows/quick/steps/research-phase.md +72 -0
  153. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +37 -0
  154. package/gsd-core/workflows/quick.md +20 -390
  155. package/gsd-core/workflows/resume-project.md +3 -0
  156. package/gsd-core/workflows/review/steps/reviewer-instances-note-1.md +4 -0
  157. package/gsd-core/workflows/review/steps/reviewer-instances-note-2.md +3 -0
  158. package/gsd-core/workflows/review.md +15 -8
  159. package/gsd-core/workflows/section-manifest.json +219 -0
  160. package/gsd-core/workflows/sketch.md +1 -1
  161. package/gsd-core/workflows/spec-phase.md +17 -14
  162. package/gsd-core/workflows/spike-wrap-up.md +20 -5
  163. package/gsd-core/workflows/spike.md +50 -16
  164. package/gsd-core/workflows/sync-skills.md +49 -11
  165. package/gsd-core/workflows/transition/steps/workstream-collision-check.md +17 -0
  166. package/gsd-core/workflows/transition.md +8 -21
  167. package/gsd-core/workflows/ui-phase.md +8 -7
  168. package/gsd-core/workflows/update/steps/channel-banner.md +7 -0
  169. package/gsd-core/workflows/update.md +18 -7
  170. package/gsd-core/workflows/verify-phase.md +4 -7
  171. package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +36 -0
  172. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +21 -0
  173. package/gsd-core/workflows/verify-work.md +8 -58
  174. package/hooks/dist/gsd-agent-isolation-guard.js +428 -0
  175. package/hooks/dist/gsd-check-update-worker.js +14 -5
  176. package/hooks/dist/gsd-cursor-subagent-start.js +532 -26
  177. package/hooks/dist/gsd-read-injection-scanner.js +7 -0
  178. package/hooks/dist/gsd-statusline.js +72 -6
  179. package/hooks/dist/gsd-worktree-path-guard.js +2 -1
  180. package/hooks/dist/gsd-write-guard.js +359 -0
  181. package/hooks/dist/lib/isolation-sentinel.js +268 -0
  182. package/hooks/dist/managed-hooks-registry.cjs +2 -0
  183. package/hooks/gsd-agent-isolation-guard.js +428 -0
  184. package/hooks/gsd-check-update-worker.js +14 -5
  185. package/hooks/gsd-cursor-subagent-start.js +532 -26
  186. package/hooks/gsd-read-injection-scanner.js +7 -0
  187. package/hooks/gsd-statusline.js +72 -6
  188. package/hooks/gsd-worktree-path-guard.js +2 -1
  189. package/hooks/gsd-write-guard.js +359 -0
  190. package/hooks/hooks.json +12 -0
  191. package/hooks/lib/isolation-sentinel.js +268 -0
  192. package/hooks/managed-hooks-registry.cjs +2 -0
  193. package/package.json +14 -5
  194. package/pi/gsd.cjs +57 -12
  195. package/scripts/build-hooks.js +9 -0
  196. package/scripts/changeset/lint.cjs +9 -2
  197. package/scripts/changeset/serialize.cjs +5 -1
  198. package/scripts/gen-capability-matrix.cjs +1 -1
  199. package/scripts/gen-context-index.cjs +448 -0
  200. package/scripts/gen-inventory-manifest.cjs +101 -1
  201. package/scripts/gen-prompt-budget-parity-corpus.cjs +645 -0
  202. package/scripts/gen-registry.cjs +39 -15
  203. package/scripts/gen-section-manifest.cjs +638 -0
  204. package/scripts/generate-package-identity.cjs +4 -2
  205. package/scripts/lint-allow-test-rule-refs.allowlist.json +17 -31
  206. package/scripts/lint-compiled-artifact-sync.cjs +6 -1
  207. package/scripts/lint-docs-command-form.cjs +195 -0
  208. package/scripts/lint-docs-required.cjs +9 -1
  209. package/scripts/lint-emitted-drift-ack.cjs +215 -20
  210. package/scripts/lint-example-parser-parity.cjs +395 -0
  211. package/scripts/lint-test-file-count.allowlist.json +27 -1
  212. package/scripts/mutation-matrix.cjs +13 -0
  213. package/scripts/prompt-injection-scan.sh +27 -6
  214. package/scripts/registry-schema.cjs +323 -94
  215. package/scripts/run-tests.cjs +3 -2
  216. package/scripts/validate-registry.cjs +10 -6
  217. package/skills/gsd-autonomous/SKILL.md +1 -1
  218. package/skills/gsd-execute-phase/SKILL.md +1 -1
  219. package/skills/gsd-mempalace-capture/SKILL.md +1 -1
  220. package/skills/gsd-new-milestone/SKILL.md +1 -1
  221. package/skills/gsd-plan-phase/SKILL.md +2 -2
  222. package/vscode/package.json +1 -1
  223. package/scripts/gen-emitted-baseline.cjs +0 -145
@@ -0,0 +1,110 @@
1
+ ## 8.5. Chunked Planning Mode
2
+
3
+ **Skip if `CHUNKED_MODE` is `false`.**
4
+
5
+ Chunked mode splits the single planner run into a short outline run + N short per-plan
6
+ runs (~3–5 min each), committing each plan individually for crash resilience. Rerunning
7
+ `/gsd:plan-phase {N} --chunked` resumes from the last committed plan.
8
+
9
+ For recovering plans from a prior *non-chunked* run, use step 6's "Add more plans" or
10
+ proceed to `/gsd:execute-phase` — don't start a fresh chunked run over them.
11
+
12
+ ### 8.5.1 Outline Phase (outline-only mode, ~2 min)
13
+
14
+ **Resume detection:** If `${PHASE_DIR}/${PADDED_PHASE}-PLAN-OUTLINE.md` exists and contains
15
+ the `## OUTLINE COMPLETE` marker (written by the outline agent — #2762), skip to 8.5.2.
16
+
17
+ ```bash
18
+ OUTLINE_FILE="${PHASE_DIR}/${PADDED_PHASE}-PLAN-OUTLINE.md"
19
+ if [[ -f "$OUTLINE_FILE" ]] && grep -q "^## OUTLINE COMPLETE" "$OUTLINE_FILE"; then
20
+ # reuse existing outline — skip to 8.5.2
21
+ fi
22
+ ```
23
+
24
+ Display:
25
+ ```text
26
+ ◆ Chunked mode: spawning outline planner... (runs in a subagent — no output until it returns, ~1–5 min; expected, not a freeze)
27
+ ```
28
+
29
+ Spawn the planner in **outline-only** mode — it must write only the outline manifest, not any
30
+ PLAN.md files:
31
+
32
+ ```javascript
33
+ Agent(
34
+ prompt="{same planning_context as step 8, plus:}
35
+
36
+ **Chunked mode: outline-only.**
37
+ Do NOT write any PLAN.md files in this Task.
38
+ Write only: {PHASE_DIR}/{PADDED_PHASE}-PLAN-OUTLINE.md
39
+
40
+ The outline must be a markdown table with columns:
41
+ Plan ID | Objective | Wave | Depends On | Requirements
42
+
43
+ End the file with a final line `## OUTLINE COMPLETE` — §8.5.1's resume-check greps
44
+ the file for it, so it MUST be written here, not just returned.
45
+ Return: ## OUTLINE COMPLETE with plan count.",
46
+ subagent_type="gsd-planner",
47
+ model="{planner_model}",
48
+ description="Outline Phase {phase} (chunked)",
49
+ run_in_background=true
50
+ )
51
+ ```
52
+
53
+ **ORCHESTRATOR RULE — ALL RUNTIMES:** `TS=$(date +%s)`; repeat `PLANNER_STALL_RESULT=$(gsd_stall_watch "$TS" "{outputFile}" "$OUTLINE_FILE" "## OUTLINE COMPLETE")` while waiting/active.
54
+
55
+ Handle return:
56
+ - **`marker_received`:** Read `PLAN-OUTLINE.md`, extract plan list. Continue to 8.5.2.
57
+ - **`stalled` / any other return or empty:** Display error. Offer: 1) Retry outline, 2) Stop.
58
+
59
+ ### 8.5.2 Per-Plan Tasks (single-plan mode, ~3-5 min each)
60
+
61
+ For each plan entry extracted from `PLAN-OUTLINE.md`:
62
+
63
+ 1. **Resume check:** Skip if `${PHASE_DIR}/{plan_id}-PLAN.md` exists with valid frontmatter
64
+ (resume safety) — UNLESS `--reviews` is set, whose purpose is to REPLAN with review
65
+ feedback (§6), so existing plans are overwritten, not skipped (#2762).
66
+
67
+ ```bash
68
+ PLAN_FILE="${PHASE_DIR}/${plan_id}-PLAN.md"
69
+ if [[ -f "$PLAN_FILE" ]] && head -1 "$PLAN_FILE" | grep -q '^---' && [[ "$ARGUMENTS" != *"--reviews"* ]]; then
70
+ continue # resume safety — NOT under --reviews (replan)
71
+ fi
72
+ ```
73
+
74
+ 2. Display:
75
+ ```text
76
+ ◆ Chunked mode: planning {plan_id} ({k}/{N})... (runs in a subagent — no output until it returns, ~1–5 min; expected, not a freeze)
77
+ ```
78
+
79
+ 3. Spawn the planner in **single-plan** mode — it must write exactly one PLAN.md file:
80
+ ```javascript
81
+ Agent(
82
+ prompt="{same planning_context as step 8, plus:}
83
+
84
+ **Chunked mode: single-plan.**
85
+ Write exactly ONE plan file: {PHASE_DIR}/{plan_id}-PLAN.md
86
+ Plan to write: {plan_id} — {objective}
87
+ Wave: {wave} | Depends on: {depends_on}
88
+ Phase requirement IDs to cover in this plan: {plan_requirements}
89
+
90
+ Return: ## PLAN COMPLETE with the plan ID.",
91
+ subagent_type="gsd-planner",
92
+ model="{planner_model}",
93
+ description="Plan {plan_id} (chunked {k}/{N})",
94
+ run_in_background=true
95
+ )
96
+ ```
97
+
98
+ **ORCHESTRATOR RULE — ALL RUNTIMES:** `TS=$(date +%s)`; repeat `PLANNER_STALL_RESULT=$(gsd_stall_watch "$TS" "{outputFile}" "$PLAN_FILE" "## PLAN COMPLETE")` while waiting/active — `stalled` falls into step 4 (preserves prior committed chunks).
99
+
100
+ 4. **Verify disk:** Check `${PHASE_DIR}/{plan_id}-PLAN.md` exists. If missing: offer 1) Retry, 2) Stop.
101
+
102
+ 5. **Commit per-plan:**
103
+ ```bash
104
+ _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}"; if [ -f "$GSD_TOOLS" ]; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif command -v gsd-tools >/dev/null 2>&1; then GSD_TOOLS="$(command -v gsd-tools)"; gsd_run() { "$GSD_TOOLS" "$@"; }; elif [ -f "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; else echo "ERROR: gsd-tools.cjs not found at $GSD_TOOLS and gsd-tools is not on PATH. Run: npx -y @opengsd/gsd-core@latest --claude --local" >&2; exit 1; fi; 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
105
+ gsd_run query commit "docs(${PADDED_PHASE}): plan ${plan_id} (chunked)" --files "${PHASE_DIR}/${plan_id}-PLAN.md"
106
+ ```
107
+
108
+ After all N plans are written and committed, treat this as `## PLANNING COMPLETE` and continue
109
+ to step 9.
110
+
@@ -0,0 +1,8 @@
1
+ ## 3.5. Handle PRD Express Path
2
+
3
+ **Skip if:** No `--prd` flag in arguments.
4
+
5
+ **If `--prd <filepath>` provided:**
6
+
7
+ Read and execute `gsd-core/workflows/plan-phase/steps/prd-express-path.md` — it reads the PRD (`$PRD_FILE`), generates `CONTEXT.md` (every PRD requirement/story/criterion → locked decision, uncovered areas → "Claude's Discretion", canonical refs extracted from ROADMAP.md + PRD-referenced specs), commits it, sets `context_content`, and bypasses step 4 (Load CONTEXT.md). The rest of the workflow proceeds normally with the PRD-derived context.
8
+
@@ -0,0 +1,17 @@
1
+ ### Research-Only Early Exit (`--research-phase`)
2
+
3
+ **Skip if:** `RESEARCH_ONLY` is `false` (the default).
4
+
5
+ **If `RESEARCH_ONLY=true`:** the user invoked `/gsd:plan-phase --research-phase <N>` for research-only mode. Do **not** continue to Section 5.5+ (validation strategy, planner, plan-checker, verification, gaps, bounce, post-planning-gaps). Print the research-complete summary and exit cleanly:
6
+
7
+ ```text
8
+ ✓ Research-only mode complete (#3042)
9
+
10
+ Phase: ${PHASE}
11
+ RESEARCH.md: ${research_path}
12
+
13
+ Re-run /gsd:plan-phase ${PHASE} to plan the phase using this research,
14
+ or /gsd:plan-phase ${PHASE} --research to refresh research and plan.
15
+ ```
16
+
17
+ This exits the workflow. The planner / plan-checker / verifier blocks below are skipped.
@@ -0,0 +1,16 @@
1
+ ### 5.0. Research-Only Modifiers (`--view`, `--research`)
2
+
3
+ **Skip if:** `RESEARCH_ONLY` is `false`.
4
+
5
+ Three branches in research-only mode (`--research-phase <N>`):
6
+
7
+ 1. **`--view`**: print `RESEARCH.md` to stdout, no spawn, exit. If `RESEARCH.md` is missing, error with: `--view requires an existing RESEARCH.md; drop --view to spawn the researcher.`
8
+ 2. **`--research`** (force-refresh): re-spawn researcher unconditionally — fall through to "Spawn gsd-phase-researcher" below.
9
+ 3. **Neither flag AND `has_research=true`:** auto-use the existing research and exit cleanly — do not prompt, do not re-spawn. Emit `RESEARCH.md already exists for Phase ${PHASE}, using it. To force-refresh, re-invoke with --research; to print, re-invoke with --view. Path: ${research_path}` then exit. The explicit-flag escape hatches cover any deviation; this matches §5.1's promptless auto-use of existing research, removing the §5.0/§5.1 inconsistency (#159).
10
+
11
+ ```bash
12
+ if [[ "$VIEW_ONLY" == "true" ]]; then
13
+ [[ -f "$research_path" ]] || { echo "Error: --view requires an existing RESEARCH.md (Phase ${PHASE}). Drop --view to spawn the researcher."; exit 1; }
14
+ cat "$research_path"; exit 0
15
+ fi
16
+ ```
@@ -0,0 +1,17 @@
1
+ ## 2.5. Validate `--reviews` Prerequisite
2
+
3
+ **Skip if:** No `--reviews` flag.
4
+
5
+ **If `--reviews` AND `--gaps`:** Error — cannot combine `--reviews` with `--gaps`. These are conflicting modes.
6
+
7
+ **If `--reviews` AND `has_reviews` is false (no REVIEWS.md in phase dir):**
8
+
9
+ Error:
10
+ ```
11
+ No REVIEWS.md found for Phase {N}. Run reviews first:
12
+
13
+ /gsd:review --phase {N}
14
+
15
+ Then re-run /gsd:plan-phase {N} --reviews
16
+ ```
17
+ Exit workflow.
@@ -0,0 +1,149 @@
1
+ # Bounded Stall-Detection Helpers (#2650)
2
+
3
+ Every planner/plan-checker spawn in `plan-phase.md` dispatches with
4
+ `run_in_background=true`, records `TS=$(date +%s)`, and then repeatedly
5
+ calls `gsd_stall_watch` until it returns something other than
6
+ `waiting`/`active`. This mirrors the already-shipped `executor.stall_*`
7
+ pattern (`execute-phase.md`, bug #3212, commit `e7942c21b`) but — unlike
8
+ that prose-only surveillance, which cannot run during a *blocking* `Agent()`
9
+ call — each `gsd_stall_watch` call is a real, bounded bash subprocess wait
10
+ issued as its own tool call, so it returns control to the orchestrator on
11
+ its own schedule regardless of whether the backgrounded agent's own
12
+ completion notification ever arrives.
13
+
14
+ **Binding `{outputFile}` (load-bearing, not optional):** every `gsd_stall_watch`
15
+ call below takes `{outputFile}` as its second argument — a literal token the
16
+ orchestrator must substitute with the REAL path from the immediately preceding
17
+ `run_in_background=true` Agent() call's returned `async_launched` result,
18
+ exactly as `docs-update.md:471` already does ("Read tool: file_path: `{outputFile
19
+ from README agent result}`"). This is NOT a bash variable the snippet below
20
+ assigns — there is nothing upstream that assigns one, so a bash variable
21
+ reference here would silently stay empty forever. With `{outputFile}` correctly
22
+ substituted, `[ -f "$output_file" ]` can find the real file and the
23
+ `marker_received` path is reachable; left as a literal (or as an unbound bash
24
+ variable), `marker_found` can never become `true` and every spawn silently
25
+ falls back to the mtime-only path — for the plan-checker spawn specifically,
26
+ that fallback is broken (see next paragraph), so binding this correctly there
27
+ is not a nice-to-have.
28
+
29
+ **Plan-checker's artifact glob needs the marker, not just mtimes:** the
30
+ plan-checker spawn watches `*-PLAN.md` for freshness, but a checker that
31
+ PASSES touches none of those files — no fresh mtime, ever, on a clean run.
32
+ Without `{outputFile}` correctly bound to the real completion output, a
33
+ healthy plan-checker that returns `## VERIFICATION PASSED` in two minutes
34
+ would still be declared `stalled` once `planner.stall_threshold_minutes`
35
+ elapses — reporting a succeeded agent as hung, which is worse than the
36
+ original unbounded wait. The marker path (via `{outputFile}`) is the ONLY
37
+ working completion signal for that spawn; the artifact glob is secondary
38
+ there.
39
+
40
+ **Single-cycle by design, not one long-lived loop:** `gsd_stall_watch` sleeps
41
+ for exactly one `PLANNER_STALL_INTERVAL_MINUTES` and returns — it does NOT
42
+ loop internally for the full `PLANNER_STALL_THRESHOLD_MINUTES`. A single Bash
43
+ tool call blocking for `threshold + interval` minutes (up to 15 min at
44
+ defaults) risks the *host tool's own* timeout killing the call before it ever
45
+ prints a result — silently defeating the fix it exists to ship. Looping at
46
+ the orchestrator-prose level instead means every cycle is a short (default 5
47
+ min), real, bounded call that reliably hands control back — the outer
48
+ threshold is enforced by `dispatch_ts` accumulating across calls, not by one
49
+ call's own duration.
50
+
51
+ **Disclosed tradeoff:** the first cycle always sleeps a full
52
+ `PLANNER_STALL_INTERVAL_MINUTES` before its first check, so a planner that
53
+ completes in seconds is not observed by this path until that interval
54
+ elapses (default 5 min) — slower than a plain blocking call's near-instant
55
+ return on success. This is deliberate: it trades a bounded, at-most-one-
56
+ interval delay on the (common) success path for eliminating the unbounded,
57
+ possibly-indefinite hang on the (rare, previously unrecoverable) stall path
58
+ this issue is about. `PLANNER_STALL_INTERVAL_MINUTES` is the knob for
59
+ projects that want a tighter success-path latency at the cost of more
60
+ config-get calls.
61
+
62
+ This block is independent of, and never gated behind, the `query
63
+ teams-status` / `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS` guard used for the
64
+ researcher spawn — the stall path applies on every runtime, teams-active or
65
+ not (AC2).
66
+
67
+ ```bash
68
+ _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}"; if [ -f "$GSD_TOOLS" ]; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif command -v gsd-tools >/dev/null 2>&1; then GSD_TOOLS="$(command -v gsd-tools)"; gsd_run() { "$GSD_TOOLS" "$@"; }; elif [ -f "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; else echo "ERROR: gsd-tools.cjs not found at $GSD_TOOLS and gsd-tools is not on PATH. Run: npx -y @opengsd/gsd-core@latest --claude --local" >&2; exit 1; fi; 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
69
+ PLANNER_STALL_INTERVAL_MINUTES=$(gsd_run query config-get planner.stall_detect_interval_minutes 2>/dev/null || echo "5")
70
+ PLANNER_STALL_THRESHOLD_MINUTES=$(gsd_run query config-get planner.stall_threshold_minutes 2>/dev/null || echo "10")
71
+ # Both values are config-controlled (.planning/config.json, editable by any repo
72
+ # contributor) and both flow into `$(( ))` arithmetic below. A non-numeric
73
+ # value there is NOT a code-execution risk (empirically verified: bash's
74
+ # arithmetic evaluator hard-errors on a `$(cmd)`-shaped operand instead of
75
+ # invoking it — "syntax error: operand expected", command never runs) but IS
76
+ # a reliability risk this fix cannot afford: a malformed config value would
77
+ # abort the stall-watcher itself with a bash syntax error, silently defeating
78
+ # the exact hang-recovery this issue is about. Reject anything that is not a
79
+ # bare non-negative integer before it is ever used, so a bad config value
80
+ # degrades to the safe default instead of crashing the watcher.
81
+ [[ "$PLANNER_STALL_INTERVAL_MINUTES" =~ ^[0-9]+$ ]] || PLANNER_STALL_INTERVAL_MINUTES=5
82
+ [[ "$PLANNER_STALL_THRESHOLD_MINUTES" =~ ^[0-9]+$ ]] || PLANNER_STALL_THRESHOLD_MINUTES=10
83
+
84
+ # gsd_stall_should_recover — pure decision function, no IO, no sleeping. Given how
85
+ # long the orchestrator has been waiting plus two liveness signals (a completion
86
+ # marker found in the agent's output file, and fresh on-disk artifact activity),
87
+ # decides whether to keep waiting, treat the wait as satisfied, or auto-surface the
88
+ # existing accept/retry/stop recovery menu (9a/11a). Never kills or retries anything
89
+ # itself — it only classifies. Re-validates both numeric args as bare non-negative
90
+ # integers (defense in depth — safe to call with any input, not just the resolved
91
+ # config globals above) before either ever reaches arithmetic expansion.
92
+ gsd_stall_should_recover() {
93
+ local elapsed_seconds="$1" threshold_minutes="$2" marker_found="$3" artifact_fresh="$4"
94
+ [[ "$elapsed_seconds" =~ ^[0-9]+$ ]] || elapsed_seconds=0
95
+ [[ "$threshold_minutes" =~ ^[0-9]+$ ]] || threshold_minutes=10
96
+ local threshold_seconds=$(( threshold_minutes * 60 ))
97
+ if [ "$marker_found" = "true" ]; then
98
+ echo "marker_received"; return 0
99
+ fi
100
+ if [ "$artifact_fresh" = "true" ]; then
101
+ echo "active"; return 0
102
+ fi
103
+ if [ "$elapsed_seconds" -ge "$threshold_seconds" ]; then
104
+ echo "stalled"; return 0
105
+ fi
106
+ echo "waiting"; return 0
107
+ }
108
+
109
+ # gsd_stall_watch — ONE bounded, real (non-LLM-side) sleep-and-check cycle, not
110
+ # a long-lived loop (see "Single-cycle by design" above — a single Bash tool
111
+ # call spanning the full threshold risks the host tool's own timeout killing
112
+ # it first). Sleeps exactly one PLANNER_STALL_INTERVAL_MINUTES, then checks for
113
+ # a completion marker in $2 (the outputFile returned by the run_in_background
114
+ # Agent() call) or fresh mtime activity under $3 (an artifact glob), against
115
+ # elapsed time since $1 (an epoch-seconds dispatch_ts the CALLER records once,
116
+ # before the first call, and passes unchanged on every repeat). Remaining args
117
+ # are completion markers. Prints exactly one of: marker_received | active |
118
+ # waiting | stalled. The caller repeats the call while the result is
119
+ # waiting/active; any other result ends the wait.
120
+ gsd_stall_watch() {
121
+ local dispatch_ts="$1" output_file="$2" artifact_glob="$3"; shift 3
122
+ local markers=("$@")
123
+ [[ "$dispatch_ts" =~ ^[0-9]+$ ]] || dispatch_ts=$(date +%s)
124
+ sleep "$(( PLANNER_STALL_INTERVAL_MINUTES * 60 ))"
125
+ local now elapsed marker_found artifact_fresh
126
+ now=$(date +%s)
127
+ elapsed=$(( now - dispatch_ts ))
128
+ marker_found="false"
129
+ if [ -f "$output_file" ]; then
130
+ for m in "${markers[@]}"; do
131
+ if grep -qF "$m" "$output_file" 2>/dev/null; then marker_found="true"; break; fi
132
+ done
133
+ fi
134
+ # -mmin -N ("modified less than N minutes ago"), not -newermt "@<epoch>":
135
+ # -newermt's "@<epoch>" shorthand is a GNU-date convenience the shipped
136
+ # BSD find(1) on macOS does NOT understand ("Can't parse date/time:
137
+ # @<epoch>", verified live) — with the 2>/dev/null below that failed
138
+ # silently and permanently degraded artifact_fresh to false on every
139
+ # macOS run. -mmin -N needs no epoch/date-string conversion at all and is
140
+ # supported identically by GNU find (Linux, Git-for-Windows' bundled
141
+ # findutils) and BSD find (macOS). $artifact_glob stays intentionally
142
+ # unquoted — the shell, not find, expands it into the matching file list.
143
+ artifact_fresh="false"
144
+ if [ -n "$(find $artifact_glob -mmin "-${PLANNER_STALL_INTERVAL_MINUTES}" 2>/dev/null)" ]; then
145
+ artifact_fresh="true"
146
+ fi
147
+ gsd_stall_should_recover "$elapsed" "$PLANNER_STALL_THRESHOLD_MINUTES" "$marker_found" "$artifact_fresh"
148
+ }
149
+ ```