@opengsd/gsd-core 1.16.0 → 1.18.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 (211) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -2
  3. package/.opencode/plugins/gsd-core.js +251 -1
  4. package/.opencode/plugins/gsd-statusline/tui.tsx +55 -0
  5. package/GEMINI.md +1 -1
  6. package/agents/gsd-advisor-researcher.compact.md +1 -1
  7. package/agents/gsd-advisor-researcher.md +1 -1
  8. package/agents/gsd-assumptions-analyzer.compact.md +1 -1
  9. package/agents/gsd-assumptions-analyzer.md +1 -1
  10. package/agents/gsd-code-fixer.compact.md +1 -1
  11. package/agents/gsd-code-fixer.md +1 -1
  12. package/agents/gsd-code-reviewer.compact.md +1 -1
  13. package/agents/gsd-code-reviewer.md +1 -1
  14. package/agents/gsd-codebase-mapper.compact.md +1 -1
  15. package/agents/gsd-codebase-mapper.md +1 -1
  16. package/agents/gsd-debug-session-manager.compact.md +6 -0
  17. package/agents/gsd-debug-session-manager.md +6 -0
  18. package/agents/gsd-debugger.md +1 -1
  19. package/agents/gsd-doc-verifier.compact.md +4 -4
  20. package/agents/gsd-doc-verifier.md +4 -3
  21. package/agents/gsd-doc-writer.compact.md +23 -5
  22. package/agents/gsd-doc-writer.md +19 -6
  23. package/agents/gsd-eval-auditor.compact.md +1 -1
  24. package/agents/gsd-eval-auditor.md +1 -1
  25. package/agents/gsd-executor.md +7 -3
  26. package/agents/gsd-integration-checker.compact.md +1 -1
  27. package/agents/gsd-integration-checker.md +1 -1
  28. package/agents/gsd-nyquist-auditor.compact.md +1 -1
  29. package/agents/gsd-nyquist-auditor.md +1 -1
  30. package/agents/gsd-phase-researcher.md +1 -1
  31. package/agents/gsd-plan-checker.md +2 -1
  32. package/agents/gsd-planner.md +5 -4
  33. package/agents/gsd-project-researcher.compact.md +1 -1
  34. package/agents/gsd-project-researcher.md +1 -1
  35. package/agents/gsd-research-synthesizer.compact.md +5 -5
  36. package/agents/gsd-research-synthesizer.md +5 -5
  37. package/agents/gsd-roadmapper.compact.md +1 -1
  38. package/agents/gsd-roadmapper.md +1 -1
  39. package/agents/gsd-security-auditor.compact.md +1 -1
  40. package/agents/gsd-security-auditor.md +1 -1
  41. package/agents/gsd-ui-auditor.compact.md +1 -1
  42. package/agents/gsd-ui-auditor.md +1 -1
  43. package/agents/gsd-ui-checker.compact.md +1 -1
  44. package/agents/gsd-ui-checker.md +1 -1
  45. package/agents/gsd-ui-researcher.compact.md +13 -1
  46. package/agents/gsd-ui-researcher.md +28 -1
  47. package/agents/gsd-verifier.md +1 -1
  48. package/bin/gsd-mcp-server.js +3 -1
  49. package/bin/install.js +398 -95
  50. package/commands/gsd/code-review.md +8 -0
  51. package/commands/gsd/pause-work.md +1 -1
  52. package/gsd-core/bin/gsd-tools.cjs +34 -12
  53. package/gsd-core/bin/lib/adr-parser.cjs +37 -5
  54. package/gsd-core/bin/lib/capability-ledger.cjs +25 -0
  55. package/gsd-core/bin/lib/capability-lock.cjs +29 -7
  56. package/gsd-core/bin/lib/capability-registry.cjs +138 -80
  57. package/gsd-core/bin/lib/capability-source.cjs +3 -1
  58. package/gsd-core/bin/lib/capability-state.cjs +6 -3
  59. package/gsd-core/bin/lib/capability-validator.cjs +8 -5
  60. package/gsd-core/bin/lib/capability-writer.cjs +2 -1
  61. package/gsd-core/bin/lib/commands.cjs +33 -9
  62. package/gsd-core/bin/lib/config-loader.cjs +3 -3
  63. package/gsd-core/bin/lib/decision-coverage-support.cjs +19 -0
  64. package/gsd-core/bin/lib/gate-context-drift.cjs +5 -4
  65. package/gsd-core/bin/lib/gate-decision-coverage-plan.cjs +15 -0
  66. package/gsd-core/bin/lib/gate-decision-coverage-verify.cjs +13 -0
  67. package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +23 -3
  68. package/gsd-core/bin/lib/gate-predicate.cjs +8 -12
  69. package/gsd-core/bin/lib/gate-tdd-review-checkpoint.cjs +60 -5
  70. package/gsd-core/bin/lib/gate-ui-safety.cjs +2 -1
  71. package/gsd-core/bin/lib/graphify.cjs +3 -2
  72. package/gsd-core/bin/lib/init.cjs +43 -16
  73. package/gsd-core/bin/lib/install-effort-resolver.cjs +2 -2
  74. package/gsd-core/bin/lib/install-engine.cjs +11 -0
  75. package/gsd-core/bin/lib/install-model-override-resolver.cjs +4 -3
  76. package/gsd-core/bin/lib/install-profiles.cjs +5 -3
  77. package/gsd-core/bin/lib/install-shadow-report.cjs +12 -0
  78. package/gsd-core/bin/lib/installer-migrations/011-opencode-mcp-no-prompts.cjs +99 -0
  79. package/gsd-core/bin/lib/installer-migrations.cjs +54 -1
  80. package/gsd-core/bin/lib/jsonc-edit.cjs +418 -0
  81. package/gsd-core/bin/lib/loop-host-contract.cjs +2 -1
  82. package/gsd-core/bin/lib/mcp-server.cjs +5 -1
  83. package/gsd-core/bin/lib/opencode-mcp-entry.cjs +39 -0
  84. package/gsd-core/bin/lib/opencode-version.cjs +486 -0
  85. package/gsd-core/bin/lib/phase.cjs +141 -11
  86. package/gsd-core/bin/lib/quick-batch-dispatch.cjs +2 -1
  87. package/gsd-core/bin/lib/report-parser.cjs +57 -1
  88. package/gsd-core/bin/lib/review-lane-invocation.cjs +40 -0
  89. package/gsd-core/bin/lib/review-reviewer-selection.cjs +20 -1
  90. package/gsd-core/bin/lib/roadmap.cjs +56 -5
  91. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +142 -6
  92. package/gsd-core/bin/lib/runtime-homes.cjs +18 -0
  93. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +58 -3
  94. package/gsd-core/bin/lib/runtime-slash.cjs +13 -0
  95. package/gsd-core/bin/lib/shell-command-projection.cjs +2 -0
  96. package/gsd-core/bin/lib/smart-entry.cjs +100 -53
  97. package/gsd-core/bin/lib/state-document.cjs +37 -3
  98. package/gsd-core/bin/lib/state-transition.cjs +98 -19
  99. package/gsd-core/bin/lib/state.cjs +544 -126
  100. package/gsd-core/bin/lib/task-command-router.cjs +2 -3
  101. package/gsd-core/bin/lib/tdd-red-evidence.cjs +5 -0
  102. package/gsd-core/bin/lib/tdd-test-files.cjs +42 -0
  103. package/gsd-core/bin/lib/update-context.cjs +18 -1
  104. package/gsd-core/bin/lib/verification.cjs +107 -17
  105. package/gsd-core/bin/lib/verify.cjs +36 -1
  106. package/gsd-core/references/agent-contracts.md +1 -1
  107. package/gsd-core/references/agent-skills-bootstrap.md +26 -7
  108. package/gsd-core/references/git-integration.md +3 -3
  109. package/gsd-core/references/planner-reversibility.md +18 -4
  110. package/gsd-core/references/planner-verify-command-grounding.md +18 -0
  111. package/gsd-core/references/tdd.md +4 -3
  112. package/gsd-core/references/verifier-phase-gates.md +8 -1
  113. package/gsd-core/workflows/add-backlog.md +10 -2
  114. package/gsd-core/workflows/add-phase.md +5 -1
  115. package/gsd-core/workflows/add-tests.md +5 -1
  116. package/gsd-core/workflows/add-todo.md +5 -1
  117. package/gsd-core/workflows/ai-integration-phase.md +5 -1
  118. package/gsd-core/workflows/audit-milestone.md +21 -9
  119. package/gsd-core/workflows/autonomous/templates/minimal-context.md +41 -0
  120. package/gsd-core/workflows/autonomous.md +52 -64
  121. package/gsd-core/workflows/check-todos.md +5 -1
  122. package/gsd-core/workflows/code-review-fix.md +5 -1
  123. package/gsd-core/workflows/code-review.md +21 -2
  124. package/gsd-core/workflows/complete-milestone/detail/elaboration.md +5 -1
  125. package/gsd-core/workflows/complete-milestone.md +25 -5
  126. package/gsd-core/workflows/debug.md +5 -1
  127. package/gsd-core/workflows/diagnose-issues.md +5 -1
  128. package/gsd-core/workflows/discuss-phase/modes/chain.md +5 -1
  129. package/gsd-core/workflows/discuss-phase-assumptions.md +11 -3
  130. package/gsd-core/workflows/discuss-phase.md +5 -1
  131. package/gsd-core/workflows/docs-update/detail/elaboration.md +7 -6
  132. package/gsd-core/workflows/docs-update.md +27 -5
  133. package/gsd-core/workflows/edit-phase.md +5 -1
  134. package/gsd-core/workflows/eval-review.md +11 -3
  135. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +5 -1
  136. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +1 -1
  137. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +12 -6
  138. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +19 -2
  139. package/gsd-core/workflows/execute-phase/steps/regression-gate.md +18 -1
  140. package/gsd-core/workflows/execute-phase/steps/verify-phase-goal.md +5 -1
  141. package/gsd-core/workflows/execute-phase.md +40 -7
  142. package/gsd-core/workflows/execute-plan.md +8 -4
  143. package/gsd-core/workflows/extract-learnings.md +6 -2
  144. package/gsd-core/workflows/forensics.md +5 -1
  145. package/gsd-core/workflows/import.md +5 -1
  146. package/gsd-core/workflows/insert-phase.md +5 -1
  147. package/gsd-core/workflows/manager.md +10 -2
  148. package/gsd-core/workflows/map-codebase.md +5 -1
  149. package/gsd-core/workflows/milestone-summary.md +10 -2
  150. package/gsd-core/workflows/new-milestone.md +13 -5
  151. package/gsd-core/workflows/new-project.md +9 -5
  152. package/gsd-core/workflows/next.md +8 -4
  153. package/gsd-core/workflows/pause-work.md +3 -3
  154. package/gsd-core/workflows/plan-phase/steps/auto-advance-check.md +70 -0
  155. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +5 -1
  156. package/gsd-core/workflows/plan-phase/steps/closed-phase-gate.md +6 -2
  157. package/gsd-core/workflows/plan-phase.md +41 -73
  158. package/gsd-core/workflows/plan-review-convergence.md +17 -5
  159. package/gsd-core/workflows/plant-seed.md +8 -4
  160. package/gsd-core/workflows/progress.md +6 -2
  161. package/gsd-core/workflows/quick-batch.md +5 -1
  162. package/gsd-core/workflows/quick.md +22 -7
  163. package/gsd-core/workflows/remove-phase.md +5 -1
  164. package/gsd-core/workflows/resume-project.md +19 -11
  165. package/gsd-core/workflows/review.md +6 -2
  166. package/gsd-core/workflows/scan.md +5 -1
  167. package/gsd-core/workflows/secure-phase.md +6 -2
  168. package/gsd-core/workflows/settings.md +3 -3
  169. package/gsd-core/workflows/ship.md +13 -10
  170. package/gsd-core/workflows/thread.md +10 -2
  171. package/gsd-core/workflows/transition.md +5 -1
  172. package/gsd-core/workflows/ui-phase.md +5 -1
  173. package/gsd-core/workflows/ui-review.md +6 -2
  174. package/gsd-core/workflows/ultraplan-phase.md +5 -1
  175. package/gsd-core/workflows/update.md +5 -1
  176. package/gsd-core/workflows/validate-phase.md +6 -2
  177. package/gsd-core/workflows/verify-work/detail/elaboration.md +1 -1
  178. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +5 -1
  179. package/gsd-core/workflows/verify-work.md +11 -3
  180. package/hooks/dist/gsd-agent-isolation-guard.js +3 -1
  181. package/hooks/dist/gsd-check-update.js +26 -20
  182. package/hooks/dist/gsd-context-monitor.js +14 -5
  183. package/hooks/dist/gsd-cursor-subagent-start.js +2 -1
  184. package/hooks/dist/gsd-secret-read-guard.js +189 -15
  185. package/hooks/dist/gsd-statusline.js +321 -22
  186. package/hooks/dist/gsd-validate-commit.sh +27 -5
  187. package/hooks/gsd-agent-isolation-guard.js +3 -1
  188. package/hooks/gsd-check-update.js +26 -20
  189. package/hooks/gsd-context-monitor.js +14 -5
  190. package/hooks/gsd-cursor-subagent-start.js +2 -1
  191. package/hooks/gsd-secret-read-guard.js +189 -15
  192. package/hooks/gsd-statusline.js +321 -22
  193. package/hooks/gsd-validate-commit.sh +27 -5
  194. package/package.json +1 -1
  195. package/scripts/audit-workflow-script-paths.cjs +1 -1
  196. package/scripts/diagnose-validate-commit-in-tier.cjs +270 -0
  197. package/scripts/diagnose-validate-commit-phases.cjs +348 -0
  198. package/scripts/diagnose-windows-spawn-watcher.cjs +317 -0
  199. package/scripts/docs-guard-registry.cjs +2 -0
  200. package/scripts/gen-capability-registry.cjs +7 -1
  201. package/scripts/gen-loop-host-contract.cjs +4 -1
  202. package/scripts/lib/macos-conformance-tier.generated.cjs +13 -0
  203. package/scripts/lib/platform-conformance-tier.generated.cjs +26 -0
  204. package/scripts/lint-phase-arg-assignment.cjs +2 -2
  205. package/scripts/lint-workflow-shellcheck-baseline.json +16 -16
  206. package/scripts/mutation-matrix.cjs +3 -1
  207. package/scripts/release-tarball-smoke.cjs +15 -7
  208. package/scripts/xtrace-phase-timing.cjs +205 -0
  209. package/skills/gsd-code-review/SKILL.md +8 -0
  210. package/skills/gsd-pause-work/SKILL.md +1 -1
  211. package/vscode/package.json +1 -1
@@ -12,6 +12,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
12
12
  const node_fs_1 = __importDefault(require("node:fs"));
13
13
  const node_path_1 = __importDefault(require("node:path"));
14
14
  const security_cjs_1 = require("./security.cjs");
15
+ const tdd_test_files_cjs_1 = require("./tdd-test-files.cjs");
15
16
  // eslint-disable-next-line @typescript-eslint/no-require-imports
16
17
  const ioMod = require("./io.cjs");
17
18
  const { output, error, ERROR_REASON } = ioMod;
@@ -37,9 +38,7 @@ function isBehaviorAddingTaskContent(content) {
37
38
  .filter(Boolean);
38
39
  hasSourceFiles = fileLines.some((file) => !/\.md$/i.test(file) &&
39
40
  !/\.json$/i.test(file) &&
40
- !/\.test\.[^.]+$/i.test(file) &&
41
- !/\.spec\.[^.]+$/i.test(file) &&
42
- !/(^|[\\/])tests?[\\/]/i.test(file) &&
41
+ !(0, tdd_test_files_cjs_1.isTestFilePath)(file) &&
43
42
  !/\.(yml|yaml|toml|ini|cfg|conf|properties)$/i.test(file) &&
44
43
  !/(^|[\\/])\.env(\..+)?$/i.test(file));
45
44
  }
@@ -10,6 +10,7 @@ exports.RedEvidenceReason = {
10
10
  TargetFailed: 'target_test_failed', Green: 'unexpected_green', Empty: 'zero_tests_discovered',
11
11
  NoFailure: 'nonzero_exit_without_test_failure', LoadFailure: 'fixture_or_load_failure',
12
12
  NoTarget: 'no_target_test_failure', Invalid: 'invalid_record', Unreadable: 'unreadable_record',
13
+ BuildFailure: 'build_failure',
13
14
  };
14
15
  /** Basename of a path-like string ('' for non-strings) — separators `/` and `\`. */
15
16
  function baseOf(p) {
@@ -59,6 +60,10 @@ function classifyRedEvidence(input) {
59
60
  };
60
61
  if (exitCode === 0)
61
62
  return { verdict: exports.RedEvidenceVerdict.Invalid, reason: exports.RedEvidenceReason.Green, evidence };
63
+ // A package that did not compile or load asserted nothing: refuse it as its own reason, ahead of
64
+ // the generic unusable-report refusal, because the fix is different (make the test compile).
65
+ if (report.buildFailed)
66
+ return { verdict: exports.RedEvidenceVerdict.Invalid, reason: exports.RedEvidenceReason.BuildFailure, evidence };
62
67
  if (!report.valid)
63
68
  return { verdict: exports.RedEvidenceVerdict.Invalid, reason: exports.RedEvidenceReason.Invalid, evidence };
64
69
  if (report.tests.length === 0)
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ /**
3
+ * The single definition of "a test file" for the TDD gate (#5106).
4
+ *
5
+ * Two places decide what a test file is: the behavior-adding check (`task-command-router.cts`) and
6
+ * the RED-commit pathspec in `workflows/execute-phase.md`. They were separate lists, so the pathspec
7
+ * learned the Go / Elixir / RSpec / pytest names in #4715 while the router never did, and a task
8
+ * listing only `placement_test.go` was classified behavior-adding — the gate then asked for a RED
9
+ * commit for the task that IS the RED commit.
10
+ *
11
+ * `TEST_FILE_PATHSPECS` is the list in git-pathspec spelling, which is what the workflow passes to
12
+ * `evaluation-scope --pathspec`; `isTestFilePath` evaluates the same list for a task's `<files>`.
13
+ * A test pins the workflow's pathspec flags to this array, so the two cannot drift apart again.
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.TEST_FILE_PATHSPECS = void 0;
17
+ exports.isTestFilePath = isTestFilePath;
18
+ /** Git-pathspec spelling. A trailing `/` is a directory, anything else a basename glob. */
19
+ exports.TEST_FILE_PATHSPECS = [
20
+ '*.test.*', '*.spec.*', 'tests/', '__tests__/',
21
+ '*_test.go', 'test_*.py', '*_test.py', '*_test.exs', '*_spec.rb', '*_test.rb',
22
+ ];
23
+ /** Directory names a `<files>` entry may sit under. `test/` is the singular spelling many projects use. */
24
+ const TEST_DIRECTORY_NAMES = new Set([
25
+ ...exports.TEST_FILE_PATHSPECS.filter((spec) => spec.endsWith('/')).map((spec) => spec.slice(0, -1)),
26
+ 'test',
27
+ ]);
28
+ function escapeRegex(text) {
29
+ return text.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
30
+ }
31
+ /** A basename glob where `*` is any run of non-separator characters. */
32
+ const FILE_GLOBS = exports.TEST_FILE_PATHSPECS
33
+ .filter((spec) => !spec.endsWith('/'))
34
+ .map((spec) => new RegExp(`^${spec.split('*').map(escapeRegex).join('[^/]*')}$`, 'i'));
35
+ /** True when `file` (either separator) is a test file by the shared convention list. */
36
+ function isTestFilePath(file) {
37
+ const segments = file.split(/[\\/]/).filter(Boolean);
38
+ const base = segments[segments.length - 1] ?? '';
39
+ if (FILE_GLOBS.some((glob) => glob.test(base)))
40
+ return true;
41
+ return segments.slice(0, -1).some((segment) => TEST_DIRECTORY_NAMES.has(segment.toLowerCase()));
42
+ }
@@ -147,7 +147,24 @@ function resolveGlobalCandidate(fs, env, home, preferred) {
147
147
  /**
148
148
  * Pure resolver. Returns { installedVersion, scope, runtime, gsdDir }.
149
149
  */
150
- function resolveUpdateContext({ home, cwd, env = {}, fs, preferredConfigDir = '', preferredRuntime = '', }) {
150
+ function resolveUpdateContext(opts) {
151
+ const base = resolveBaseUpdateContext(opts);
152
+ if (base.runtime !== 'opencode' || !base.gsdDir)
153
+ return base;
154
+ // The install records which OpenCode generation it targets; surface it when present.
155
+ const raw = opts.fs.readFile(node_path_1.default.join(base.gsdDir, 'gsd-install-state.json'));
156
+ if (raw == null)
157
+ return base;
158
+ try {
159
+ const hg = JSON.parse(raw).hostGeneration;
160
+ if (hg && hg.runtime === 'opencode' && (hg.generation === 1 || hg.generation === 2)) {
161
+ return { ...base, hostGeneration: hg.generation };
162
+ }
163
+ }
164
+ catch { /* an unreadable state file contributes nothing */ }
165
+ return base;
166
+ }
167
+ function resolveBaseUpdateContext({ home, cwd, env = {}, fs, preferredConfigDir = '', preferredRuntime = '', }) {
151
168
  // Expand a leading `~/` before any probe.
152
169
  preferredConfigDir = expandHome(preferredConfigDir, home);
153
170
  const preferred = preferredRuntime || inferPreferredRuntime({ fs, env, preferredConfigDir });
@@ -258,6 +258,11 @@ function reportStatusOf(fm, filePath) {
258
258
  * too): it re-runs the verifier, which regenerates VERIFICATION.md and its
259
259
  * digest (#4682, #4887).
260
260
  */
261
+ /**
262
+ * The one remedy for a stale report (#4682 / #5118), shared by the route table and by the more specific
263
+ * diagnostic below so the two cannot drift: only re-running the verifier regenerates VERIFICATION.md.
264
+ */
265
+ const STALE_REMEDY = 'Re-run execute-phase for this phase: it resumes at the verification gates and re-runs the verifier, regenerating VERIFICATION.md and its digest. verify-work alone cannot refresh a stale report.';
261
266
  const VERIFICATION_ROUTES = Object.freeze({
262
267
  passed: Object.freeze({
263
268
  next_action: 'Verification passed — continue.',
@@ -279,7 +284,7 @@ const VERIFICATION_ROUTES = Object.freeze({
279
284
  stale: Object.freeze({
280
285
  // #4682 / #5118: the only remedy for a stale report is re-running the
281
286
  // verifier; /gsd-verify-work on its own never rewrites VERIFICATION.md.
282
- next_action: 'Verification is stale — covered source files changed after the verifier last ran. Re-run execute-phase for this phase: it resumes at the verification gates and re-runs the verifier, regenerating VERIFICATION.md and its digest. verify-work alone cannot refresh a stale report.',
287
+ next_action: `Verification is stale — covered source files changed after the verifier last ran. ${STALE_REMEDY}`,
283
288
  command: 'execute-phase',
284
289
  tail: '',
285
290
  }),
@@ -540,7 +545,7 @@ function phaseArtifactPaths(phaseDir, projectRoot) {
540
545
  const scan = scanPhasePlans(phaseDir);
541
546
  // Fail closed on a non-COMPLETE scan (unreadable phase dir, or an
542
547
  // unreadable nested plans/ dir) — a partial scan's invisible contents can
543
- // never be proven covered (mirrors allCurrentArtifactsCovered's own
548
+ // never be proven covered (mirrors uncoveredCurrentArtifacts's own
544
549
  // fail-closed contract).
545
550
  if (scan.scope !== SCOPE.COMPLETE) {
546
551
  return { ok: false, reason: `phase directory scan is incomplete (scope: ${scan.scope}): ${phaseDir}` };
@@ -849,9 +854,9 @@ function computeCoveredDigest(projectRoot, coveredFiles, version = FINGERPRINT_V
849
854
  * each is represented in `coveredFiles` — matched by suffix (mirrors
850
855
  * `matchRequestedFile`'s convention) since `coveredFiles` holds
851
856
  * project-root-relative paths while the scan returns phase-relative
852
- * filenames. Returns `true` if every current plan/summary is covered,
853
- * `false` otherwise — callers only ever branch on this pass/fail, so no
854
- * caller needs which artifact was uncovered.
857
+ * filenames. Returns the current plans/summaries the report never declared
858
+ * (empty when every one is covered), so the stale diagnostic can name them (#4817); `null` when the
859
+ * scan itself was incomplete, which callers treat as stale.
855
860
  *
856
861
  * Fails CLOSED on an incomplete scan: `scanPhasePlans` never throws on a
857
862
  * readdir failure — it reports it via `scope` (`SCOPE.UNREADABLE` for the
@@ -860,18 +865,18 @@ function computeCoveredDigest(projectRoot, coveredFiles, version = FINGERPRINT_V
860
865
  * contract (`planning-scope.cts`): zero items under a non-`COMPLETE` scope
861
866
  * is a NON-answer, never "this phase has no plans." Branching on `scope`
862
867
  * here (rather than a try/catch, which this scan never triggers) is what
863
- * makes an unreadable `plans/` dir report `false` instead of silently
868
+ * makes an unreadable `plans/` dir report `null` (stale) instead of silently
864
869
  * treating its invisible contents as vacuously covered — the same
865
870
  * fail-open regression #3057 B3 fixed for the legacy path.
866
871
  */
867
- function allCurrentArtifactsCovered(phaseDir, coveredFiles) {
872
+ function uncoveredCurrentArtifacts(phaseDir, coveredFiles) {
868
873
  const scan = scanPhasePlans(phaseDir);
869
874
  if (scan.scope !== SCOPE.COMPLETE)
870
- return false;
875
+ return null;
871
876
  const coveredPosix = canonicalizeCoveredFiles(coveredFiles);
872
- return [...scan.allPlanFiles, ...scan.summaryFiles].every((artifact) => {
877
+ return [...scan.allPlanFiles, ...scan.summaryFiles].filter((artifact) => {
873
878
  const artifactPosix = toPosix(artifact);
874
- return coveredPosix.some((c) => c === artifactPosix || c.endsWith(`/${artifactPosix}`));
879
+ return !coveredPosix.some((c) => c === artifactPosix || c.endsWith(`/${artifactPosix}`));
875
880
  });
876
881
  }
877
882
  /**
@@ -943,6 +948,73 @@ function defaultPhaseCleanCommitTimesMs(phaseDir, files, execGitFn = shell_comma
943
948
  }
944
949
  return commitTimes;
945
950
  }
951
+ /**
952
+ * #4857: which declared covered files changed since the report was written.
953
+ * `covered_digest` is one aggregate hash, so it can say THAT something moved
954
+ * but not what; this names the files by time instead — each covered file
955
+ * (report paths and, for v2+, shared planning documents excluded, exactly as
956
+ * the digest excludes them) whose clean commit time, or mtime when it is
957
+ * uncommitted or edited since, is newer than the report's own. A file that
958
+ * can no longer be statted counts as moved. Advisory only: it never decides
959
+ * staleness (the digest does), and an unreadable clock yields fewer names,
960
+ * never a wrong status. Timed through the same injectable resolver the
961
+ * legacy mtime check uses, rooted at the project so the covered paths
962
+ * (project-root-relative) resolve.
963
+ */
964
+ function coveredFilesMovedSinceReport(projectRoot, phaseDir, reportPath, coveredFiles, version, fsImpl, resolver) {
965
+ const sharedRoots = version >= 2 ? sharedRootsFor(projectRoot, phaseDir, null) : [];
966
+ const files = canonicalizeCoveredFiles(coveredFiles, { version })
967
+ .filter((f) => !isVerificationReportPath(f) && !isSharedPlanningDoc(f, sharedRoots));
968
+ if (files.length === 0)
969
+ return [];
970
+ const reportRel = toPosix(node_path_1.default.relative(projectRoot, reportPath));
971
+ let commitTimes;
972
+ try {
973
+ commitTimes = resolver(projectRoot, [reportRel, ...files]);
974
+ }
975
+ catch {
976
+ commitTimes = new Map();
977
+ }
978
+ const timeOf = (rel) => {
979
+ const committed = commitTimes.get(rel);
980
+ if (committed !== undefined)
981
+ return committed;
982
+ try {
983
+ return fsImpl.statSync(node_path_1.default.join(projectRoot, rel)).mtimeMs;
984
+ }
985
+ catch {
986
+ return null;
987
+ }
988
+ };
989
+ const reportTime = timeOf(reportRel);
990
+ if (reportTime === null)
991
+ return [];
992
+ return files.filter((rel) => {
993
+ const t = timeOf(rel);
994
+ return t === null || t > reportTime;
995
+ });
996
+ }
997
+ /** At most this many uncovered artifact names are spelled out in `next_action`; the rest are counted. */
998
+ const STALE_NAMED_ARTIFACT_LIMIT = 5;
999
+ function staleNextAction(detail) {
1000
+ const name = (items) => {
1001
+ const shown = items.slice(0, STALE_NAMED_ARTIFACT_LIMIT).map((a) => io.formatDiagnosticToken(a));
1002
+ const more = items.length - shown.length;
1003
+ return `${shown.join(', ')}${more > 0 ? ` and ${more} more` : ''}`;
1004
+ };
1005
+ const reasons = [];
1006
+ if (detail.moved.length > 0) {
1007
+ reasons.push(`covered source files changed after the verifier last ran: ${name(detail.moved)}`);
1008
+ }
1009
+ else if (detail.digestMoved) {
1010
+ reasons.push('covered source files changed after the verifier last ran');
1011
+ }
1012
+ if (detail.uncovered.length > 0) {
1013
+ reasons.push(`the report never covered ${name(detail.uncovered)} (a current phase plan or summary it did not declare in covered_files)`);
1014
+ }
1015
+ const tail = detail.digestMoved ? '' : '; no covered file changed';
1016
+ return `Verification is stale — ${reasons.join(', and ')}${tail}. ${STALE_REMEDY}`;
1017
+ }
946
1018
  /**
947
1019
  * The one result builder (#5118): every `VerificationStatusResult` is
948
1020
  * projected from `VERIFICATION_ROUTES[status]` here — `route` (the bare
@@ -951,12 +1023,18 @@ function defaultPhaseCleanCommitTimesMs(phaseDir, files, execGitFn = shell_comma
951
1023
  */
952
1024
  function routeResult(status, ctx) {
953
1025
  const entry = VERIFICATION_ROUTES[status];
1026
+ const detail = status === VERIFICATION_STATUS.STALE && ctx.staleDetail
1027
+ && (ctx.staleDetail.uncovered.length > 0 || ctx.staleDetail.moved.length > 0)
1028
+ ? ctx.staleDetail
1029
+ : undefined;
954
1030
  return {
955
1031
  status,
956
- next_action: entry.next_action,
1032
+ next_action: detail ? staleNextAction(detail) : entry.next_action,
957
1033
  next_command: projectNextCommand(entry.command, ctx.runtime, `${ctx.phaseArg}${entry.tail}`),
958
1034
  route: entry.command,
959
1035
  ...(ctx.message !== undefined ? { message: ctx.message } : {}),
1036
+ ...(detail && detail.uncovered.length > 0 ? { uncovered_artifacts: detail.uncovered } : {}),
1037
+ ...(detail && detail.moved.length > 0 ? { moved_files: detail.moved } : {}),
960
1038
  ...(ctx.staleCheckIndeterminate ? { staleCheckIndeterminate: true } : {}),
961
1039
  };
962
1040
  }
@@ -1396,12 +1474,13 @@ function readVerificationStatus(phaseDir, opts = {}) {
1396
1474
  typeof coveredDigestVal === 'string' &&
1397
1475
  coveredDigestVal.trim().length > 0;
1398
1476
  let staleCheckIndeterminate = false;
1477
+ let staleDetail;
1399
1478
  let isStale;
1400
1479
  if (declaresFingerprint) {
1401
1480
  // Stated directly rather than relying on `null !== coveredDigestVal`
1402
1481
  // being true whenever the pair is malformed: `!hasWellFormedFingerprint`
1403
1482
  // fails closed explicitly, and its `||` short-circuit means
1404
- // computeCoveredDigest/allCurrentArtifactsCovered never run on a
1483
+ // computeCoveredDigest/uncoveredCurrentArtifacts never run on a
1405
1484
  // malformed (wrong-shaped) `coveredFilesVal`. The two `||`s after it
1406
1485
  // short-circuit in turn: the live-directory re-scan (for a plan/summary
1407
1486
  // added AFTER verification and never declared in covered_files) only
@@ -1423,17 +1502,28 @@ function readVerificationStatus(phaseDir, opts = {}) {
1423
1502
  // #5095 (R1): v3's digest is computed over the declared set UNIONED
1424
1503
  // with the phase's own live artifacts — a plan/summary added after
1425
1504
  // fingerprinting already moves the digest itself, so the separate
1426
- // live-directory re-scan (`allCurrentArtifactsCovered`) is redundant
1505
+ // live-directory re-scan (`uncoveredCurrentArtifacts`) is redundant
1427
1506
  // for v3 and is not run.
1428
1507
  isStale =
1429
1508
  computeCoveredDigest(projectRoot, coveredFilesVal, storedVersion, { phaseDir }) !== coveredDigestVal;
1509
+ if (isStale) {
1510
+ const moved = coveredFilesMovedSinceReport(projectRoot, phaseDir, located.filePath, coveredFilesVal, storedVersion, fsImpl, phaseCleanCommitTimesMs);
1511
+ if (moved.length > 0)
1512
+ staleDetail = { uncovered: [], moved, digestMoved: true };
1513
+ }
1430
1514
  }
1431
1515
  else {
1432
1516
  // v1/v2: unchanged — the digest alone cannot see a plan/summary that
1433
1517
  // was never declared, so the live-directory re-scan still runs.
1434
- isStale =
1435
- computeCoveredDigest(projectRoot, coveredFilesVal, storedVersion, { phaseDir }) !== coveredDigestVal ||
1436
- !allCurrentArtifactsCovered(phaseDir, coveredFilesVal);
1518
+ const digestMoved = computeCoveredDigest(projectRoot, coveredFilesVal, storedVersion, { phaseDir }) !== coveredDigestVal;
1519
+ const uncovered = uncoveredCurrentArtifacts(phaseDir, coveredFilesVal);
1520
+ isStale = digestMoved || uncovered === null || uncovered.length > 0;
1521
+ const moved = digestMoved
1522
+ ? coveredFilesMovedSinceReport(projectRoot, phaseDir, located.filePath, coveredFilesVal, storedVersion, fsImpl, phaseCleanCommitTimesMs)
1523
+ : [];
1524
+ if ((uncovered !== null && uncovered.length > 0) || moved.length > 0) {
1525
+ staleDetail = { uncovered: uncovered ? [...uncovered].sort() : [], moved, digestMoved };
1526
+ }
1437
1527
  }
1438
1528
  }
1439
1529
  else {
@@ -1449,7 +1539,7 @@ function readVerificationStatus(phaseDir, opts = {}) {
1449
1539
  if (isStale) {
1450
1540
  // #4682 / #5118: the one stale route — execute-phase's verify_phase_goal
1451
1541
  // step re-runs the verifier, regenerating VERIFICATION.md and its digest.
1452
- return route(VERIFICATION_STATUS.STALE);
1542
+ return route(VERIFICATION_STATUS.STALE, staleDetail ? { staleDetail } : {});
1453
1543
  }
1454
1544
  // 3. Route the writer-set member through the one table.
1455
1545
  return route(reportStatus, { staleCheckIndeterminate });
@@ -1147,6 +1147,38 @@ function unreadableFileVerdict(evidence, displayPath) {
1147
1147
  : { error: 'File unreadable', path: displayPath, read_error: evidence.reason };
1148
1148
  return (0, gate_verdict_cjs_1.gateUnreadable)(false, payload);
1149
1149
  }
1150
+ /**
1151
+ * Issue #4927 — a PLAN.md can be cut off by a tool that still reported the write
1152
+ * as successful (runtimes cap tool-call output; a ~450-line plan stopped inside
1153
+ * an XML tag). The planner and the plan checker both run `verify plan-structure`
1154
+ * on every plan, so this is where a silent partial write is caught, independent
1155
+ * of what the write call returned.
1156
+ *
1157
+ * Three signals, each a strong sign of a partial file and none of them a shape a
1158
+ * finished plan carries:
1159
+ * - the incremental-write sentinel (`<!-- gsd:write-continue -->`) left behind
1160
+ * (the planner's large-file fallback never removed it);
1161
+ * - `<tasks>` opened and never closed;
1162
+ * - the file ends inside a tag (`...</ta`). Prose such as `a < b` does not
1163
+ * match: a tag needs a name character straight after the `<` or `</`.
1164
+ */
1165
+ function scanPlanTruncation(content) {
1166
+ const errors = [];
1167
+ const text = content || '';
1168
+ if (/<!--\s*gsd:write-continue\s*-->/.test(text)) {
1169
+ errors.push('Plan appears truncated: the incremental-write sentinel `<!-- gsd:write-continue -->` is still present — '
1170
+ + 'the last section was never written. Finish the write (or re-write the plan) before treating it as complete.');
1171
+ }
1172
+ if (/<tasks>/.test(text) && !/<\/tasks>/.test(text)) {
1173
+ errors.push('Plan appears truncated: `<tasks>` is opened but `</tasks>` never appears — the file was cut off before the task list ended.');
1174
+ }
1175
+ const trimmed = text.replace(/\s+$/, '');
1176
+ const lastLt = trimmed.lastIndexOf('<');
1177
+ if (lastLt !== -1 && trimmed.indexOf('>', lastLt) === -1 && /^<\/?[A-Za-z_!][^<>]{0,200}$/.test(trimmed.slice(lastLt))) {
1178
+ errors.push(`Plan appears truncated: the file ends inside a tag (\`${trimmed.slice(lastLt).slice(0, 40)}\`) — the write stopped mid-payload.`);
1179
+ }
1180
+ return errors;
1181
+ }
1150
1182
  function cmdVerifyPlanStructure(cwd, filePath, raw) {
1151
1183
  if (!filePath) {
1152
1184
  error('file path required');
@@ -1172,6 +1204,8 @@ function cmdVerifyPlanStructure(cwd, filePath, raw) {
1172
1204
  const fm = extractFrontmatter(content, fullPath);
1173
1205
  const errors = [];
1174
1206
  const warnings = [];
1207
+ // #4927: a plan the write tool reported as written but that ends mid-payload.
1208
+ errors.push(...scanPlanTruncation(content));
1175
1209
  const required = ['phase', 'plan', 'type', 'wave', 'depends_on', 'files_modified', 'autonomous', 'must_haves'];
1176
1210
  for (const field of required) {
1177
1211
  if (fm[field] === undefined)
@@ -1220,7 +1254,8 @@ function cmdVerifyPlanStructure(cwd, filePath, raw) {
1220
1254
  continue;
1221
1255
  if (!decisionCheckpointOffsets.some((offset) => offset < at)) {
1222
1256
  warnings.push('Task rated <reversibility rating="one-way"> has no preceding checkpoint:decision — '
1223
- + 'a one-way door must be confirmed before the agent walks through it');
1257
+ + 'a one-way door must be confirmed before the agent walks through it (unless the decision is already settled '
1258
+ + 'in CONTEXT.md, an ingested ADR, or an answered checkpoint)');
1224
1259
  }
1225
1260
  }
1226
1261
  const echoScan = scanNegativeGrepCommentEcho(content);
@@ -33,7 +33,7 @@ This doc describes what IS, not what should be. Casing inconsistencies are docum
33
33
  | gsd-code-reviewer | Source-code review | No marker (writes REVIEW.md) | `gsd-core/workflows/code-review.md` reads REVIEW.md | artifact+query |
34
34
  | gsd-assumptions-analyzer | Assumption extraction | No marker (returns `## Assumptions` sections) | `gsd-core/workflows/discuss-phase-assumptions.md` reads the inline `## Assumptions` sections from the agent's return | structured-return |
35
35
  | gsd-doc-classifier | Planning-doc classification | No marker (writes `.planning/intel/classifications/*.json`) | `gsd-core/workflows/ingest-docs.md` reads the classification JSON | artifact+query |
36
- | gsd-doc-verifier | Doc validation | No marker (writes JSON to `.planning/tmp/`) | `.planning/tmp/verify-{doc_filename}.json` artifact, read by `gsd-core/workflows/docs-update.md` | artifact+query |
36
+ | gsd-doc-verifier | Doc validation | No marker (writes JSON to `.planning/tmp/`) | `{project_root}/.planning/tmp/verify-{doc_key}.json` artifact (the orchestrator-assigned `result_path`, keyed by the doc's full relative path), read by `gsd-core/workflows/docs-update.md` | artifact+query |
37
37
  | gsd-doc-writer | Doc generation | No marker (writes docs directly) | generated doc files, consumed by `gsd-core/workflows/docs-update.md` and `gsd-core/workflows/docs-update/steps/dispatch-monorepo-packages.md` | artifact+query |
38
38
  | gsd-domain-researcher | Domain research | No marker (writes the AI-SPEC.md domain section via Edit) | `gsd-core/workflows/ai-integration-phase.md` reads the AI-SPEC.md section after the agent returns | artifact+query |
39
39
  | gsd-eval-auditor | Evaluation coverage audit | No marker (writes the REVIEW.md audit section) | `gsd-core/workflows/eval-review.md` reads REVIEW.md | artifact+query |
@@ -22,22 +22,41 @@ In your mandatory init step — right after `mandatory-initial-read.md` / the
22
22
  injection also runs (e.g. Claude Code). The guard is what keeps the two seams from
23
23
  doubling the block.
24
24
 
25
- 2. **Query your configured skills.** Use **your own agent type** — the `name:` value in
26
- your frontmatter (e.g. an agent whose frontmatter says `name: gsd-executor` queries
27
- `gsd-executor`). The query is read-only and idempotent — it exits 0 with an empty
28
- block when nothing is configured for your type:
25
+ 2. **Query your configured skills.** Use **your own agent type**: the one your agent
26
+ file states on its `agent_skills:` pointer line, as `(agent type: \`gsd-executor\`)`.
27
+ Do not read it from your frontmatter `name:` — Claude Code shows a subagent only the
28
+ body, the Kimi converter drops frontmatter, and Codex keeps it in a TOML key. Run the
29
+ query with `--skills-only`, in the **same bash call** as the launcher so `gsd_run` is
30
+ defined when it runs. Substitute your type for `<your-agent-type>`:
29
31
 
30
32
  ```bash
31
- _AGENT_SKILLS=$(gsd_run query agent-skills <YOUR-FRONTMATTER-NAME> 2>/dev/null || true)
33
+ # 1. Launcher: paste the bash block from gsd-run-resolver.md here unless gsd_run
34
+ # is already defined in this shell (@~/.claude/gsd-core/references/gsd-run-resolver.md).
35
+ # 2. Query:
36
+ _AGENT_SKILLS=$(gsd_run query agent-skills <your-agent-type> --skills-only)
32
37
  ```
33
38
 
34
- The runtime `gsd_run` resolver is the standard one from
35
- `_runtime-launcher.snippet.sh`; your own init already defines it.
39
+ Not every agent file defines `gsd_run`, so supply it as above rather than assuming
40
+ your init did. The query is read-only and idempotent. Its stderr and exit status stay
41
+ visible, so a missing launcher or a failed query is never mistaken for "nothing
42
+ configured".
36
43
 
37
44
  3. **Read every listed skill.** The block emits entries as `@<path>/SKILL.md`
38
45
  includes — `Read` each one before starting work. If the block is empty, there is
39
46
  nothing to do (zero overhead).
40
47
 
48
+ ## What the query returns, per runtime
49
+
50
+ | Call | Claude | Non-Claude (Codex, Kimi, OpenCode, Kilo, …) |
51
+ |---|---|---|
52
+ | `query agent-skills <type> --skills-only` (the self-load) | configured skills, or an empty block | configured skills, or an empty block |
53
+ | `query agent-skills <type>` (orchestrator-side `${AGENT_SKILLS_*}` injection) | configured skills, or an empty block | configured skills, or — when nothing is configured and the agent is installed — the agent's own **persona** (#2454, #4407) |
54
+
55
+ On a non-Claude runtime the plain query can answer with the agent's whole persona
56
+ (about 47 KB for the executor). `--skills-only` is what keeps the self-load from
57
+ reading your own persona back into your context as if it were a skills block. Never
58
+ run the query without it from inside an agent.
59
+
41
60
  ## What self-load does and does not cover
42
61
 
43
62
  | Skill form | Self-loads? | Notes |
@@ -144,10 +144,10 @@ gsd_run query commit "docs({phase}-{plan}): complete [plan-name] plan" --files .
144
144
  </format>
145
145
 
146
146
  <format name="handoff">
147
- ## Handoff (WIP)
147
+ ## Handoff (Pause)
148
148
 
149
149
  ```
150
- wip: [phase-name] paused at task [X]/[Y]
150
+ docs(pause): [phase-name] paused at task [X]/[Y]
151
151
 
152
152
  Current: [task name]
153
153
  [If blocked:] Blocked: [reason]
@@ -156,7 +156,7 @@ Current: [task name]
156
156
  What to commit:
157
157
 
158
158
  ```bash
159
- gsd_run query commit "wip: [phase-name] paused at task [X]/[Y]" --files .planning/
159
+ gsd_run query commit "docs(pause): [phase-name] paused at task [X]/[Y]" --files .planning/
160
160
  ```
161
161
 
162
162
  </format>
@@ -20,7 +20,7 @@ this turns out wrong three phases from now, what does undoing it cost?*
20
20
  |---|---|---|
21
21
  | `reversible` | Local and cheap — one file, one function, an implementation swapped behind a stable interface. | `reversible` decisions get no checkpoint and no flag; the task proceeds normally. |
22
22
  | `costly` | Undo touches many call sites or needs a coordinated change — a shared interface shape, a cross-module contract, a dependency major bump. | `costly` decisions are flagged in the plan so the reader sees the weight, but this does not block execution. |
23
- | `one-way` | Undo requires a data migration, breaks a published contract, or cannot be done at all — on-disk/wire format, public API shape, external-service lock-in, a schema other systems already read. | The planner inserts a `checkpoint:decision` **before** the dependent task, so the human confirms the door before the agent walks through it. |
23
+ | `one-way` | Undo requires a data migration, breaks a published contract, or cannot be done at all — on-disk/wire format, public API shape, external-service lock-in, a schema other systems already read. | The planner inserts a `checkpoint:decision` **before** the dependent task, so the human confirms the door before the agent walks through it — unless the human already settled that decision (see *Already settled*). |
24
24
 
25
25
  **When unsure, rate it `reversible`.** The value of this feature is
26
26
  *discrimination*. A planner that rates everything `one-way` produces checkpoint
@@ -101,6 +101,21 @@ the same terms as any other ingested text (ADR-1577,
101
101
  carrying something that belongs in `<context>`, and long free text is where
102
102
  smuggled structure hides.
103
103
 
104
+ ## Already settled
105
+
106
+ A `one-way` decision the human has **already settled** keeps its rating for the
107
+ record and gets **no** new `checkpoint:decision`. It is settled when any of
108
+ these records the human choosing this exact option:
109
+
110
+ - the phase `CONTEXT.md` `<decisions>` block (discuss-phase output);
111
+ - an ADR ingested with `--ingest`;
112
+ - a `checkpoint:decision` earlier in this phase's plans that was already answered.
113
+
114
+ Only a **newly surfaced** `one-way` decision — one nothing above records — earns a
115
+ fresh checkpoint before the implementing task. If you cannot point at the record,
116
+ treat the decision as newly surfaced. `gsd-planner` and this reference state the
117
+ same rule; if they ever diverge, this file wins.
118
+
104
119
  ## Anti-patterns
105
120
 
106
121
  - **Everything is `one-way`.** The most common failure. Re-read the undo cost:
@@ -111,9 +126,8 @@ the same terms as any other ingested text (ADR-1577,
111
126
  - **A rationale that restates the rating.** "This is irreversible because it
112
127
  cannot be undone" tells the reader nothing. Name the migration, the contract,
113
128
  or the dependent system.
114
- - **Gating a decision already made.** If the phase CONTEXT.md records the human
115
- choosing this exact option, the door is already walked through. Keep the
116
- rating for the record; do not insert a checkpoint to re-ask.
129
+ - **Gating a decision already made.** See *Already settled* below: a decision the
130
+ human has already settled does not earn a fresh checkpoint.
117
131
  - **Using the gate as a substitute for design.** The checkpoint buys deliberation
118
132
  on a door you must walk through. The better move, when available, is to *make
119
133
  the decision reversible* — put the format behind a writer seam, version the
@@ -16,6 +16,24 @@ preferred over `cd <dir> && npm run <script>` — it does not depend on the exec
16
16
  `prior_verify_commands` is empty and you cannot ground a path, say so in the plan instead of
17
17
  guessing one.
18
18
 
19
+ **Run a count/match gate before it ships (#4927).** A gate you author — or one a `RESEARCH.md`
20
+ hands you — that counts or matches text (`grep -c`, `grep -q`, `rg`, `test -n "$(grep …)"`) is a
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`):
24
+
25
+ - **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.
29
+ - **Its literal can never match the deliverable.** `grep` is case-sensitive, so `grep -c "quickstart"`
30
+ fails on a correct `## Quickstart`. Match the real casing of the text the task writes, or use
31
+ `-i` when casing is not part of the contract.
32
+
33
+ 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
35
+ this way — run `grep`/`rg`/`test` gates; do not execute a gate that builds, installs, or writes.
36
+
19
37
  **Root-relative, never absolute (#4767).** The paths in your prompt's `<required_reading>` are
20
38
  absolute on purpose — a subagent's cwd may differ from the orchestrator's (#2376). That rule
21
39
  covers prompt *inputs* only. Every path in the plan *body* — `<files>`, `<verify>`, `<automated>`,
@@ -96,11 +96,12 @@ After completion, create SUMMARY.md with:
96
96
  2. Write test describing expected behavior (from `<behavior>` element)
97
97
  3. Run test - it MUST fail **intentionally** (#3770): the TARGET test you named must be the test that fails, on an assertion for the planned behavior. A nonzero exit alone is NOT RED — syntax errors, zero-test discovery, fixture crashes, parser errors, and unrelated assertions are INVALID_RED and must not authorize GREEN.
98
98
  4. Persist the RED evidence from the actual command: `{command, exitCode, output, targetTest, targetFile, expected, actual}`. Preserve the real exit status and unmodified report; record the planned expected result and observed actual result. Write the record inside the project directory; the classifier refuses a path outside it. It reads only these fields, so the further fields below stay in the record for review.
99
- - Select evidence by **report format**, regardless of runner or package manager. The classifier has a shared TAP adapter for Node's built-in test runner and Vitest (`tap` / `tap-flat`), a JUnit XML adapter for Maven Surefire/Failsafe, a swift-testing console adapter for `swift test`, and a Python `unittest` text adapter. Other producers of these formats use the same adapters.
99
+ - Select evidence by **report format**, regardless of runner or package manager. The classifier has a shared TAP adapter for Node's built-in test runner and Vitest (`tap` / `tap-flat`), a JUnit XML adapter for Maven Surefire/Failsafe, a swift-testing console adapter for `swift test`, a Python `unittest` text adapter, and a `go test` text adapter. Other producers of these formats use the same adapters.
100
100
  - TAP: capture the complete reporter output, including its test plan. Node summary comments are optional. The report must start with its TAP output: capture the reporter's own stdout (for npm scripts, `npm run --silent`), not package-manager banners. Use the target test's exact name; when names repeat, use its qualified name as emitted in the report (`tap-flat` emits only the full `file > suite > test` name). Suite/file summary failures are not target assertions. If the reporter is incompatible, rerun the planned target with a supported reporter through the project's test command, capture that real command and result, then submit the rerun's record to the classifier below. Preserve other console output separately rather than editing the report to make it parse.
101
101
  - JUnit XML: capture the unchanged target report generated by the actual run. For Maven this is `target/surefire-reports/TEST-*.xml` or `target/failsafe-reports/TEST-*.xml`. Record the report path, run start, and modification time as `reportPath`, `runStartedAt`, and `reportModifiedAt`; require the report to be newer than the run start and verify the named target class or method executed in that run. Name the target as the method, the class (simple or package-qualified), or `<package.Class>#<method>`. Put one complete XML document in `output`, preserving console output alongside it. Missing, stale, or ambiguous reports require STOP and investigation.
102
+ - `go test`: capture the complete console output (`go test ./...` or `-v`; `-json` is not supported) including every package summary line (`FAIL\tpkg\t0.004s`). A failed test is its `--- FAIL: TestName` line; the same test name failing in two packages is ambiguous and needs the package-qualified target `import/path.TestName`. A package that does not compile (`FAIL\tpkg [build failed]`) or load (`[setup failed]`) asserted nothing — the usual cause is a test naming a symbol that does not exist yet — and the whole run is refused as `build_failure`, even if another package genuinely failed. Make the test compile and fail on its assertion, then record the run again.
102
103
  - swift-testing and `unittest`: capture the complete console output without color (`NO_COLOR=1`), including the final summary. swift-testing needs one `Test` result line (passed, failed, skipped, or cancelled) for every test its `Test run with N tests` summary counts; it names a test by its quoted display name or, without one, by its function name (`addsNumbers()`), and either name identifies the target; repeated swift-testing names are ambiguous and block GREEN. `unittest` needs one `FAIL:`/`ERROR:` header for every failure or error its `FAILED (...)` line counts; repeated headers from one test's failing subTests count as one failing test, a method name that repeats across classes needs its `module.Class.method` id, and a `unittest.loader._FailedTest` entry is a load failure, never RED.
103
- - Pass the unchanged record to `gsd_run check tdd-red-evidence <record.json> --raw` and require `RED_EVIDENCE_OK` before GREEN. Every `INVALID_RED` reason (`unexpected_green`, `zero_tests_discovered`, `nonzero_exit_without_test_failure`, `fixture_or_load_failure`, `no_target_test_failure`, `invalid_record`, `unreadable_record`) trips the gate. `invalid_record` includes unsupported or malformed reports; inspect `evidence.report_errors`. An unsupported format requires a supported reporter or a parser adapter before GREEN. Classifier rejection never authorizes a fallback to self-attestation.
104
+ - Pass the unchanged record to `gsd_run check tdd-red-evidence <record.json> --raw` and require `RED_EVIDENCE_OK` before GREEN. Every `INVALID_RED` reason (`unexpected_green`, `zero_tests_discovered`, `nonzero_exit_without_test_failure`, `fixture_or_load_failure`, `no_target_test_failure`, `build_failure`, `invalid_record`, `unreadable_record`) trips the gate. `invalid_record` includes unsupported or malformed reports; inspect `evidence.report_errors`. An unsupported format requires a supported reporter or a parser adapter before GREEN. Classifier rejection never authorizes a fallback to self-attestation.
104
105
  - After machine validation, inspect whether the target actually executed and failed on the planned assertion for the intended reason. Zero tests, a missing, skipped, or cancelled target, setup, collection, import, syntax, or fixture faults, unrelated failures, unexpected green, and incomplete or ambiguous evidence block GREEN. Record this semantic assessment as `semanticAssessment` in the RED evidence and in the SUMMARY's RED entry; never fabricate reports, counters, or parser verdicts.
105
106
  5. If test passes: feature exists or test is wrong. Investigate.
106
107
  6. Commit: `test({phase}-{plan}): add failing test for [feature]`
@@ -341,7 +342,7 @@ TDD Plans: {count} | Gate violations: {count}
341
342
 
342
343
  ### What the Review Checks
343
344
 
344
- 1. **Gate sequence:** Each TDD plan has RED → GREEN commits in order
345
+ 1. **Gate sequence:** Each TDD plan has RED → GREEN commits in order. A plan is scored on its own `test(<plan>):` / `feat(<plan>):` commits, with one exception (#4867): a RED-only probe plan is satisfied by a `feat(...)` commit under a plan that declares it in `depends_on`, directly or through a chain. That is the gap-closure shape (probe plan, then the fix plan that depends on it). The plan's own RED is still required, a plan nothing depends on is scored alone, and the report names the plan that supplied the GREEN (`Plan {id} GREEN is supplied by dependent plan {id}.`).
345
346
  2. **Test quality:** RED phase tests fail for the right reason (not import errors or syntax)
346
347
  3. **Minimal GREEN:** Implementation is minimal — no premature optimization in GREEN phase
347
348
  4. **Refactor discipline:** If REFACTOR commit exists, tests still pass
@@ -39,7 +39,12 @@ fi
39
39
  ```
40
40
 
41
41
  The handler returns JSON `{ skipped, blocking: false, total, honored,
42
- not_honored: [...], message }`. A non-zero `DECISION_EXIT` (`69` `UNAVAILABLE`: the gate
42
+ not_honored: [...], message }`, plus `reason` on a skip or a caller error. On a
43
+ caller error about the phase directory (`reason` is `'phase directory not found'`,
44
+ `'phase path is not a directory'`, `'phase directory unreadable'` or
45
+ `'phase directory holds no plans'`), `total` and `honored` are `null` and
46
+ `not_honored` is `[]`; nothing was checked, so there is no count to report.
47
+ A non-zero `DECISION_EXIT` (`69` `UNAVAILABLE`: the gate
43
48
  could not read CONTEXT.md or the phase artifacts, #5170) is **not** "no decisions": record
44
49
  `Decision coverage: not evaluated (the gate could not read its evidence)` in VERIFICATION.md
45
50
  and do not report counts.
@@ -49,6 +54,8 @@ section) to VERIFICATION.md regardless of outcome — even when all
49
54
  decisions are honored, recording the count helps reviewers spot drift over
50
55
  time. Set `decision_coverage` in the verification result to
51
56
  `{honored, total, not_honored: [...]}` so downstream tooling can read it.
57
+ On a caller error that is `{honored: null, total: null, not_honored: []}`;
58
+ pass the nulls through rather than coercing them to `0`.
52
59
 
53
60
  **Status impact:** none. The decision gate does NOT influence the
54
61
  `gaps_found` / `human_needed` / `passed` decision tree in Step 9. Its
@@ -55,7 +55,11 @@ Plans:
55
55
  Apply the `project_code` prefix (if set in `.planning/config.json`) so the backlog directory name is consistent with all other phase-creation paths:
56
56
 
57
57
  ```bash
58
- SLUG=$(gsd_run query generate-slug "$ARGUMENTS" --raw)
58
+ IFS= read -rd '' GSD_ARGS <<'GSD_ARGS_END' || true
59
+ $ARGUMENTS
60
+ GSD_ARGS_END
61
+ GSD_ARGS=${GSD_ARGS%$'\n'}
62
+ SLUG=$(gsd_run query generate-slug "$GSD_ARGS" --raw)
59
63
  PROJECT_CODE=$(gsd_run query config-get project_code --raw 2>/dev/null || echo "")
60
64
  PREFIX=$([ -n "$PROJECT_CODE" ] && echo "${PROJECT_CODE}-" || echo "")
61
65
  PHASE_DIR=".planning/phases/${PREFIX}${NEXT}-${SLUG}"
@@ -66,7 +70,11 @@ touch "${PHASE_DIR}/.gitkeep"
66
70
  ## Step 5: Commit
67
71
 
68
72
  ```bash
69
- gsd_run query commit "docs: add backlog item ${NEXT} — ${ARGUMENTS}" --files .planning/ROADMAP.md "${PHASE_DIR}/.gitkeep"
73
+ IFS= read -rd '' GSD_ARGS <<'GSD_ARGS_END' || true
74
+ $ARGUMENTS
75
+ GSD_ARGS_END
76
+ GSD_ARGS=${GSD_ARGS%$'\n'}
77
+ gsd_run query commit "docs: add backlog item ${NEXT} — ${GSD_ARGS}" --files .planning/ROADMAP.md "${PHASE_DIR}/.gitkeep"
70
78
  ```
71
79
 
72
80
  ## Step 6: Report