@opengsd/gsd-core 1.7.0-rc.6 → 1.7.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 (76) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/agents/gsd-executor.md +2 -1
  4. package/agents/gsd-security-auditor.md +13 -15
  5. package/bin/install.js +5 -4
  6. package/commands/gsd/mempalace-capture.md +27 -1
  7. package/commands/gsd/surface.md +6 -6
  8. package/gsd-core/bin/gsd-tools.cjs +63 -2
  9. package/gsd-core/bin/lib/api-coverage.cjs +3 -4
  10. package/gsd-core/bin/lib/audit.cjs +7 -6
  11. package/gsd-core/bin/lib/capability-registry.cjs +57 -57
  12. package/gsd-core/bin/lib/check-command-router.cjs +1 -1
  13. package/gsd-core/bin/lib/clock.cjs +19 -0
  14. package/gsd-core/bin/lib/commands.cjs +48 -9
  15. package/gsd-core/bin/lib/config-loader.cjs +6 -2
  16. package/gsd-core/bin/lib/config.cjs +12 -0
  17. package/gsd-core/bin/lib/core-utils.cjs +8 -2
  18. package/gsd-core/bin/lib/drift.cjs +4 -4
  19. package/gsd-core/bin/lib/frontmatter.cjs +22 -0
  20. package/gsd-core/bin/lib/gsd2-import.cjs +2 -1
  21. package/gsd-core/bin/lib/init.cjs +27 -13
  22. package/gsd-core/bin/lib/install-engine.cjs +3 -2
  23. package/gsd-core/bin/lib/installer-migration-authoring.cjs +2 -1
  24. package/gsd-core/bin/lib/installer-migrations.cjs +1 -1
  25. package/gsd-core/bin/lib/markdown-sectionizer.cjs +342 -0
  26. package/gsd-core/bin/lib/markdown-table.cjs +698 -0
  27. package/gsd-core/bin/lib/milestone.cjs +217 -31
  28. package/gsd-core/bin/lib/phase-command-router.cjs +50 -2
  29. package/gsd-core/bin/lib/phase-lifecycle.cjs +62 -36
  30. package/gsd-core/bin/lib/phase-locator.cjs +23 -2
  31. package/gsd-core/bin/lib/phase.cjs +436 -61
  32. package/gsd-core/bin/lib/plan-scan.cjs +3 -0
  33. package/gsd-core/bin/lib/roadmap-parser.cjs +218 -13
  34. package/gsd-core/bin/lib/roadmap.cjs +100 -49
  35. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +7 -6
  36. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +3 -2
  37. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +2 -1
  38. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +25 -17
  39. package/gsd-core/bin/lib/schema-detect.cjs +2 -1
  40. package/gsd-core/bin/lib/security.cjs +1 -1
  41. package/gsd-core/bin/lib/shell-command-projection.cjs +61 -25
  42. package/gsd-core/bin/lib/smart-entry.cjs +4 -3
  43. package/gsd-core/bin/lib/state-transition.cjs +100 -45
  44. package/gsd-core/bin/lib/state.cjs +391 -126
  45. package/gsd-core/bin/lib/surface.cjs +2 -2
  46. package/gsd-core/bin/lib/template.cjs +2 -1
  47. package/gsd-core/bin/lib/uat.cjs +54 -8
  48. package/gsd-core/bin/lib/ui-safety-gate.cjs +23 -1
  49. package/gsd-core/bin/lib/verify.cjs +4 -3
  50. package/gsd-core/bin/lib/workstream.cjs +3 -2
  51. package/gsd-core/bin/lib/worktree-safety.cjs +1 -1
  52. package/gsd-core/bin/lib/write-set.cjs +38 -0
  53. package/gsd-core/bin/shared/config-schema.manifest.json +2 -0
  54. package/gsd-core/references/checkpoints.md +12 -0
  55. package/gsd-core/workflows/add-tests.md +1 -1
  56. package/gsd-core/workflows/debug.md +2 -0
  57. package/gsd-core/workflows/execute-phase.md +5 -3
  58. package/gsd-core/workflows/fast.md +8 -22
  59. package/gsd-core/workflows/progress.md +2 -2
  60. package/gsd-core/workflows/quick.md +2 -0
  61. package/gsd-core/workflows/review.md +42 -3
  62. package/gsd-core/workflows/secure-phase.md +1 -1
  63. package/gsd-core/workflows/ship.md +8 -2
  64. package/gsd-core/workflows/spec-phase.md +1 -1
  65. package/gsd-core/workflows/transition.md +1 -1
  66. package/hooks/dist/gsd-statusline.js +164 -14
  67. package/hooks/gsd-statusline.js +164 -14
  68. package/package.json +4 -2
  69. package/scripts/ci-test-scope.cjs +39 -1
  70. package/scripts/gen-golden-install-parity-zcode.cjs +35 -45
  71. package/scripts/gen-install-tree-fixtures.cjs +75 -0
  72. package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -1
  73. package/scripts/lint-table-schema-drift.cjs +157 -0
  74. package/skills/gsd-mempalace-capture/SKILL.md +27 -1
  75. package/skills/gsd-surface/SKILL.md +6 -6
  76. package/vscode/package.json +1 -1
@@ -305,8 +305,8 @@ function applySurface(runtimeConfigDir, layout, manifest, clusterMap, registry,
305
305
  // surface-path agents lack path-prefix rewrites and Co-Authored-By trailers,
306
306
  // diverging from a fresh install.
307
307
  const _homedirFn = opts?.homedir ?? (() => node_os_1.default.homedir());
308
- const _resolvedTarget = node_path_1.default.resolve(layout.configDir).replace(/\\/g, '/');
309
- const _homeDir = _homedirFn().replace(/\\/g, '/');
308
+ const _resolvedTarget = (0, shell_command_projection_cjs_1.posixNormalize)(node_path_1.default.resolve(layout.configDir));
309
+ const _homeDir = (0, shell_command_projection_cjs_1.posixNormalize)(_homedirFn());
310
310
  const _isGlobal = (layout.scope ?? 'global') === 'global';
311
311
  const _isOpencode = layout.runtime === 'opencode';
312
312
  const _isWindowsHost = (opts?.platform ?? process.platform) === 'win32';
@@ -11,6 +11,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
11
11
  };
12
12
  const node_fs_1 = __importDefault(require("node:fs"));
13
13
  const node_path_1 = __importDefault(require("node:path"));
14
+ const clock_cjs_1 = require("./clock.cjs");
14
15
  // eslint-disable-next-line @typescript-eslint/no-require-imports
15
16
  const ioMod = require("./io.cjs");
16
17
  const { output, error } = ioMod;
@@ -85,7 +86,7 @@ function cmdTemplateFill(cwd, templateType, options, raw) {
85
86
  return;
86
87
  }
87
88
  const padded = normalizePhaseName(options.phase);
88
- const today = new Date().toISOString().split('T')[0];
89
+ const today = clock_cjs_1.realClock.localToday();
89
90
  const phaseName = options.name || phaseInfo.phase_name || 'Unnamed';
90
91
  const phaseSlug = phaseInfo.phase_slug || generateSlugInternal(phaseName);
91
92
  const phaseId = `${padded}-${phaseSlug}`;
@@ -21,6 +21,9 @@ const { output, error } = io;
21
21
  const markdownSectionizer = require("./markdown-sectionizer.cjs");
22
22
  const { collectSection, tokenizeHeadings } = markdownSectionizer;
23
23
  // eslint-disable-next-line @typescript-eslint/no-require-imports
24
+ const markdownTable = require("./markdown-table.cjs");
25
+ const { splitTableRow } = markdownTable;
26
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
24
27
  const roadmapParser = require("./roadmap-parser.cjs");
25
28
  const { getMilestonePhaseFilter } = roadmapParser;
26
29
  // eslint-disable-next-line @typescript-eslint/no-require-imports
@@ -292,24 +295,67 @@ function parseVerificationItems(content, status) {
292
295
  // Use the seam to locate the ## Human Verification section (ADR-1372 T5).
293
296
  const hvSection = collectSection(content, (h) => /^human\s+verification/i.test(h.text) && h.level === 2, { levelBounded: true });
294
297
  if (hvSection) {
298
+ // #2245 review Fix 3: reverted to the pre-Phase-4 (HEAD 2cbf18642)
299
+ // implementation. The live Human Verification section is NOT a strict
300
+ // GFM table — the planner/verifier templates mix table rows, numbered
301
+ // items, and bullet items in the same section (and a `### N.` heading
302
+ // format is common too), so a table-XOR-list read (parse a table, and
303
+ // if it parses, suppress numbered/bullet items entirely) silently
304
+ // dropped items on any mixed or malformed section: a malformed
305
+ // `| N | … |` table with no valid header/delimiter yielded ZERO items
306
+ // instead of reading the rows positionally. This per-line scan reads
307
+ // table rows AND numbered items AND bullet items as a UNION (whichever
308
+ // pattern a given line matches), exactly like OLD, and reads
309
+ // `| N | desc |` rows even without a valid table header/delimiter.
310
+ //
311
+ // #2245 audit: the table-row branch's CELL SPLIT is name/position-
312
+ // addressed via `splitTableRow` (escape-aware, canonical) instead of a
313
+ // hand-rolled pipe regex — candidacy itself is decided WITHOUT a table
314
+ // regex (a leading `|` plus a purely-numeric first cell), so this no
315
+ // longer needs an allow-adhoc-markdown suppression at all.
295
316
  const lines = hvSection.body.split('\n');
296
317
  for (const line of lines) {
297
- // Match table rows: | N | description | ... |
298
- const tableMatch = line.match(/\|\s*(\d+)\s*\|\s*([^|]+)/);
318
+ const trimmedLine = line.trim();
319
+ // Match table rows: | N | description | ... — candidacy requires a
320
+ // leading pipe and a purely-numeric first cell (mirrors what the old
321
+ // regex effectively required: a "|digit|" cell immediately followed
322
+ // by more content), with at least 2 physical cells so a bare "| N |"
323
+ // with nothing after it is NOT treated as a row.
324
+ //
325
+ // #2245 review Fix 9: this is NOT the same as OLD for a row whose
326
+ // ONLY content past the digit cell is trailing whitespace (e.g.
327
+ // "| N | ", no second delimiting `|`). OLD's `([^|]+)` regex ran
328
+ // against the RAW (untrimmed) line and its `\s*` would backtrack to
329
+ // let `[^|]+` swallow that trailing whitespace, so OLD matched and
330
+ // pushed an item with an EMPTY (`.trim()`-collapsed) name. Here,
331
+ // `trimmedLine = line.trim()` strips that trailing whitespace BEFORE
332
+ // `splitTableRow` ever sees it, collapsing the line to a single cell
333
+ // (`candidateCells.length === 1`), which fails the `>= 2` check —
334
+ // the item is silently dropped instead. A real, acceptable behaviour
335
+ // change (an empty-named UAT item is not useful either way), but the
336
+ // two implementations are NOT equivalent on this input.
337
+ let tableCells = null;
338
+ if (trimmedLine.startsWith('|')) {
339
+ const candidateCells = splitTableRow(trimmedLine);
340
+ if (candidateCells.length >= 2 && /^\d+$/.test(candidateCells[0])) {
341
+ tableCells = candidateCells;
342
+ }
343
+ }
299
344
  // Match bullet items: - description
300
345
  const bulletMatch = line.match(/^[-*]\s+(.+)/);
301
346
  // Match numbered items: 1. description
302
347
  const numberedMatch = line.match(/^(\d+)\.\s+(.+)/);
303
- if (tableMatch) {
348
+ if (tableCells) {
304
349
  // Skip rows that already have a passing result (PASS, pass, resolved, etc.)
305
- const rowRemainder = line.slice(tableMatch.index + tableMatch[0].length);
306
- const cellValues = rowRemainder.split('|').map(c => c.trim());
307
- const hasPassResult = cellValues.some(c => /^pass$/i.test(c) || /^resolved$/i.test(c));
350
+ // — checked over every cell AFTER the description column, mirroring
351
+ // OLD's rowRemainder scan (which only ever saw cells past the
352
+ // description, the description itself having already been consumed).
353
+ const hasPassResult = tableCells.slice(2).some(c => /^pass$/i.test(c) || /^resolved$/i.test(c));
308
354
  if (hasPassResult)
309
355
  continue;
310
356
  items.push({
311
- test: parseInt(tableMatch[1], 10),
312
- name: tableMatch[2].trim(),
357
+ test: parseInt(tableCells[0], 10),
358
+ name: tableCells[1] ?? '',
313
359
  result: 'human_needed',
314
360
  category: 'human_uat',
315
361
  });
@@ -67,14 +67,36 @@ function checkUiPresence(text) {
67
67
  }
68
68
  // Normalise CRLF so the pattern sees consistent line boundaries.
69
69
  const normalised = text.replace(/\r\n/g, '\n');
70
+ // #2150: an explicit `**UI hint**: yes|no` metadata line is the author's
71
+ // authoritative declaration of whether the phase has a UI surface — progress.md
72
+ // and new-project.md already parse this line (`UI hint.*yes`). The bare token
73
+ // `UI` in the line itself must not count as a UI indicator, and the declaration
74
+ // overrides token-sniffing. Line-anchored (`m`) so a mid-line prose mention is
75
+ // not treated as the metadata line; word-boundary on the value so `nope`/`not`
76
+ // do not match `no`.
77
+ const hintMatch = normalised.match(/^\s*\*\*UI hint\*\*\s*:\s*(yes|no)\b/im);
78
+ const hint = hintMatch ? hintMatch[1].toLowerCase() : null;
79
+ // Strip ANY `**UI hint**:` line before token-sniffing so a hint without a
80
+ // recognised yes/no (or one we did not short-circuit on) cannot false-positive
81
+ // on the bare `UI` token.
82
+ const sniffable = normalised
83
+ .split('\n')
84
+ .filter((line) => !/^\s*\*\*UI hint\*\*\s*:/i.test(line))
85
+ .join('\n');
70
86
  const found = new Set();
71
- for (const line of normalised.split('\n')) {
87
+ for (const line of sniffable.split('\n')) {
72
88
  // Reset lastIndex before each line so the global pattern restarts from 0.
73
89
  UI_GATE_PATTERN_GLOBAL.lastIndex = 0;
74
90
  for (const m of line.matchAll(UI_GATE_PATTERN_GLOBAL)) {
75
91
  found.add(m[2].toLowerCase());
76
92
  }
77
93
  }
94
+ if (hint === 'no') {
95
+ return { hasUI: false, tokens: [] };
96
+ }
97
+ if (hint === 'yes') {
98
+ return { hasUI: true, tokens: [...found] };
99
+ }
78
100
  return { hasUI: found.size > 0, tokens: [...found] };
79
101
  }
80
102
  // ── CLI entry point ─────────────────────────────────────────────────────────
@@ -13,6 +13,7 @@ const node_fs_1 = __importDefault(require("node:fs"));
13
13
  const node_path_1 = __importDefault(require("node:path"));
14
14
  const node_os_1 = __importDefault(require("node:os"));
15
15
  const validate_cjs_1 = require("./validate.cjs");
16
+ const clock_cjs_1 = require("./clock.cjs");
16
17
  const validate_cjs_2 = require("./validate.cjs");
17
18
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- planning-workspace.cjs is an export= CommonJS module
18
19
  const planningWorkspace = require("./planning-workspace.cjs");
@@ -1049,7 +1050,7 @@ function cmdValidateConsistency(cwd, raw) {
1049
1050
  .sort();
1050
1051
  for (const dir of dirs) {
1051
1052
  const phasePath = node_path_1.default.join(phaseRoot, dir);
1052
- const phaseLabel = node_path_1.default.relative(planBase, phasePath).replace(/\\/g, '/');
1053
+ const phaseLabel = (0, shell_command_projection_cjs_1.posixNormalize)(node_path_1.default.relative(planBase, phasePath));
1053
1054
  const phaseFiles = node_fs_1.default.readdirSync(phasePath);
1054
1055
  const plans = phaseFiles.filter((f) => f.endsWith('-PLAN.md')).sort();
1055
1056
  const planNums = plans
@@ -1591,7 +1592,7 @@ function cmdValidateHealth(cwd, options, raw) {
1591
1592
  stateContent += `**Current phase:** (determining...)\n`;
1592
1593
  stateContent += `**Status:** Resuming\n\n`;
1593
1594
  stateContent += `## Session Log\n\n`;
1594
- stateContent += `- ${new Date().toISOString().split('T')[0]}: STATE.md regenerated by ${slash('health')} --repair\n`;
1595
+ stateContent += `- ${clock_cjs_1.realClock.localToday()}: STATE.md regenerated by ${slash('health')} --repair\n`;
1595
1596
  writeStateMd(statePath, stateContent, cwd);
1596
1597
  repairActions.push({ action: repair, success: true, path: 'STATE.md' });
1597
1598
  break;
@@ -1647,7 +1648,7 @@ function cmdValidateHealth(cwd, options, raw) {
1647
1648
  case 'backfillMilestones': {
1648
1649
  if (!options['backfill'] && !options['repair'])
1649
1650
  break;
1650
- const today = new Date().toISOString().split('T')[0];
1651
+ const today = clock_cjs_1.realClock.localToday();
1651
1652
  let backfilled = 0;
1652
1653
  for (const ver of missingFromRegistry) {
1653
1654
  try {
@@ -17,6 +17,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
17
17
  };
18
18
  const node_fs_1 = __importDefault(require("node:fs"));
19
19
  const node_path_1 = __importDefault(require("node:path"));
20
+ const clock_cjs_1 = require("./clock.cjs");
20
21
  // eslint-disable-next-line @typescript-eslint/no-require-imports
21
22
  const io = require("./io.cjs");
22
23
  const { output, error } = io;
@@ -154,7 +155,7 @@ function cmdWorkstreamCreate(cwd, name, options, raw) {
154
155
  }
155
156
  (0, shell_command_projection_cjs_1.platformEnsureDir)(wsDir);
156
157
  (0, shell_command_projection_cjs_1.platformEnsureDir)(node_path_1.default.join(wsDir, 'phases'));
157
- const today = new Date().toISOString().split('T')[0];
158
+ const today = clock_cjs_1.realClock.localToday();
158
159
  const stateContent = [
159
160
  '---',
160
161
  `workstream: ${slug}`,
@@ -264,7 +265,7 @@ function cmdWorkstreamComplete(cwd, name, options, raw) {
264
265
  if (active === name)
265
266
  setActiveWorkstream(cwd, null);
266
267
  const archiveDir = node_path_1.default.join(root, 'milestones');
267
- const today = new Date().toISOString().split('T')[0];
268
+ const today = clock_cjs_1.realClock.localToday();
268
269
  let archivePath = node_path_1.default.join(archiveDir, `ws-${name}-${today}`);
269
270
  let suffix = 1;
270
271
  while (node_fs_1.default.existsSync(archivePath)) {
@@ -440,7 +440,7 @@ function rescueSummaryArtifacts(worktreePath, repoRoot, deps) {
440
440
  // relPath is the path relative to the worktree root (e.g. ".planning/q1-SUMMARY.md")
441
441
  // Normalize to forward slashes so the Set comparison against `git status --porcelain`
442
442
  // output works on Windows too (git always emits forward slashes in porcelain output).
443
- const relPath = absPath.slice(worktreePath.length).replace(/^[/\\]/, '').replace(/\\/g, '/');
443
+ const relPath = (0, shell_command_projection_cjs_1.posixNormalize)(absPath.slice(worktreePath.length).replace(/^[/\\]/, ''));
444
444
  // #706: skip rescue when the SUMMARY is already committed on the branch.
445
445
  // Use `git cat-file -e HEAD:<relPath>` (not `ls-files --error-unmatch`) so
446
446
  // the check is against the committed tree, not the index. ls-files also
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ /**
3
+ * Write-Set — shared fail-loud parse `Result` and per-surface write-set
4
+ * contracts (ADR-2143, epic #2143). Pure, Node built-ins only, no I/O.
5
+ * Compiled by tsc to gsd-core/bin/lib/write-set.cjs.
6
+ *
7
+ * ADR-2143 §5 (fail-loud parsing, no null-swallow): seam parse operations
8
+ * and document-model accessors return a typed `Result<T>` — never a bare
9
+ * `null` a caller can mistake for "empty but fine." This is the same
10
+ * `{ ok: true; value: T } | { ok: false; reason: string }` shape
11
+ * `markdown-table.cts` already defined for `parseMarkdownTable` /
12
+ * `appendQuickTaskRow`; this module is now the single source of truth for
13
+ * it and `markdown-table.cjs` re-exports the type so existing importers of
14
+ * `Result` from that module keep working unchanged.
15
+ *
16
+ * NOTE: deliberately distinct from command-routing-hub's dispatch `Result`
17
+ * (`{ok,data}|{ok:false,kind}`) — the two never mix (different modules,
18
+ * different shapes, different purposes).
19
+ *
20
+ * ADR-2143 §6 (write-set results for multi-surface commands, no
21
+ * OR-into-one-flag): a command that mutates more than one surface returns
22
+ * an explicit per-surface write-set — `{ surface, applied }` outcomes — and
23
+ * its top-level "did this fully succeed" signal is true only if EVERY
24
+ * surface in the set applied. ORing independent surfaces into a single
25
+ * boolean is the direct anti-pattern that let a checkbox-only partial
26
+ * write (#2140) report full success.
27
+ */
28
+ Object.defineProperty(exports, "__esModule", { value: true });
29
+ exports.writeSetComplete = writeSetComplete;
30
+ /**
31
+ * True only if the write-set is non-empty AND every surface in it applied.
32
+ * An empty write-set is never "complete" — there is nothing to be complete
33
+ * about, so treating it as vacuously true would let a no-op masquerade as
34
+ * a full success (the same OR-into-one-flag class ADR-2143 §6 prohibits).
35
+ */
36
+ function writeSetComplete(ws) {
37
+ return ws.length > 0 && ws.every((o) => o.applied);
38
+ }
@@ -71,6 +71,8 @@
71
71
  "workflow.context_coverage_gate",
72
72
  "statusline.show_last_command",
73
73
  "statusline.context_position",
74
+ "statusline.show_context_tokens",
75
+ "statusline.show_git",
74
76
  "workflow.max_discuss_passes",
75
77
  "features.thinking_partner",
76
78
  "context",
@@ -9,6 +9,18 @@ Plans execute autonomously. Checkpoints formalize interaction points where human
9
9
  3. **User only does what requires human judgment** - Visual checks, UX evaluation, "does this feel right?"
10
10
  4. **Secrets come from user, automation comes from Claude** - Ask for API keys, then Claude uses them via CLI
11
11
  5. **Auto-mode bypasses verification/decision checkpoints** — When `workflow._auto_chain_active` or `workflow.auto_advance` is true in config: human-verify auto-approves, decision auto-selects first option, human-action still stops (auth gates cannot be automated)
12
+ 6. **`gate="blocking-human"` is never auto-approved** — a checkpoint carrying this gate stops for a human in *every* mode, including auto-mode, regardless of its type. Rule 5 does not apply to it.
13
+
14
+ **The `gate` attribute:**
15
+
16
+ | Value | Auto-mode behavior | Use for |
17
+ |-------|--------------------|---------|
18
+ | `gate="blocking"` | Bypassed per rule 5 (human-verify auto-approves, decision auto-selects) | The default. Post-hoc verification and implementation choices that are safe to take the recommended path on when unattended. |
19
+ | `gate="blocking-human"` | **Never bypassed.** Stops for a human in auto-mode too. | Irreversible or trust-establishing steps a human must actually see: package-legitimacy verification before install, and any decision whose default answer would be wrong to assume. |
20
+
21
+ Reach for `gate="blocking-human"` whenever auto-approving the checkpoint would defeat its purpose. If the checkpoint exists because a human must *decide* something, `blocking` is the wrong gate — auto-mode will decide it for them.
22
+
23
+ The gate spans two layers, and both must honor it. `gsd-executor` refuses to auto-approve a `gate="blocking-human"` checkpoint and escalates it via `checkpoint_return_format` precisely so a human sees it; `execute-phase`'s `checkpoint_handling` step then decides what the user is actually shown. An orchestrator that dispatches on checkpoint *type* alone would auto-approve the very checkpoint the executor just refused to auto-approve, nullifying that refusal one layer up and letting an unattended `--auto` / `--chain` run install a package no human ever vetted.
12
24
  </overview>
13
25
 
14
26
  <checkpoint_types>
@@ -308,7 +308,7 @@ If there are passing tests to commit:
308
308
 
309
309
  ```bash
310
310
  git add {test files}
311
- git commit -m "test(phase-${phase_number}): add unit and E2E tests from add-tests command"
311
+ git commit -m "test(phase-${phase_number}): add unit and E2E tests from add-tests command" -- {test files}
312
312
  ```
313
313
 
314
314
  Present next steps:
@@ -190,6 +190,8 @@ Create `.planning/debug/{slug}.md` with initial state using the Write tool (neve
190
190
 
191
191
  After initial context setup, spawn the session manager to handle the full checkpoint/continuation loop. The session manager handles specialist_hint dispatch internally: when gsd-debugger returns ROOT CAUSE FOUND it extracts the specialist_hint field and invokes the matching skill (e.g. typescript-expert, swift-concurrency) before offering fix options.
192
192
 
193
+ > **Foreground, blocking spawn — #2196.** The `Agent(subagent_type="gsd-debug-session-manager", …)` call below is FOREGROUND and BLOCKING — it returns the compact session summary directly. Wait for it; do not background it, and do not poll for it. Never pass an agent or session identifier to `TaskOutput` — an agent ID is NOT a task ID, so `TaskOutput <agent-id>` always returns `No task found with ID`. If the spawn returns no usable result (the handoff is lost), do NOT claim the session is still running: preserve the checkpoint at `.planning/debug/{slug}.md`, report the failed handoff plainly, and resume by re-spawning the session manager or via `/gsd:debug continue {slug}`.
194
+
193
195
  Print before spawning (runs in a subagent — no output until it returns, ~1–5 min; expected, not a freeze):
194
196
  ```
195
197
  [debug] Delegating loop to session manager...
@@ -1056,11 +1056,13 @@ AUTO_MODE=$(gsd_run query check auto-mode --pick active 2>/dev/null || echo "fal
1056
1056
  ```
1057
1057
 
1058
1058
  When executor returns a checkpoint AND `AUTO_MODE` is `true`:
1059
- - **human-verify** → Auto-spawn continuation agent with `{user_response}` = `"approved"`. Log `⚡ Auto-approved checkpoint`.
1060
- - **decision** → Auto-spawn continuation agent with `{user_response}` = first option from checkpoint details. Log `⚡ Auto-selected: [option]`.
1059
+ - **human-verify** → Auto-spawn continuation agent with `{user_response}` = `"approved"`. Log `⚡ Auto-approved checkpoint`. **Except `blocking-human`.**
1060
+ - **decision** → Auto-spawn continuation agent with `{user_response}` = first option from checkpoint details. Log `⚡ Auto-selected: [option]`. **Except `blocking-human`.**
1061
1061
  - **human-action** → Present to user (existing behavior below). Auth gates cannot be automated.
1062
1062
 
1063
- **Standard flow (not auto-mode, or human-action type):**
1063
+ **Carve-out — overrides all branches above.** If the returned `Gate:` is `blocking-human`, or its `<what-built>` mentions `Package verification required before install` or `Package install failed — human verification required`, never auto-approve or auto-select, regardless of type. Present to user (standard flow below). Log `⛔ blocking-human gate — auto-mode suspended`.
1064
+
1065
+ **Standard flow (not auto-mode, human-action, or blocking-human):**
1064
1066
 
1065
1067
  1. Spawn agent for checkpoint plan
1066
1068
  2. Agent runs until checkpoint task or auth gate → returns structured state
@@ -64,32 +64,18 @@ Use conventional commit format: `fix:`, `feat:`, `docs:`, `chore:`, `refactor:`
64
64
 
65
65
  <step name="log_to_state">
66
66
  If `.planning/STATE.md` exists and has a "Quick Tasks Completed" table, append a row
67
- that matches the existing table's schema. If no table exists, skip silently.
68
- If the table's schema is unrecognized, skip with a brief log rather than append a
69
- malformed row.
67
+ that matches the existing table's schema via the schema-backed `gsd-tools
68
+ quick-tasks-append` helper (`markdown-table.cjs`'s `appendQuickTaskRow`; #2133,
69
+ ADR-2143 §3/§7). If no table exists, skip silently. If the table's schema is
70
+ unrecognized, the helper fails loud (non-zero exit) instead of silently guessing
71
+ a column count — this replaces the prior inline `awk NF-2` arithmetic that was
72
+ the root cause of #2133.
70
73
 
71
74
  ```bash
75
+ _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
72
76
  # Detect whether STATE.md has a Quick Tasks Completed table
73
77
  if grep -q "Quick Tasks Completed" .planning/STATE.md 2>/dev/null; then
74
- # Read the table header line to determine the column schema.
75
- # quick.md Step 7 creates a 5-column table:
76
- # | # | Description | Date | Commit | Directory |
77
- # Count pipe characters in the header to determine column count.
78
- HEADER_LINE=$(grep -A2 "Quick Tasks Completed" .planning/STATE.md 2>/dev/null | grep "^|" | head -1)
79
- # Count columns: number of | separators minus 1 gives column count
80
- COL_COUNT=$(echo "$HEADER_LINE" | awk -F'|' '{print NF-1}')
81
-
82
- if [ "$COL_COUNT" -eq 5 ] && echo "$HEADER_LINE" | grep -qi "Description" && echo "$HEADER_LINE" | grep -qi "Commit" && echo "$HEADER_LINE" | grep -qi "Directory"; then
83
- # 5-column schema from quick.md Step 7: | # | Description | Date | Commit | Directory |
84
- # Determine the next row number by counting existing data rows (non-separator, non-header).
85
- NEXT_NUM=$(awk '/Quick Tasks Completed/{found=1} found && /^\|/ && !/^[|][-: |]*[|]$/ && !/Description/{count++} END{print count+1}' .planning/STATE.md 2>/dev/null || echo "1")
86
- # Get the latest commit hash (short)
87
- COMMIT_HASH=$(git rev-parse --short HEAD 2>/dev/null || echo "—")
88
- echo "| $NEXT_NUM | $TASK | $(date +%Y-%m-%d) | $COMMIT_HASH | — |" >> .planning/STATE.md
89
- else
90
- # Unrecognized table schema — skip to avoid appending a malformed row.
91
- echo "⚠ fast.md log_to_state: Quick Tasks Completed table has unrecognized schema (${COL_COUNT} columns); skipping STATE.md update."
92
- fi
78
+ gsd_run quick-tasks-append --task "$TASK" || echo "⚠ fast.md log_to_state: could not append Quick Tasks row (see message above); continuing."
93
79
  fi
94
80
  ```
95
81
  </step>
@@ -536,7 +536,7 @@ State: "Current phase is {X}. Milestone has {N} phases (highest: {Y})."
536
536
  | Condition | Meaning | Action |
537
537
  |-----------|---------|--------|
538
538
  | current phase < highest phase | More phases remain | Go to **Route C** |
539
- | current phase = highest phase | Milestone complete | Go to **Route D** |
539
+ | current phase = highest phase | All phases complete | Go to **Route D** |
540
540
 
541
541
  ---
542
542
 
@@ -602,7 +602,7 @@ NEXT_HAS_UI=$(echo "$NEXT_PHASE_SECTION" | grep -qi "UI hint.*yes" && echo "true
602
602
 
603
603
  ---
604
604
 
605
- **Route D: Milestone complete**
605
+ **Route D: All phases complete (milestone ready to close)**
606
606
 
607
607
  ```
608
608
  ---
@@ -971,6 +971,8 @@ Use `date` from init:
971
971
  | ${quick_id} | ${DESCRIPTION} | ${date} | ${commit_hash} | [${quick_id}-${slug}](./quick/${quick_id}-${slug}/) |
972
972
  ```
973
973
 
974
+ For a schema-safe append outside this workflow (e.g. from fast.md), `gsd-tools quick-tasks-append --task <text>` performs the equivalent write via the shared, schema-backed `appendQuickTaskRow` helper (#2133, ADR-2143 §3/§7).
975
+
974
976
  **7d. Update "Last activity" line:**
975
977
 
976
978
  Use `date` from init:
@@ -255,6 +255,8 @@ shell-interpolated). Exact invocation in `gsd-core/references/reviewer-instances
255
255
 
256
256
  For each selected CLI, invoke in sequence (not parallel — avoid rate limits):
257
257
 
258
+ **Timeout guidance (#2194):** prompt-fed source-grounded reviews are slow — measured ~570s for Codex at `xhigh` effort and ~525s for headless Claude on a large plan set. Each of the Gemini / Claude / Codex blocks below MUST be invoked with a high Bash `timeout:` — at least `900000` (15 min), and `1200000` (20 min) for Codex `xhigh` or headless Claude — so a lane is not killed mid-review. On Claude Code, raise the host cap via `BASH_MAX_TIMEOUT_MS` if a review can exceed it. A silent empty output after a long run is a **timeout kill, not a crash** — the Codex `0xc0000142` misdiagnosis persisted because the empty-output branches below cannot distinguish the two; treat an empty result on a slow lane as a dropped lane and re-run with more time rather than diagnosing a CLI/sandbox failure. A cross-AI review that silently drops a lane is blind in one eye.
259
+
258
260
  **Gemini:**
259
261
  ```bash
260
262
  if [ -n "$GEMINI_MODEL" ] && [ "$GEMINI_MODEL" != "null" ]; then
@@ -364,7 +366,12 @@ fi
364
366
  # prompt as an ARGUMENT, not stdin. A full review prompt can exceed the OS argument limit, so
365
367
  # reference the prompt file by path rather than inlining it. Capture stderr so a failure is
366
368
  # diagnosable instead of a silent empty result.
367
- CURSOR_PROMPT_ARG="Read the file at /tmp/gsd-review-prompt-{phase}.md in full and carry out the review request it contains. Output only the resulting markdown review. Do not edit any files."
369
+ # #2176: same absolute-root anchor as the Antigravity block — cursor-agent runs
370
+ # in the repo cwd, but repo-relative references in the assembled prompt still
371
+ # need an explicit root to resolve against. rev-parse (not bare pwd) so the
372
+ # anchor is correct even when /gsd:review is invoked from a repo subdirectory.
373
+ _CURSOR_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
374
+ CURSOR_PROMPT_ARG="Read the file at /tmp/gsd-review-prompt-{phase}.md in full and carry out the review request it contains. The repository under review is at $_CURSOR_ROOT — resolve every relative file path in the review request against that absolute root. Output only the resulting markdown review. Do not edit any files."
368
375
  cursor-agent -p --mode ask --trust --output-format text "$CURSOR_PROMPT_ARG" 2>/tmp/gsd-review-cursor-{phase}.err > /tmp/gsd-review-cursor-{phase}.md
369
376
  if [ ! -s /tmp/gsd-review-cursor-{phase}.md ]; then
370
377
  echo "Cursor review failed or returned empty output. stderr:" > /tmp/gsd-review-cursor-{phase}.md
@@ -456,7 +463,19 @@ if [ -n "$AGY_MODEL" ] && [ "$AGY_MODEL" != "null" ]; then
456
463
  else
457
464
  set --
458
465
  fi
459
- _AGY_PROMPT="Read the file at /tmp/gsd-review-prompt-{phase}.md in full and carry out the review request it contains. Output only the resulting markdown review. Do not edit any files."
466
+ # #2176: grant the reviewer the repo under review. Without --add-dir, agy's
467
+ # permission context never receives the cwd repo — the agent anchors on its own
468
+ # ~/.gemini/antigravity-cli/scratch dir and reviews the plan text in isolation
469
+ # (the exact failure the Review Instructions forbid). Capability-probed like the
470
+ # Codex bypass flag so an older agy without --add-dir still runs; the prompt
471
+ # anchor below keeps absolute-path reads possible on that fallback.
472
+ if agy --help 2>/dev/null | grep -q -- '--add-dir'; then
473
+ set -- "$@" --add-dir "$_AGY_WS"
474
+ fi
475
+ # #2176: anchor the prompt to the absolute repo root so repo-relative references
476
+ # in the assembled review prompt resolve even on the no---add-dir fallback, and
477
+ # require an explicit self-report if the reviewer still cannot read the repo.
478
+ _AGY_PROMPT="Read the file at /tmp/gsd-review-prompt-{phase}.md in full and carry out the review request it contains. The repository under review is at $_AGY_WS — resolve every relative file path in the review request against that absolute root and verify claims against those files. If you cannot read files under $_AGY_WS, begin your output with the exact line REVIEWED-WITHOUT-REPO-ACCESS before the review. Output only the resulting markdown review. Do not edit any files."
460
479
  # Capability-probe an external wall-clock killer (GNU coreutils `timeout` or the
461
480
  # macOS Homebrew `gtimeout`). Stock macOS ships NEITHER — a bare `timeout …` would
462
481
  # fail with rc 127 ("command not found") and silently lose the reviewer, so fall
@@ -523,6 +542,26 @@ if [ ! -s /tmp/gsd-review-antigravity-{phase}.md ]; then
523
542
  echo "If no agy run started, that is the pre-session-stall case: check whether a new ~/.gemini/antigravity-cli/brain/<conv-id>/ dir appeared within ~30s of launch."
524
543
  } > /tmp/gsd-review-antigravity-{phase}.md
525
544
  fi
545
+
546
+ # #2176: blind-review marker. Two tells that the reviewer ran without repo
547
+ # access: the prompt's mandated REVIEWED-WITHOUT-REPO-ACCESS self-report in the
548
+ # first lines of output, or the agent DECLARING the scratch dir as its
549
+ # workspace. Both patterns are anchored — the self-report to the head of the
550
+ # file, the scratch tell to a workspace-declaration phrasing — so a grounded
551
+ # review that merely QUOTES these strings (e.g. reviewing this very file) is
552
+ # never mis-stamped. Stamp a machine-readable marker so the Consensus Summary
553
+ # down-weights the review instead of counting an ungrounded verdict at full
554
+ # weight. (Temp file + mv, no in-place sed — BSD/GNU safe.)
555
+ if [ -s /tmp/gsd-review-antigravity-{phase}.md ] && \
556
+ { head -5 /tmp/gsd-review-antigravity-{phase}.md | grep -q 'REVIEWED-WITHOUT-REPO-ACCESS' || \
557
+ grep -qiE '(workspace|working) (directory|dir).{0,40}antigravity-cli/scratch' /tmp/gsd-review-antigravity-{phase}.md; }; then
558
+ {
559
+ echo "> [reviewed-without-repo-access] This reviewer ran without visibility into the repo under review — down-weight its verdict in the Consensus Summary."
560
+ echo ""
561
+ cat /tmp/gsd-review-antigravity-{phase}.md
562
+ } > /tmp/gsd-review-antigravity-{phase}.md.tmp && \
563
+ mv /tmp/gsd-review-antigravity-{phase}.md.tmp /tmp/gsd-review-antigravity-{phase}.md
564
+ fi
526
565
  ```
527
566
 
528
567
  **Ollama (local, OpenAI-compatible):**
@@ -835,7 +874,7 @@ trimmed_reviewers: # only present if at least one reviewer was trimmed
835
874
 
836
875
  ## Consensus Summary
837
876
 
838
- {synthesize common concerns across all reviewers. CodeRabbit is a diff-only reviewer (it never received the source-grounding prompt), so do not weight its verdict as a grounded plan review — fold in its diff findings, but base plan-level consensus on the prompt-fed reviewers.}
877
+ {synthesize common concerns across all reviewers. CodeRabbit is a diff-only reviewer (it never received the source-grounding prompt), so do not weight its verdict as a grounded plan review — fold in its diff findings, but base plan-level consensus on the prompt-fed reviewers. A reviewer output carrying the `[reviewed-without-repo-access]` marker (or beginning with `REVIEWED-WITHOUT-REPO-ACCESS`) ran without repo access (#2176) — treat it the same way: note its concerns, but do not count its verdict at full consensus weight.}
839
878
 
840
879
  ### Agreed Strengths
841
880
  {strengths mentioned by 2+ reviewers}
@@ -108,7 +108,7 @@ Agent(
108
108
  "<files_to_read>{PLAN, SUMMARY, impl files, SECURITY.md}</files_to_read>" +
109
109
  "<threat_register>{threat register}</threat_register>" +
110
110
  "<config>asvs_level: {SECURITY_ASVS}, block_on: {SECURITY_BLOCK_ON}</config>" +
111
- "<constraints>Never modify implementation files. Verify mitigations exist — do not scan for new threats. Escalate implementation gaps.</constraints>" +
111
+ "<constraints>Never modify implementation files. Verify mitigations exist — do not scan for new threats. Escalate implementation gaps. Return a structured verdict only — do NOT write SECURITY.md (the orchestrator owns the file write).</constraints>" +
112
112
  "${AGENT_SKILLS_AUDITOR}",
113
113
  subagent_type="gsd-security-auditor",
114
114
  model="{AUDITOR_MODEL}",
@@ -394,9 +394,15 @@ gsd_run query state.update "Last Activity" "$(date +%Y-%m-%d)"
394
394
  gsd_run query state.update "Status" "Phase ${PHASE_NUMBER} shipped — PR #${PR_NUMBER}"
395
395
  ```
396
396
 
397
- If `commit_docs` is true:
397
+ If `commit_docs` is true, commit the ship-note AND push it onto the PR branch so
398
+ it reaches the default branch when the PR merges. Without this push the ship-note
399
+ commit stays local-only and is silently discarded when the branch is deleted on
400
+ merge (#2138). The `[ci skip]` trailer suppresses the redundant pipeline the push
401
+ would otherwise trigger (GitHub honors `[ci skip]` / `[skip ci]`):
402
+
398
403
  ```bash
399
- gsd_run query commit "docs(${padded_phase}): ship phase ${PHASE_NUMBER} — PR #${PR_NUMBER}" --files .planning/STATE.md
404
+ gsd_run query commit "docs(${padded_phase}): ship phase ${PHASE_NUMBER} — PR #${PR_NUMBER} [ci skip]" --files .planning/STATE.md
405
+ git push origin ${CURRENT_BRANCH} 2>&1 || echo "⚠ track_shipping: ship-note push failed — it is local-only; rerun: git push origin ${CURRENT_BRANCH}"
400
406
  ```
401
407
  </step>
402
408
 
@@ -456,7 +456,7 @@ Write to: `{phase_dir}/{padded_phase}-SPEC.md`
456
456
 
457
457
  ```bash
458
458
  git add "${phase_dir}/${padded_phase}-SPEC.md"
459
- git commit -m "spec(phase-${phase_number}): add SPEC.md for ${phase_name} — ${requirement_count} requirements (#2213)"
459
+ git commit -m "spec(phase-${phase_number}): add SPEC.md for ${phase_name} — ${requirement_count} requirements (#2213)" -- "${phase_dir}/${padded_phase}-SPEC.md"
460
460
  ```
461
461
 
462
462
  If `commit_docs` is false: Skip commit. Note that SPEC.md was written but not committed.
@@ -597,7 +597,7 @@ Do NOT auto-invoke any further slash commands.
597
597
 
598
598
  ---
599
599
 
600
- **Route B: Milestone complete (all phases done)**
600
+ **Route B: All phases complete (milestone ready to close)**
601
601
 
602
602
  **This route is only reached when:**
603
603
  - `is_last_phase: true` AND no other active workstreams exist (or flat mode)