@opengsd/gsd-core 1.17.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 (176) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/agents/gsd-advisor-researcher.compact.md +1 -1
  4. package/agents/gsd-advisor-researcher.md +1 -1
  5. package/agents/gsd-assumptions-analyzer.compact.md +1 -1
  6. package/agents/gsd-assumptions-analyzer.md +1 -1
  7. package/agents/gsd-code-fixer.compact.md +1 -1
  8. package/agents/gsd-code-fixer.md +1 -1
  9. package/agents/gsd-code-reviewer.compact.md +1 -1
  10. package/agents/gsd-code-reviewer.md +1 -1
  11. package/agents/gsd-codebase-mapper.compact.md +1 -1
  12. package/agents/gsd-codebase-mapper.md +1 -1
  13. package/agents/gsd-debugger.md +1 -1
  14. package/agents/gsd-doc-verifier.compact.md +4 -4
  15. package/agents/gsd-doc-verifier.md +4 -3
  16. package/agents/gsd-doc-writer.compact.md +23 -5
  17. package/agents/gsd-doc-writer.md +19 -6
  18. package/agents/gsd-eval-auditor.compact.md +1 -1
  19. package/agents/gsd-eval-auditor.md +1 -1
  20. package/agents/gsd-executor.md +7 -3
  21. package/agents/gsd-integration-checker.compact.md +1 -1
  22. package/agents/gsd-integration-checker.md +1 -1
  23. package/agents/gsd-nyquist-auditor.compact.md +1 -1
  24. package/agents/gsd-nyquist-auditor.md +1 -1
  25. package/agents/gsd-phase-researcher.md +1 -1
  26. package/agents/gsd-plan-checker.md +2 -1
  27. package/agents/gsd-planner.md +5 -4
  28. package/agents/gsd-project-researcher.compact.md +1 -1
  29. package/agents/gsd-project-researcher.md +1 -1
  30. package/agents/gsd-research-synthesizer.compact.md +5 -5
  31. package/agents/gsd-research-synthesizer.md +5 -5
  32. package/agents/gsd-roadmapper.compact.md +1 -1
  33. package/agents/gsd-roadmapper.md +1 -1
  34. package/agents/gsd-security-auditor.compact.md +1 -1
  35. package/agents/gsd-security-auditor.md +1 -1
  36. package/agents/gsd-ui-auditor.compact.md +1 -1
  37. package/agents/gsd-ui-auditor.md +1 -1
  38. package/agents/gsd-ui-checker.compact.md +1 -1
  39. package/agents/gsd-ui-checker.md +1 -1
  40. package/agents/gsd-ui-researcher.compact.md +13 -1
  41. package/agents/gsd-ui-researcher.md +28 -1
  42. package/agents/gsd-verifier.md +1 -1
  43. package/bin/install.js +38 -2
  44. package/commands/gsd/code-review.md +8 -0
  45. package/commands/gsd/pause-work.md +1 -1
  46. package/gsd-core/bin/gsd-tools.cjs +18 -11
  47. package/gsd-core/bin/lib/capability-ledger.cjs +25 -0
  48. package/gsd-core/bin/lib/capability-lock.cjs +29 -7
  49. package/gsd-core/bin/lib/capability-registry.cjs +70 -64
  50. package/gsd-core/bin/lib/capability-source.cjs +3 -1
  51. package/gsd-core/bin/lib/capability-state.cjs +6 -3
  52. package/gsd-core/bin/lib/capability-validator.cjs +6 -4
  53. package/gsd-core/bin/lib/commands.cjs +5 -1
  54. package/gsd-core/bin/lib/config-loader.cjs +3 -3
  55. package/gsd-core/bin/lib/decision-coverage-support.cjs +19 -0
  56. package/gsd-core/bin/lib/gate-decision-coverage-plan.cjs +15 -0
  57. package/gsd-core/bin/lib/gate-decision-coverage-verify.cjs +13 -0
  58. package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +23 -3
  59. package/gsd-core/bin/lib/gate-predicate.cjs +8 -12
  60. package/gsd-core/bin/lib/gate-tdd-review-checkpoint.cjs +60 -5
  61. package/gsd-core/bin/lib/init.cjs +11 -4
  62. package/gsd-core/bin/lib/install-effort-resolver.cjs +2 -2
  63. package/gsd-core/bin/lib/install-model-override-resolver.cjs +4 -3
  64. package/gsd-core/bin/lib/install-profiles.cjs +5 -3
  65. package/gsd-core/bin/lib/phase.cjs +120 -11
  66. package/gsd-core/bin/lib/report-parser.cjs +57 -1
  67. package/gsd-core/bin/lib/review-lane-invocation.cjs +40 -0
  68. package/gsd-core/bin/lib/review-reviewer-selection.cjs +20 -1
  69. package/gsd-core/bin/lib/roadmap.cjs +56 -5
  70. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +47 -2
  71. package/gsd-core/bin/lib/runtime-homes.cjs +18 -0
  72. package/gsd-core/bin/lib/shell-command-projection.cjs +2 -0
  73. package/gsd-core/bin/lib/smart-entry.cjs +24 -16
  74. package/gsd-core/bin/lib/state-document.cjs +37 -3
  75. package/gsd-core/bin/lib/state-transition.cjs +31 -10
  76. package/gsd-core/bin/lib/state.cjs +90 -60
  77. package/gsd-core/bin/lib/task-command-router.cjs +2 -3
  78. package/gsd-core/bin/lib/tdd-red-evidence.cjs +5 -0
  79. package/gsd-core/bin/lib/tdd-test-files.cjs +42 -0
  80. package/gsd-core/bin/lib/verify.cjs +36 -1
  81. package/gsd-core/references/agent-contracts.md +1 -1
  82. package/gsd-core/references/agent-skills-bootstrap.md +26 -7
  83. package/gsd-core/references/git-integration.md +3 -3
  84. package/gsd-core/references/planner-reversibility.md +18 -4
  85. package/gsd-core/references/planner-verify-command-grounding.md +18 -0
  86. package/gsd-core/references/tdd.md +4 -3
  87. package/gsd-core/references/verifier-phase-gates.md +8 -1
  88. package/gsd-core/workflows/add-backlog.md +10 -2
  89. package/gsd-core/workflows/add-phase.md +5 -1
  90. package/gsd-core/workflows/add-tests.md +5 -1
  91. package/gsd-core/workflows/add-todo.md +5 -1
  92. package/gsd-core/workflows/ai-integration-phase.md +5 -1
  93. package/gsd-core/workflows/audit-milestone.md +21 -9
  94. package/gsd-core/workflows/autonomous/templates/minimal-context.md +41 -0
  95. package/gsd-core/workflows/autonomous.md +52 -64
  96. package/gsd-core/workflows/check-todos.md +5 -1
  97. package/gsd-core/workflows/code-review-fix.md +5 -1
  98. package/gsd-core/workflows/code-review.md +5 -1
  99. package/gsd-core/workflows/complete-milestone/detail/elaboration.md +5 -1
  100. package/gsd-core/workflows/complete-milestone.md +25 -5
  101. package/gsd-core/workflows/debug.md +5 -1
  102. package/gsd-core/workflows/diagnose-issues.md +5 -1
  103. package/gsd-core/workflows/discuss-phase/modes/chain.md +5 -1
  104. package/gsd-core/workflows/discuss-phase-assumptions.md +11 -3
  105. package/gsd-core/workflows/discuss-phase.md +5 -1
  106. package/gsd-core/workflows/docs-update/detail/elaboration.md +7 -6
  107. package/gsd-core/workflows/docs-update.md +27 -5
  108. package/gsd-core/workflows/edit-phase.md +5 -1
  109. package/gsd-core/workflows/eval-review.md +11 -3
  110. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +5 -1
  111. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +12 -6
  112. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +19 -2
  113. package/gsd-core/workflows/execute-phase/steps/regression-gate.md +18 -1
  114. package/gsd-core/workflows/execute-phase/steps/verify-phase-goal.md +5 -1
  115. package/gsd-core/workflows/execute-phase.md +37 -6
  116. package/gsd-core/workflows/execute-plan.md +8 -4
  117. package/gsd-core/workflows/extract-learnings.md +6 -2
  118. package/gsd-core/workflows/forensics.md +5 -1
  119. package/gsd-core/workflows/import.md +5 -1
  120. package/gsd-core/workflows/insert-phase.md +5 -1
  121. package/gsd-core/workflows/manager.md +10 -2
  122. package/gsd-core/workflows/map-codebase.md +5 -1
  123. package/gsd-core/workflows/milestone-summary.md +10 -2
  124. package/gsd-core/workflows/new-milestone.md +13 -5
  125. package/gsd-core/workflows/new-project.md +9 -5
  126. package/gsd-core/workflows/next.md +8 -4
  127. package/gsd-core/workflows/pause-work.md +3 -3
  128. package/gsd-core/workflows/plan-phase/steps/auto-advance-check.md +70 -0
  129. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +5 -1
  130. package/gsd-core/workflows/plan-phase/steps/closed-phase-gate.md +6 -2
  131. package/gsd-core/workflows/plan-phase.md +41 -73
  132. package/gsd-core/workflows/plan-review-convergence.md +17 -5
  133. package/gsd-core/workflows/plant-seed.md +8 -4
  134. package/gsd-core/workflows/progress.md +6 -2
  135. package/gsd-core/workflows/quick-batch.md +5 -1
  136. package/gsd-core/workflows/quick.md +22 -7
  137. package/gsd-core/workflows/remove-phase.md +5 -1
  138. package/gsd-core/workflows/resume-project.md +5 -1
  139. package/gsd-core/workflows/review.md +6 -2
  140. package/gsd-core/workflows/scan.md +5 -1
  141. package/gsd-core/workflows/secure-phase.md +6 -2
  142. package/gsd-core/workflows/settings.md +3 -3
  143. package/gsd-core/workflows/ship.md +13 -10
  144. package/gsd-core/workflows/thread.md +10 -2
  145. package/gsd-core/workflows/transition.md +5 -1
  146. package/gsd-core/workflows/ui-phase.md +5 -1
  147. package/gsd-core/workflows/ui-review.md +6 -2
  148. package/gsd-core/workflows/ultraplan-phase.md +5 -1
  149. package/gsd-core/workflows/update.md +5 -1
  150. package/gsd-core/workflows/validate-phase.md +6 -2
  151. package/gsd-core/workflows/verify-work/detail/elaboration.md +1 -1
  152. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +5 -1
  153. package/gsd-core/workflows/verify-work.md +11 -3
  154. package/hooks/dist/gsd-agent-isolation-guard.js +3 -1
  155. package/hooks/dist/gsd-cursor-subagent-start.js +2 -1
  156. package/hooks/dist/gsd-secret-read-guard.js +72 -14
  157. package/hooks/dist/gsd-validate-commit.sh +27 -5
  158. package/hooks/gsd-agent-isolation-guard.js +3 -1
  159. package/hooks/gsd-cursor-subagent-start.js +2 -1
  160. package/hooks/gsd-secret-read-guard.js +72 -14
  161. package/hooks/gsd-validate-commit.sh +27 -5
  162. package/package.json +1 -1
  163. package/scripts/audit-workflow-script-paths.cjs +1 -1
  164. package/scripts/diagnose-validate-commit-in-tier.cjs +270 -0
  165. package/scripts/diagnose-validate-commit-phases.cjs +348 -0
  166. package/scripts/diagnose-windows-spawn-watcher.cjs +317 -0
  167. package/scripts/docs-guard-registry.cjs +2 -0
  168. package/scripts/lib/macos-conformance-tier.generated.cjs +9 -0
  169. package/scripts/lib/platform-conformance-tier.generated.cjs +16 -0
  170. package/scripts/lint-phase-arg-assignment.cjs +2 -2
  171. package/scripts/lint-workflow-shellcheck-baseline.json +16 -16
  172. package/scripts/release-tarball-smoke.cjs +1 -0
  173. package/scripts/xtrace-phase-timing.cjs +205 -0
  174. package/skills/gsd-code-review/SKILL.md +8 -0
  175. package/skills/gsd-pause-work/SKILL.md +1 -1
  176. package/vscode/package.json +1 -1
@@ -1067,6 +1067,62 @@ function describeGoalShapedTitle(description) {
1067
1067
  return (`description looks goal-shaped, not title-shaped (${reasons}). It was written verbatim ` +
1068
1068
  `as the phase title; consider a short title with the detail moved to **Goal:**.`);
1069
1069
  }
1070
+ /**
1071
+ * Phase numbers the roadmap itself declares — a section header or a roadmap bullet — sentinels
1072
+ * excluded. This is deliberately NOT the allocation set: allocation also counts on-disk directories
1073
+ * and sibling-worktree phases so a new number cannot collide, but a `Depends on:` line has to point
1074
+ * at something a reader of THIS roadmap can find (#5113).
1075
+ */
1076
+ function roadmapDeclaredPhaseNums(rawContent) {
1077
+ const { regex: headerPattern, phaseNumGroup } = buildPhaseHeadingScanRegex(PHASE_HEADING_BASELINE.LABEL_ONLY);
1078
+ const bulletPattern = /^[ \t]*-[ \t]*\[[^\]]{0,200}\][ \t]*\*{0,2}Phase[ \t]+(\d+)(?=[:.\s*]|$)/gim;
1079
+ const nums = new Set();
1080
+ let m;
1081
+ while ((m = headerPattern.exec(rawContent)) !== null) {
1082
+ const num = parseInt(m[phaseNumGroup], 10);
1083
+ if (!isSentinelPhaseId(num))
1084
+ nums.add(num);
1085
+ }
1086
+ while ((m = bulletPattern.exec(rawContent)) !== null) {
1087
+ const num = parseInt(m[1], 10);
1088
+ if (!isSentinelPhaseId(num))
1089
+ nums.add(num);
1090
+ }
1091
+ return [...nums];
1092
+ }
1093
+ /**
1094
+ * The `**Depends on:**` line for a freshly added numeric phase: the nearest lower phase the roadmap
1095
+ * declares, or `Nothing` when it declares none. It was `newId - 1` regardless, which named a phase
1096
+ * that exists only on a sibling branch, as an orphan directory, or not at all when numbering has a
1097
+ * gap (#5113). Custom-named phases carry no dependency line.
1098
+ */
1099
+ function dependsOnLine(newPhaseId, declared) {
1100
+ if (typeof newPhaseId !== 'number')
1101
+ return '';
1102
+ let nearest = null;
1103
+ for (const n of declared)
1104
+ if (n < newPhaseId && (nearest === null || n > nearest))
1105
+ nearest = n;
1106
+ return `\n**Depends on:** ${nearest === null ? 'Nothing' : `Phase ${nearest}`}`;
1107
+ }
1108
+ /**
1109
+ * Splice a phase entry into `rawContent` at `at`, changing nothing else: the blank lines around the
1110
+ * block and its line endings are the block's own, so the surrounding document is published byte for
1111
+ * byte (#5113). `block` is LF text that starts and ends with a newline.
1112
+ */
1113
+ function spliceRoadmapBlock(rawContent, atOffset, block) {
1114
+ const eol = rawContent.includes('\r\n') ? '\r\n' : '\n';
1115
+ // The insertion offset is found by searching for '\n---', which lands between the CR and the LF of
1116
+ // a CRLF pair; splice before the pair instead of through it.
1117
+ const at = rawContent[atOffset - 1] === '\r' && rawContent[atOffset] === '\n' ? atOffset - 1 : atOffset;
1118
+ const before = rawContent.slice(0, at);
1119
+ const after = rawContent.slice(at);
1120
+ const body = block.replace(/^\n+/, '').replace(/\n+$/, '\n').replace(/\n/g, eol);
1121
+ const blankBefore = before === '' || /(?:\r?\n){2}$/.test(before);
1122
+ const lead = blankBefore ? '' : /\n$/.test(before) ? eol : eol + eol;
1123
+ const tail = after === '' || /^\r?\n/.test(after) ? '' : eol;
1124
+ return before + lead + body + tail + after;
1125
+ }
1070
1126
  /**
1071
1127
  * #3163: compute the byte offset in `rawContent` where a new `### Phase N:`
1072
1128
  * entry should be inserted — at the end of the active phase list, scoped to the
@@ -1310,13 +1366,11 @@ function cmdPhaseAdd(cwd, description, raw, customId) {
1310
1366
  const dirPath = node_path_1.default.join(planningDir(cwd), 'phases', _dirName);
1311
1367
  (0, shell_command_projection_cjs_1.platformEnsureDir)(dirPath);
1312
1368
  (0, shell_command_projection_cjs_1.platformWriteSync)(node_path_1.default.join(dirPath, '.gitkeep'), '');
1313
- const dependsOn = config.phase_naming === 'custom'
1314
- ? ''
1315
- : `\n**Depends on:** Phase ${typeof _newPhaseId === 'number' ? _newPhaseId - 1 : 'TBD'}`;
1369
+ const dependsOn = config.phase_naming === 'custom' ? '' : dependsOnLine(_newPhaseId, roadmapDeclaredPhaseNums(rawContent));
1316
1370
  const phaseEntry = `\n### Phase ${_newPhaseId}: ${description}\n\n**Goal:** [To be planned]\n**Requirements**: TBD${dependsOn}\n**Plans:** 0 plans\n\nPlans:\n- [ ] TBD (run ${(0, runtime_slash_cjs_1.formatGsdSlash)('plan-phase', (0, runtime_slash_cjs_1.resolveRuntime)(cwd))} ${_newPhaseId} to break down)\n`;
1317
1371
  const insertAt = phaseEntryInsertOffset(rawContent, cwd);
1318
- const updatedContent = rawContent.slice(0, insertAt) + phaseEntry + rawContent.slice(insertAt);
1319
- (0, shell_command_projection_cjs_1.platformWriteSync)(roadmapPath, updatedContent);
1372
+ const updatedContent = spliceRoadmapBlock(rawContent, insertAt, phaseEntry);
1373
+ (0, shell_command_projection_cjs_1.platformWriteSync)(roadmapPath, updatedContent, { verbatim: true });
1320
1374
  return { newPhaseId: _newPhaseId, dirName: _dirName };
1321
1375
  });
1322
1376
  const titleWarning = describeGoalShapedTitle(description);
@@ -1432,12 +1486,10 @@ function cmdPhaseAddBatch(cwd, descriptions, raw) {
1432
1486
  const dirPath = node_path_1.default.join(planningDir(cwd), 'phases', dirName);
1433
1487
  (0, shell_command_projection_cjs_1.platformEnsureDir)(dirPath);
1434
1488
  (0, shell_command_projection_cjs_1.platformWriteSync)(node_path_1.default.join(dirPath, '.gitkeep'), '');
1435
- const dependsOn = config.phase_naming === 'custom'
1436
- ? ''
1437
- : `\n**Depends on:** Phase ${typeof newPhaseId === 'number' ? newPhaseId - 1 : 'TBD'}`;
1489
+ const dependsOn = config.phase_naming === 'custom' ? '' : dependsOnLine(newPhaseId, roadmapDeclaredPhaseNums(rawContent));
1438
1490
  const phaseEntry = `\n### Phase ${newPhaseId}: ${description}\n\n**Goal:** [To be planned]\n**Requirements**: TBD${dependsOn}\n**Plans:** 0 plans\n\nPlans:\n- [ ] TBD (run ${(0, runtime_slash_cjs_1.formatGsdSlash)('plan-phase', (0, runtime_slash_cjs_1.resolveRuntime)(cwd))} ${newPhaseId} to break down)\n`;
1439
1491
  const insertAt = phaseEntryInsertOffset(rawContent, cwd);
1440
- rawContent = rawContent.slice(0, insertAt) + phaseEntry + rawContent.slice(insertAt);
1492
+ rawContent = spliceRoadmapBlock(rawContent, insertAt, phaseEntry);
1441
1493
  added.push({
1442
1494
  phase_number: typeof newPhaseId === 'number' ? newPhaseId : String(newPhaseId),
1443
1495
  padded: typeof newPhaseId === 'number' ? String(newPhaseId).padStart(2, '0') : String(newPhaseId),
@@ -1447,7 +1499,7 @@ function cmdPhaseAddBatch(cwd, descriptions, raw) {
1447
1499
  naming_mode: config.phase_naming,
1448
1500
  });
1449
1501
  }
1450
- (0, shell_command_projection_cjs_1.platformWriteSync)(roadmapPath, rawContent);
1502
+ (0, shell_command_projection_cjs_1.platformWriteSync)(roadmapPath, rawContent, { verbatim: true });
1451
1503
  return added;
1452
1504
  });
1453
1505
  output({ phases: results, count: results.length }, raw);
@@ -3061,6 +3113,42 @@ function formatRequirementsLineWarning(phaseNum, rawLine, analysis) {
3061
3113
  oversized,
3062
3114
  };
3063
3115
  }
3116
+ /**
3117
+ * #4972: advisory gaps for the phase artifacts that active `verify:post` steps
3118
+ * declared `onError: "halt"` and `produces` (SECURITY.md, VALIDATION.md).
3119
+ * Missing file, an unresolved `status: draft`, or (SECURITY.md) `threats_open`
3120
+ * above zero. Any other `status` spelling counts as closed — the audit skills
3121
+ * do not pin one value, so this does not invent a vocabulary.
3122
+ */
3123
+ function auditArtifactWarnings(cwd, phaseFullDir) {
3124
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- lazy: loop-resolver pulls in the capability registry
3125
+ const loopResolver = require('./loop-resolver.cjs');
3126
+ const out = [];
3127
+ const files = node_fs_1.default.readdirSync(phaseFullDir);
3128
+ for (const hook of loopResolver.resolveActiveHooksForPoint(cwd, 'verify:post').activeHooks) {
3129
+ if (hook.kind !== 'step' || hook.onError !== 'halt')
3130
+ continue;
3131
+ for (const artifact of hook.produces ?? []) {
3132
+ if (!/^[A-Z][A-Z0-9-]*\.md$/.test(artifact))
3133
+ continue;
3134
+ const skill = hook.ref?.skill ?? hook.capId;
3135
+ const found = files.filter((f) => f === artifact || f.endsWith(`-${artifact}`)).sort()[0];
3136
+ if (!found) {
3137
+ out.push(`${artifact}: missing — the ${hook.capId} audit (/gsd:${skill}) never ran for this phase, and a verify:post step cannot block the close (#4972)`);
3138
+ continue;
3139
+ }
3140
+ const fm = frontmatterMod.extractFrontmatter(node_fs_1.default.readFileSync(node_path_1.default.join(phaseFullDir, found), 'utf-8'));
3141
+ if (typeof fm['status'] === 'string' && fm['status'].trim().toLowerCase() === 'draft') {
3142
+ out.push(`${found}: status is still draft — the ${hook.capId} audit (/gsd:${skill}) was not completed (#4972)`);
3143
+ }
3144
+ const open = Number(fm['threats_open']);
3145
+ if (artifact === 'SECURITY.md' && Number.isFinite(open) && open > 0) {
3146
+ out.push(`${found}: ${open} threat(s) still open (#4972)`);
3147
+ }
3148
+ }
3149
+ }
3150
+ return out;
3151
+ }
3064
3152
  function cmdPhaseComplete(cwd, phaseNum, raw) {
3065
3153
  if (!phaseNum) {
3066
3154
  error('phase number required for phase complete');
@@ -3274,6 +3362,23 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
3274
3362
  /* best-effort, same posture as the #2245 pre-scan above: an unreadable
3275
3363
  * SUMMARY means one fewer advisory this run, never a blocked completion. */
3276
3364
  }
3365
+ // #4972: a `verify:post` STEP declared `onError: "halt"` that produces a
3366
+ // phase artifact (security -> SECURITY.md, nyquist -> VALIDATION.md) reads as
3367
+ // a blocking precondition, but a step is advisory by construction (ADR-894
3368
+ // "steps are additive, gates block") and the workflow only prints a line the
3369
+ // operator can ignore — so a phase could close with the audit never run. This
3370
+ // verb is the one place every close passes through, so it names the gap on
3371
+ // the same ADVISORY `warnings[]` channel as the #2572 check above. Advisory,
3372
+ // not a refusal: both capabilities are on by default, so a hard block here
3373
+ // would stop every existing project's next phase close; the security
3374
+ // threats-open gate at `ship:pre` is the enforced one and is untouched.
3375
+ try {
3376
+ warnings.push(...auditArtifactWarnings(cwd, phaseFullDir));
3377
+ }
3378
+ catch {
3379
+ /* best-effort, same posture as the advisories above: an unresolvable hook
3380
+ * set or unreadable artifact means one fewer advisory, never a blocked close. */
3381
+ }
3277
3382
  let nextPhaseNum = null;
3278
3383
  let nextPhaseName = null;
3279
3384
  let isLastPhase = true;
@@ -4230,7 +4335,11 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
4230
4335
  nextPhaseDisk,
4231
4336
  }, {
4232
4337
  clock: clock_cjs_1.realClock,
4233
- roadmapProvider: () => roadmapContent,
4338
+ // #4901: the body counter derives from the same current-milestone scope
4339
+ // as its frontmatter twin below (postCompletionRoadmapScope), not the
4340
+ // whole file, so the two writers cannot disagree on which
4341
+ // `## Progress` table they count.
4342
+ roadmapProvider: () => (roadmapContent !== null ? extractCurrentMilestone(roadmapContent, cwd) : null),
4234
4343
  sourcePath: statePath,
4235
4344
  });
4236
4345
  stateContent = completeResult.content;
@@ -6,7 +6,7 @@ exports.parseTestReport = parseTestReport;
6
6
  const tap_parser_cjs_1 = require("./vendor/tap-parser.cjs");
7
7
  const saxes_cjs_1 = require("./vendor/saxes.cjs");
8
8
  exports.ReportFormat = {
9
- Tap: 'tap', Junit: 'junit', SwiftTesting: 'swift-testing', Unittest: 'unittest', Unknown: 'unknown',
9
+ Tap: 'tap', Junit: 'junit', SwiftTesting: 'swift-testing', Unittest: 'unittest', GoTest: 'go-test', Unknown: 'unknown',
10
10
  };
11
11
  exports.TestStatus = { Passed: 'passed', Failed: 'failed', Skipped: 'skipped', Todo: 'todo' };
12
12
  function parseTap(output) {
@@ -247,7 +247,63 @@ function parseUnittest(output) {
247
247
  report.valid = report.issues.length === 0;
248
248
  return report;
249
249
  }
250
+ /**
251
+ * `go test` text output (#5106). Plain console text, so the shape is matched by line:
252
+ * --- FAIL: TestName (0.00s) one per failed test (every test with -v; subtests indented)
253
+ * FAIL\texample.com/pkg\t0.004s one package summary per package, after that package's tests
254
+ * FAIL\texample.com/pkg [build failed] the package did not compile — nothing asserted anything
255
+ * A test's name carries no package, so each test is also known as `<package>.<name>`, attributed to
256
+ * the next package summary line. The same name failing in two packages is ambiguous until qualified.
257
+ * `go test -json` is not text and stays unsupported.
258
+ */
259
+ const GO_TEST_RESULT = /^[ \t]*-{3} (PASS|FAIL|SKIP): (\S+) \(\d+(?:\.\d+)?s\)$/; // -{3}: not a literal fence, see frontmatter-fence-drift
260
+ const GO_PACKAGE_LINE = /^(ok|FAIL|\?)[ \t]+(\S+)[ \t]+(?:\d+(?:\.\d+)?s\b|\(cached\)|\[([a-z ]+)\])/;
261
+ function isGoTestOutput(output) {
262
+ if (/^TAP version \d+/.test(output.trimStart()))
263
+ return false;
264
+ return output.split(/\r?\n/).some((line) => GO_TEST_RESULT.test(line) || GO_PACKAGE_LINE.test(line));
265
+ }
266
+ function parseGoTest(output) {
267
+ const report = { format: exports.ReportFormat.GoTest, valid: true, tests: [], issues: [] };
268
+ let segment = [];
269
+ let packages = 0;
270
+ for (const line of output.split(/\r?\n/)) {
271
+ const result = GO_TEST_RESULT.exec(line);
272
+ if (result) {
273
+ const name = result[2];
274
+ const status = result[1] === 'FAIL' ? exports.TestStatus.Failed : result[1] === 'SKIP' ? exports.TestStatus.Skipped : exports.TestStatus.Passed;
275
+ if (!segment.some((test) => test.name === name && test.status === status)) {
276
+ segment.push({ name, identities: [name], group: null, groupIdentities: [], status });
277
+ }
278
+ continue;
279
+ }
280
+ const pkg = GO_PACKAGE_LINE.exec(line);
281
+ if (!pkg)
282
+ continue;
283
+ packages++;
284
+ const [, outcome, importPath, bracket] = pkg;
285
+ if (outcome === 'FAIL' && (bracket === 'build failed' || bracket === 'setup failed')) {
286
+ report.buildFailed = true;
287
+ report.issues.push(`go test package ${importPath} ${bracket}`);
288
+ }
289
+ else if (outcome === 'ok' && segment.some((test) => test.status === exports.TestStatus.Failed)) {
290
+ report.issues.push(`go test package ${importPath} is ok but its output reports a failed test`);
291
+ }
292
+ for (const test of segment)
293
+ test.identities.push(`${importPath}.${test.name}`);
294
+ report.tests.push(...segment);
295
+ segment = [];
296
+ }
297
+ if (packages === 0)
298
+ report.issues.push('Missing go test package summary');
299
+ if (segment.length > 0)
300
+ report.issues.push('go test results without a following package summary');
301
+ report.valid = report.issues.length === 0;
302
+ return report;
303
+ }
250
304
  const adapters = [
305
+ // Before TAP: a run whose first package is fine begins `ok <TAB>pkg`, which the TAP matcher reads as a point.
306
+ { matches: isGoTestOutput, parse: parseGoTest },
251
307
  { matches: (output) => /^(?:TAP version \d+|(?:not )?ok\b|1\.\.\d+|#)/.test(output.trimStart()), parse: parseTap },
252
308
  { matches: (output) => output.trimStart().startsWith('<'), parse: parseJunit },
253
309
  { matches: (output) => new RegExp(SWIFT_AGGREGATE.source, 'm').test(output), parse: parseSwiftTesting },
@@ -31,6 +31,7 @@ exports.resolveTimeoutMs = resolveTimeoutMs;
31
31
  exports.nativeTimeoutToken = nativeTimeoutToken;
32
32
  exports.isEmptyReview = isEmptyReview;
33
33
  exports.fileRefPrompt = fileRefPrompt;
34
+ exports.applyPromptOverride = applyPromptOverride;
34
35
  exports.artifactPaths = artifactPaths;
35
36
  exports.resolveLaneBudget = resolveLaneBudget;
36
37
  exports.resolveLanePlan = resolveLanePlan;
@@ -229,6 +230,45 @@ function fileRefPrompt(promptPath, repoRoot) {
229
230
  `review request against that absolute root. Output only the resulting markdown review. ` +
230
231
  `Do not edit any files.`);
231
232
  }
233
+ /**
234
+ * Re-point a resolved plan at a different prompt file (`review-lane invoke --prompt-file`, #5049).
235
+ *
236
+ * The override carries a budget-trimmed prompt, so it has to land wherever THIS lane actually
237
+ * reads its prompt, and that differs by channel:
238
+ * - `stdin`: the runner streams the file `plan.stdin` names.
239
+ * - `argv-file-ref`: the prompt PATH is embedded in a sentence in argv (`fileRefPrompt`), already
240
+ * expanded by the time an override can run. Swapping `plan.promptPath` alone changes nothing
241
+ * the CLI reads, which is how the override was dropped silently for cursor, antigravity and
242
+ * kimi-code.
243
+ * - `argv`: the bare path is an argv element.
244
+ * - `openai-http`: the runner reads `plan.promptPath`.
245
+ * Argv elements are matched whole (the file-ref sentence, or the bare path), never by substring,
246
+ * so an unrelated argument that merely contains the path is left alone. `promptPath` moves with
247
+ * argv: the antigravity handler re-derives the sentence it swaps from `plan.promptPath` and would
248
+ * otherwise stop recognising it. A plan that takes no prompt (`none`) is returned unchanged.
249
+ * Pure: returns a new plan and never mutates the one it is given.
250
+ */
251
+ function applyPromptOverride(plan, promptPath, repoRoot) {
252
+ if (plan.transport !== 'spawn')
253
+ return { ...plan, promptPath };
254
+ if (plan.stdin)
255
+ return { ...plan, stdin: promptPath, promptPath };
256
+ const oldRef = fileRefPrompt(plan.promptPath, repoRoot);
257
+ const newRef = fileRefPrompt(promptPath, repoRoot);
258
+ let replaced = false;
259
+ const argv = plan.argv.map((a) => {
260
+ if (a === oldRef) {
261
+ replaced = true;
262
+ return newRef;
263
+ }
264
+ if (a === plan.promptPath) {
265
+ replaced = true;
266
+ return promptPath;
267
+ }
268
+ return a;
269
+ });
270
+ return replaced ? { ...plan, argv, promptPath } : plan;
271
+ }
232
272
  /** Run-dir artifact paths. POSIX-joined: these are workflow-visible strings, not OS paths. */
233
273
  function artifactPaths(runDir, slug) {
234
274
  const base = String(runDir ?? '').replace(/\/+$/, '');
@@ -113,6 +113,22 @@ exports.KNOWN_REVIEWER_SLUGS = (() => {
113
113
  return [];
114
114
  }
115
115
  })();
116
+ /**
117
+ * #5359: map a lane flag alias (`agy` for `--agy`) to its canonical slug via the
118
+ * descriptor roster's declared `flags` (same source as `review-lane explicit-from-argv`).
119
+ * Guarded like KNOWN_REVIEWER_SLUGS: a load failure degrades to the plain error.
120
+ */
121
+ function laneSlugForAlias(name, lanes) {
122
+ try {
123
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
124
+ const roster = lanes ?? require('./review-lane-descriptor.cjs').REVIEWER_LANES;
125
+ // `l.slug !== name`: an undetected canonical slug (`--codex` flag) is not an alias.
126
+ return roster.find((l) => l.slug !== name && l.flags.includes(`--${name}`))?.slug;
127
+ }
128
+ catch {
129
+ return undefined;
130
+ }
131
+ }
116
132
  /** Instance names are lowercase slugs that must not shadow a built-in slug. */
117
133
  exports.INSTANCE_NAME_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
118
134
  function normalizeConfiguredDefaultReviewers(rawValue) {
@@ -239,7 +255,10 @@ function resolveReviewerSelection(input) {
239
255
  // Sorted so the error order does not depend on flag order on the command line.
240
256
  const missing = [...explicitFlags].filter((slug) => !detected.has(slug)).sort();
241
257
  for (const slug of missing) {
242
- errors.push(`explicit reviewer '${slug}' is not available on this host`);
258
+ const canonical = laneSlugForAlias(slug, input.lanes);
259
+ errors.push(canonical
260
+ ? `explicit reviewer '${slug}' is a flag alias; pass its canonical slug '${canonical}'`
261
+ : `explicit reviewer '${slug}' is not available on this host`);
243
262
  }
244
263
  // Guarded on the error count as it stood BEFORE this branch, so the
245
264
  // aggregate message fires under exactly the conditions it did previously.
@@ -1075,6 +1075,7 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
1075
1075
  // refusal below declines with the analyzer's own `missing_phase_details`
1076
1076
  // vocabulary and leaves ROADMAP.md byte-identical.
1077
1077
  let missingPhaseDetails = false;
1078
+ const warnings = [];
1078
1079
  withPlanningLock(cwd, () => {
1079
1080
  // #3957 (B9.4): captured BEFORE any transform runs, so the write/report
1080
1081
  // decision below reflects whether the transforms actually changed
@@ -1190,6 +1191,16 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
1190
1191
  }
1191
1192
  return text;
1192
1193
  });
1194
+ // #4947: a word-prefixed Phase cell (`Phase 68`) is deliberately not a
1195
+ // writable table target (#4247 — the read side ignores it too), but when the
1196
+ // phase's heading section is still written the command reports
1197
+ // `updated: true` and the unwritten Status cell looked like a silent bug.
1198
+ // The row stays byte-identical; the output now says so.
1199
+ const wordPrefixedCellRe = new RegExp(`^Phase\\s+${phasePattern}\\.?(?:\\s|$)`, 'i');
1200
+ const progressTable = tableRowFound ? null : (0, markdown_table_cjs_1.findTableWithColumns)(gateActiveRegion, ['Phase', 'Status']);
1201
+ if (progressTable && progressTable.rows.some((row) => wordPrefixedCellRe.test((row['Phase'] ?? '').trim()))) {
1202
+ warnings.push(`Progress table row for phase ${formatDiagnosticToken(String(phaseNum))} was not updated: its Phase cell is word-prefixed ("Phase N"), and only a bare phase number (N or N.) is recognized. Edit that row by hand or rewrite the cell as the bare number.`);
1203
+ }
1193
1204
  // Update plan count in phase detail section.
1194
1205
  // Three recognised forms (all tolerated; canonical template uses the first):
1195
1206
  // `**Plans**: N plans` — bold word + outer colon (gsd-core/templates/roadmap.md)
@@ -1247,6 +1258,13 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
1247
1258
  // wording, so it correctly falls through to arm 3 untouched. Kept as a
1248
1259
  // caller-side classifier (NOT seam grammar) reused below against the
1249
1260
  // `boldField` node's own parsed `value`.
1261
+ // #4947: a value that is exactly `TBD` is the planner's own "no plans yet"
1262
+ // marker, not an operator annotation — once plans exist it contradicts the
1263
+ // checklist this verb inserts right under it. `TBD — note` keeps its note
1264
+ // (row 5): on the bold path the note is the node's trailing span, which the
1265
+ // caller checks is empty; on the plain path the note is part of the value,
1266
+ // so the whole-value comparison already excludes it.
1267
+ const isBareTbd = (value) => value.trim() === 'TBD';
1250
1268
  const isTemplatePlaceholder = (value) => {
1251
1269
  const trimmed = value.trim();
1252
1270
  return /^\[\s*Number of plans\b[\s\S]*\]$/i.test(trimmed);
@@ -1272,6 +1290,35 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
1272
1290
  const m = value.match(/^\d+\s+plans?/i);
1273
1291
  return m ? m[0].length : -1;
1274
1292
  };
1293
+ // #4947: the pending-plan clause a human or planner glues after the count
1294
+ // (`; 2 gap-closure plans pending (48-18, 48-19)`) is DERIVED from the same
1295
+ // plan/summary facts this verb already computes, so a completed plan left
1296
+ // listed as pending contradicts the count the verb just wrote. The verb
1297
+ // owns that one clause: ids that now have a SUMMARY.md are removed, the
1298
+ // number is recounted, and an emptied clause is dropped. Everything else in
1299
+ // the tail (`in 11 waves`, free prose) stays the operator's — a wave total
1300
+ // does not change when a plan completes. A clause whose ids are not all
1301
+ // plan ids (`(see backlog)`) is not provably derived, so it is left alone.
1302
+ const summarizedPlanIds = new Set(phaseInfo.summaries
1303
+ .map((summaryFile) => summaryFile.replace('-SUMMARY.md', '').replace('SUMMARY.md', ''))
1304
+ .filter((id) => id !== ''));
1305
+ const pendingClauseRe = /;[ \t]*(\d+)[ \t]+((?:[\w-]+[ \t]+)*?)plans?[ \t]+pending[ \t]*\(([^()\r\n]*)\)/i;
1306
+ const planIdShape = /^\d+(?:\.\d+)?-\d+(?:\.\d+)?$/;
1307
+ const reconcilePendingClause = (tail) => {
1308
+ const m = pendingClauseRe.exec(tail);
1309
+ if (!m)
1310
+ return tail;
1311
+ const ids = m[3].split(',').map((id) => id.trim());
1312
+ if (ids.length === 0 || !ids.every((id) => planIdShape.test(id)))
1313
+ return tail;
1314
+ const stillPending = ids.filter((id) => !summarizedPlanIds.has(id));
1315
+ if (stillPending.length === ids.length)
1316
+ return tail;
1317
+ const replacement = stillPending.length === 0
1318
+ ? ''
1319
+ : `; ${stillPending.length} ${m[2]}plan${stillPending.length === 1 ? '' : 's'} pending (${stillPending.join(', ')})`;
1320
+ return tail.slice(0, m.index) + replacement + tail.slice(m.index + m[0].length);
1321
+ };
1275
1322
  roadmapContent = replaceInCurrentMilestone(roadmapContent, planSectionPattern, (sectionText) => {
1276
1323
  const parsed = (0, planning_document_cjs_1.parsePlanningDoc)(sectionText, 'ROADMAP.md');
1277
1324
  if (!parsed.ok) {
@@ -1302,12 +1349,12 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
1302
1349
  const plainFractionLen = fractionCountPrefixLength(plainValue);
1303
1350
  const plainBareLen = bareCountPrefixLength(plainValue);
1304
1351
  const plainCountPrefixLen = plainFractionLen >= 0 ? plainFractionLen : plainBareLen;
1305
- if (plainCountPrefixLen < 0 && !isTemplatePlaceholder(plainValue)) {
1352
+ if (plainCountPrefixLen < 0 && !isTemplatePlaceholder(plainValue) && !isBareTbd(plainValue)) {
1306
1353
  // Arm 3: freeform prose, TBD, a bracketed human annotation, or an
1307
1354
  // empty value — leave the section exactly as it was.
1308
1355
  return sectionText;
1309
1356
  }
1310
- const plainSuffix = plainCountPrefixLen >= 0 ? plainValue.slice(plainCountPrefixLen) : '';
1357
+ const plainSuffix = plainCountPrefixLen >= 0 ? reconcilePendingClause(plainValue.slice(plainCountPrefixLen)) : '';
1311
1358
  const newPlainLine = `${indent}Plans:${spacing}${planCountText}${plainSuffix}`;
1312
1359
  const start = plainMatch.index ?? sectionText.indexOf(whole);
1313
1360
  return sectionText.slice(0, start) + newPlainLine + sectionText.slice(start + whole.length);
@@ -1320,7 +1367,11 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
1320
1367
  const fractionPrefixLen = fractionCountPrefixLength(currentValue);
1321
1368
  const barePrefixLen = bareCountPrefixLength(currentValue);
1322
1369
  const countPrefixLen = fractionPrefixLen >= 0 ? fractionPrefixLen : barePrefixLen;
1323
- if (countPrefixLen < 0 && !isTemplatePlaceholder(currentValue)) {
1370
+ const fieldNode = parsed.value.nodes.find((n) => n.id === fieldId);
1371
+ const trailingText = fieldNode && fieldNode.kind === 'boldField'
1372
+ ? parsed.value.source.slice(fieldNode.trailingSpan.start, fieldNode.trailingSpan.end)
1373
+ : '';
1374
+ if (countPrefixLen < 0 && !isTemplatePlaceholder(currentValue) && !(isBareTbd(currentValue) && trailingText.trim() === '')) {
1324
1375
  // Arm 3: freeform prose, TBD, a bracketed human annotation, or an
1325
1376
  // empty value — leave the section exactly as it was.
1326
1377
  return sectionText;
@@ -1331,7 +1382,7 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
1331
1382
  // `setFieldValue`'s valueSpan/trailingSpan split additionally preserves
1332
1383
  // any EM-DASH-separated trailing annotation (#2853) automatically — no
1333
1384
  // separate "preserve trailing" branch needed for that shape.
1334
- const suffix = countPrefixLen >= 0 ? currentValue.slice(countPrefixLen) : '';
1385
+ const suffix = countPrefixLen >= 0 ? reconcilePendingClause(currentValue.slice(countPrefixLen)) : '';
1335
1386
  const newValueToWrite = planCountText + suffix;
1336
1387
  const staged = (0, planning_document_cjs_1.setFieldValue)(parsed.value, fieldId, newValueToWrite);
1337
1388
  if (!staged.ok) {
@@ -1490,7 +1541,7 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
1490
1541
  verification_stale_check_indeterminate: verificationStaleCheckIndeterminate,
1491
1542
  };
1492
1543
  if (updated) {
1493
- output({ updated: true, ...computed }, raw, `${summaryCount}/${planCount} ${status}`);
1544
+ output(warnings.length > 0 ? { updated: true, ...computed, warnings } : { updated: true, ...computed }, raw, `${summaryCount}/${planCount} ${status}`);
1494
1545
  }
1495
1546
  else if (missingPhaseDetails) {
1496
1547
  // #4247: honest refusal — the reason names the real condition (the
@@ -2923,8 +2923,13 @@ function computePathPrefix({ isGlobal, isOpencode, isWindowsHost: _isWindowsHost
2923
2923
  // See DEFECT.WINDOWS-PATH-LEAK-IN-MARKDOWN-CONTENT in CONTEXT.md.
2924
2924
  const posixTarget = (0, shell_command_projection_cjs_1.posixNormalize)(String(resolvedTarget));
2925
2925
  const posixHome = homeDir ? (0, shell_command_projection_cjs_1.posixNormalize)(String(homeDir)) : homeDir;
2926
- if (isGlobal && posixTarget.startsWith(posixHome) && !isOpencode) {
2927
- return '$HOME' + posixTarget.slice(posixHome.length) + '/';
2926
+ // #5123: `startsWith` alone treats a SIBLING directory whose name begins with
2927
+ // the home path (`/home/u2/cfg` under `/home/u`) as under home and yields the
2928
+ // broken `$HOMEu2/cfg/`. Require a path boundary.
2929
+ const underHome = typeof posixHome === 'string' && posixHome.length > 0
2930
+ && (posixTarget === posixHome || posixTarget.startsWith(posixHome.endsWith('/') ? posixHome : `${posixHome}/`));
2931
+ if (isGlobal && underHome && !isOpencode) {
2932
+ return '$HOME' + posixTarget.slice(posixHome.length).replace(/^(?!\/)(?=.)/, '/') + '/';
2928
2933
  }
2929
2934
  if (!isGlobal && projectRelative) {
2930
2935
  // An explicit local config dir need not be the descriptor's conventional
@@ -2938,6 +2943,44 @@ function computePathPrefix({ isGlobal, isOpencode, isWindowsHost: _isWindowsHost
2938
2943
  }
2939
2944
  return `${posixTarget}/`;
2940
2945
  }
2946
+ /**
2947
+ * #5123: retarget an installed artifact's `@` includes to the REAL install root.
2948
+ * Driven by the runtime descriptor's hostBehaviors, never by a runtime name:
2949
+ *
2950
+ * - `defaultHomeIncludeRoot` (Copilot, Antigravity): their converters hard-code
2951
+ * the DEFAULT home into includes, so with `--config-dir` the payload lives
2952
+ * elsewhere and `@~/<root>/gsd-core/…` points at nothing. `@~/<root>/` and
2953
+ * `@$HOME/<root>/` become `@<pathPrefix>`. A default install (pathPrefix is
2954
+ * already `$HOME/<root>/`) is returned unchanged, so it stays byte-identical.
2955
+ * - `retargetsGsdCoreProsePath` (Kimi): Kimi has no `@` include, so its prompt
2956
+ * converter turns `~/.claude/gsd-core` into the prose "GSD core", leaving
2957
+ * `@GSD core/x.md` pointing nowhere. A `GSD core/<path>` reference followed
2958
+ * by a path segment becomes the real `<pathPrefix>gsd-core/<path>`, with the
2959
+ * unsupported `@` dropped. Bare prose "GSD core" is not followed by `/`.
2960
+ *
2961
+ * `pathPrefix` is the install root as computePathPrefix derives it
2962
+ * (`$HOME/…/` under HOME, otherwise absolute), with a trailing slash. Pure and
2963
+ * idempotent: a second pass finds none of the literals it rewrites.
2964
+ */
2965
+ function retargetInstalledIncludes(content, behaviors, pathPrefix) {
2966
+ if (typeof content !== 'string' || content.length === 0)
2967
+ return content;
2968
+ if (typeof pathPrefix !== 'string' || !pathPrefix.endsWith('/'))
2969
+ return content;
2970
+ if (!behaviors)
2971
+ return content;
2972
+ let result = content;
2973
+ if (behaviors.retargetsGsdCoreProsePath === true) {
2974
+ result = result.replace(/@?GSD core\/(?=[a-z])/g, `${pathPrefix}gsd-core/`);
2975
+ }
2976
+ const home = behaviors.defaultHomeIncludeRoot;
2977
+ if (typeof home === 'string' && home.length > 0 && pathPrefix !== `$HOME/${home}/`) {
2978
+ result = result
2979
+ .split(`@~/${home}/`).join(`@${pathPrefix}`)
2980
+ .split(`@$HOME/${home}/`).join(`@${pathPrefix}`);
2981
+ }
2982
+ return result;
2983
+ }
2941
2984
  /** Return a safe project-relative prefix for a resolved install target. */
2942
2985
  function projectRelativePrefixFromProjectRoot(projectRoot, resolvedTarget) {
2943
2986
  if (typeof projectRoot !== 'string' || typeof resolvedTarget !== 'string')
@@ -3974,4 +4017,6 @@ module.exports = {
3974
4017
  _negotiatedDispatchIsolation,
3975
4018
  // #1521: canonical non-Claude runtime list for test files and tooling
3976
4019
  NON_CLAUDE_RUNTIMES,
4020
+ // #5123: post-install include retarget (config-dir aware; Kimi prose path)
4021
+ retargetInstalledIncludes,
3977
4022
  };
@@ -40,6 +40,7 @@ exports.resolveConfigHomeFromDescriptor = resolveConfigHomeFromDescriptor;
40
40
  exports.resolveAntigravityGlobalDir = resolveAntigravityGlobalDir;
41
41
  exports.detectAntigravityDirAmbiguity = detectAntigravityDirAmbiguity;
42
42
  exports.resolveKimiGlobalDir = resolveKimiGlobalDir;
43
+ exports.resolveGsdHome = resolveGsdHome;
43
44
  exports.resolveKimiHooksTomlDir = resolveKimiHooksTomlDir;
44
45
  exports.getGlobalConfigDir = getGlobalConfigDir;
45
46
  exports.resolveSkillsBaseFromDescriptor = resolveSkillsBaseFromDescriptor;
@@ -403,6 +404,23 @@ exports.NON_REGISTRY_CONFIG_HOME_DESCRIPTORS = [
403
404
  * through resolveConfigHomeFromDescriptor and have no `kind`/`name` shape.
404
405
  */
405
406
  exports.GSD_LOCATION_ENV_KEYS = ['GSD_HOME', 'GSD_AGENTS_DIR'];
407
+ /**
408
+ * #5017: the ONE rule for the root of the GSD-owned store (`$GSD_HOME/.gsd/`:
409
+ * defaults.json, consent.json, capability overlays) — `GSD_HOME` when set and
410
+ * non-blank, otherwise the home directory. The config loader, the install-time
411
+ * resolvers, the installer's defaults writer and capability staging all call
412
+ * this, so one configuration gives one answer at install time, at dispatch time
413
+ * and in the staged capabilities. The hooks (cold-tree, no lib import) and the
414
+ * settings / new-project workflows (shell) carry the same rule inline; a test
415
+ * pins each of them to it.
416
+ *
417
+ * `fallbackHome` lets a caller that already holds an injected home directory
418
+ * (the install-time resolvers' test seam) keep using it when GSD_HOME is unset.
419
+ * A blank `GSD_HOME` is "unset": `''` must never become a relative store path.
420
+ */
421
+ function resolveGsdHome(fallbackHome = node_os_1.default.homedir()) {
422
+ return process.env['GSD_HOME'] || fallbackHome;
423
+ }
406
424
  /**
407
425
  * Resolve the directory holding the Kimi product's OWN native config.toml —
408
426
  * the file that product itself reads for providers/models/hooks/etc, and the
@@ -1131,6 +1131,8 @@ function _normalizeMd(content) {
1131
1131
  }
1132
1132
  function normalizeContent(filePath, content, opts = {}) {
1133
1133
  const encoding = opts.encoding ?? 'utf-8';
1134
+ if (opts.verbatim === true)
1135
+ return { content: content ?? '', encoding };
1134
1136
  const isMd = node_path_1.default.extname(filePath).toLowerCase() === '.md';
1135
1137
  let normalized;
1136
1138
  if (isMd) {
@@ -675,6 +675,28 @@ function actionsFor(situation, s, runtime = 'claude') {
675
675
  }
676
676
  }
677
677
  // ─── Summary line ─────────────────────────────────────────────────────────────
678
+ function phaseSummary(s) {
679
+ const phase = s.current_phase;
680
+ // Preserve the milestone ordinal introduced by #5228 when it is known.
681
+ const at = phase === null ? -1 : s.milestone_phases.findIndex((id) => comparePhaseNum(id, phase) === 0);
682
+ if (at >= 0)
683
+ return `Phase ${at + 1} of ${s.milestone_phases.length}`;
684
+ const total = s.total_phases;
685
+ const legacy = `Phase ${phase ?? '?'} of ${total ?? '?'}`;
686
+ const roadmapTotal = s.roadmap_total_phases;
687
+ // Keep the recorded pair when roadmap counts are unavailable, or when the
688
+ // pair can describe a single milestone without changing its existing text.
689
+ if (phase === null || total === null || roadmapTotal === null || roadmapTotal <= 0
690
+ || (total === roadmapTotal && comparePhaseNum(phase, total) <= 0)) {
691
+ return legacy;
692
+ }
693
+ // When the recorded pair cannot safely describe this roadmap, label its
694
+ // counts separately rather than printing a mixed-scope ratio.
695
+ const roadmapDone = s.roadmap_completed_phases;
696
+ return roadmapDone === null
697
+ ? `Phase ${phase} (${roadmapTotal} roadmap phases)`
698
+ : `Phase ${phase} (${roadmapDone} of ${roadmapTotal} roadmap phases complete)`;
699
+ }
678
700
  function buildSummary(situation, s) {
679
701
  switch (situation) {
680
702
  case 'no-project':
@@ -688,7 +710,7 @@ function buildSummary(situation, s) {
688
710
  case 'needs-first-phase':
689
711
  return 'Project initialized — plan your first phase';
690
712
  case 'planning':
691
- return `${phasePosition(s)} — needs a plan`;
713
+ return `${phaseSummary(s)} — needs a plan`;
692
714
  case 'executing':
693
715
  return progressLine('executing', s);
694
716
  case 'verify-pending':
@@ -702,24 +724,10 @@ function buildSummary(situation, s) {
702
724
  return 'Unsure of state — showing progress';
703
725
  }
704
726
  }
705
- /**
706
- * "Phase N of M" for the current phase. N is the phase's 1-based position in
707
- * the current milestone's ordered phase list (#5228), so a milestone that
708
- * continues the numbering of an earlier one (phases 9-17) reads "Phase 1 of 9",
709
- * not "Phase 9 of 9". When the phase is not in that list (or the roadmap lists
710
- * none) the STATE.md figures are printed as recorded.
711
- */
712
- function phasePosition(s) {
713
- const current = s.current_phase;
714
- const at = current === null ? -1 : s.milestone_phases.findIndex((id) => comparePhaseNum(id, current) === 0);
715
- if (at >= 0)
716
- return `Phase ${at + 1} of ${s.milestone_phases.length}`;
717
- return `Phase ${s.current_phase ?? '?'} of ${s.total_phases ?? '?'}`;
718
- }
719
727
  function progressLine(tail, s) {
720
728
  const parts = [];
721
729
  if (s.current_phase !== null && s.total_phases !== null) {
722
- parts.push(phasePosition(s));
730
+ parts.push(phaseSummary(s));
723
731
  }
724
732
  else if (s.current_phase !== null) {
725
733
  parts.push(`Phase ${s.current_phase}`);