@opengsd/gsd-core 1.7.0-rc.6 → 1.8.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 (195) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.opencode/plugins/gsd-core.js +14 -0
  4. package/README.md +2 -0
  5. package/agents/gsd-debug-session-manager.md +42 -4
  6. package/agents/gsd-debugger.md +87 -29
  7. package/agents/gsd-executor.md +31 -3
  8. package/agents/gsd-planner.md +29 -36
  9. package/agents/gsd-security-auditor.md +13 -15
  10. package/agents/gsd-verifier.md +2 -2
  11. package/bin/install.js +1157 -84
  12. package/commands/gsd/ai-integration-phase.md +1 -1
  13. package/commands/gsd/mempalace-capture.md +31 -1
  14. package/commands/gsd/new-milestone.md +1 -1
  15. package/commands/gsd/plan-phase.md +5 -3
  16. package/commands/gsd/plan-review-convergence.md +3 -2
  17. package/commands/gsd/surface.md +6 -6
  18. package/gsd-core/bin/gsd-tools.cjs +1866 -2434
  19. package/gsd-core/bin/lib/adapter-imperative.cjs +8 -1
  20. package/gsd-core/bin/lib/agent-command-router.cjs +20 -5
  21. package/gsd-core/bin/lib/api-coverage.cjs +341 -49
  22. package/gsd-core/bin/lib/audit.cjs +7 -6
  23. package/gsd-core/bin/lib/broken-windows.cjs +716 -0
  24. package/gsd-core/bin/lib/capability-command-router.cjs +733 -0
  25. package/gsd-core/bin/lib/capability-registry.cjs +157 -88
  26. package/gsd-core/bin/lib/capability-writer.cjs +6 -1
  27. package/gsd-core/bin/lib/check-command-router.cjs +129 -26
  28. package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +115 -27
  29. package/gsd-core/bin/lib/claude-orchestration.cjs +84 -9
  30. package/gsd-core/bin/lib/clock.cjs +19 -0
  31. package/gsd-core/bin/lib/command-aliases.cjs +14 -0
  32. package/gsd-core/bin/lib/commands.cjs +129 -13
  33. package/gsd-core/bin/lib/config-loader.cjs +20 -4
  34. package/gsd-core/bin/lib/config.cjs +81 -18
  35. package/gsd-core/bin/lib/core-utils.cjs +14 -3
  36. package/gsd-core/bin/lib/decisions.cjs +32 -8
  37. package/gsd-core/bin/lib/docs.cjs +6 -0
  38. package/gsd-core/bin/lib/drift.cjs +4 -4
  39. package/gsd-core/bin/lib/external-descriptor-trust.cjs +14 -2
  40. package/gsd-core/bin/lib/frontmatter.cjs +22 -0
  41. package/gsd-core/bin/lib/gap-checker.cjs +17 -2
  42. package/gsd-core/bin/lib/gsd2-import.cjs +2 -1
  43. package/gsd-core/bin/lib/init.cjs +138 -60
  44. package/gsd-core/bin/lib/install-engine.cjs +301 -25
  45. package/gsd-core/bin/lib/install-profiles.cjs +239 -1
  46. package/gsd-core/bin/lib/installer-migration-authoring.cjs +2 -1
  47. package/gsd-core/bin/lib/installer-migrations/005-opencode-baseline-commands-dir.cjs +146 -0
  48. package/gsd-core/bin/lib/installer-migrations/006-pi-extension-cjs-to-js.cjs +91 -0
  49. package/gsd-core/bin/lib/installer-migrations.cjs +45 -6
  50. package/gsd-core/bin/lib/markdown-sectionizer.cjs +449 -0
  51. package/gsd-core/bin/lib/markdown-table.cjs +698 -0
  52. package/gsd-core/bin/lib/milestone.cjs +463 -43
  53. package/gsd-core/bin/lib/model-catalog.cjs +19 -4
  54. package/gsd-core/bin/lib/model-resolver.cjs +189 -7
  55. package/gsd-core/bin/lib/onboard-projection.cjs +11 -8
  56. package/gsd-core/bin/lib/phase-command-router.cjs +50 -2
  57. package/gsd-core/bin/lib/phase-id.cjs +26 -4
  58. package/gsd-core/bin/lib/phase-lifecycle.cjs +62 -36
  59. package/gsd-core/bin/lib/phase-locator.cjs +23 -2
  60. package/gsd-core/bin/lib/phase.cjs +636 -72
  61. package/gsd-core/bin/lib/plan-scan.cjs +73 -2
  62. package/gsd-core/bin/lib/roadmap-parser.cjs +225 -17
  63. package/gsd-core/bin/lib/roadmap.cjs +113 -52
  64. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +14 -7
  65. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +3 -2
  66. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +24 -9
  67. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +41 -17
  68. package/gsd-core/bin/lib/schema-detect.cjs +2 -1
  69. package/gsd-core/bin/lib/security.cjs +1 -1
  70. package/gsd-core/bin/lib/shell-command-projection.cjs +61 -25
  71. package/gsd-core/bin/lib/smart-entry.cjs +73 -7
  72. package/gsd-core/bin/lib/state-document.cjs +7 -4
  73. package/gsd-core/bin/lib/state-transition.cjs +122 -46
  74. package/gsd-core/bin/lib/state.cjs +456 -137
  75. package/gsd-core/bin/lib/surface.cjs +53 -11
  76. package/gsd-core/bin/lib/template.cjs +2 -1
  77. package/gsd-core/bin/lib/uat.cjs +474 -13
  78. package/gsd-core/bin/lib/ui-safety-gate.cjs +23 -1
  79. package/gsd-core/bin/lib/validate.cjs +12 -8
  80. package/gsd-core/bin/lib/verification.cjs +112 -17
  81. package/gsd-core/bin/lib/verify.cjs +224 -25
  82. package/gsd-core/bin/lib/workstream.cjs +3 -2
  83. package/gsd-core/bin/lib/worktree-safety.cjs +1 -1
  84. package/gsd-core/bin/lib/write-set.cjs +38 -0
  85. package/gsd-core/bin/shared/config-schema.manifest.json +5 -2
  86. package/gsd-core/references/api-coverage.md +37 -7
  87. package/gsd-core/references/checkpoints.md +13 -1
  88. package/gsd-core/references/common-bug-patterns.md +13 -0
  89. package/gsd-core/references/debugger-bug-taxonomy.md +111 -0
  90. package/gsd-core/references/debugger-fix-acceptance.md +157 -0
  91. package/gsd-core/references/debugger-philosophy.md +1 -0
  92. package/gsd-core/references/debugger-prevention.md +98 -0
  93. package/gsd-core/references/debugger-rca-branching.md +98 -0
  94. package/gsd-core/references/debugger-repro-hardening.md +130 -0
  95. package/gsd-core/references/debugger-sbfl.md +110 -0
  96. package/gsd-core/references/debugger-semantic-recall.md +81 -0
  97. package/gsd-core/references/execute-phase-quota-recovery.md +55 -0
  98. package/gsd-core/references/execute-phase-requirement-revert.md +8 -0
  99. package/gsd-core/references/execute-phase-response-language.md +7 -0
  100. package/gsd-core/references/planner-antipatterns.md +6 -0
  101. package/gsd-core/references/planner-mvp-mode.md +12 -13
  102. package/gsd-core/references/planner-preconditions.md +156 -0
  103. package/gsd-core/references/planner-reversibility.md +132 -0
  104. package/gsd-core/references/reviewer-instances.md +9 -7
  105. package/gsd-core/references/skeleton-template.md +1 -1
  106. package/gsd-core/references/thinking-models-planning.md +3 -1
  107. package/gsd-core/templates/DEBUG.md +5 -3
  108. package/gsd-core/workflows/add-phase.md +2 -0
  109. package/gsd-core/workflows/add-tests.md +4 -2
  110. package/gsd-core/workflows/add-todo.md +32 -1
  111. package/gsd-core/workflows/ai-integration-phase.md +4 -2
  112. package/gsd-core/workflows/audit-fix.md +2 -2
  113. package/gsd-core/workflows/check-todos.md +3 -1
  114. package/gsd-core/workflows/cleanup.md +7 -1
  115. package/gsd-core/workflows/code-review.md +17 -5
  116. package/gsd-core/workflows/complete-milestone.md +3 -0
  117. package/gsd-core/workflows/debug.md +27 -5
  118. package/gsd-core/workflows/diagnose-issues.md +1 -1
  119. package/gsd-core/workflows/discovery-phase.md +7 -0
  120. package/gsd-core/workflows/discuss-phase/templates/context.md +16 -2
  121. package/gsd-core/workflows/discuss-phase-assumptions.md +3 -0
  122. package/gsd-core/workflows/do.md +7 -1
  123. package/gsd-core/workflows/docs-update.md +1 -0
  124. package/gsd-core/workflows/eval-review.md +3 -0
  125. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +4 -4
  126. package/gsd-core/workflows/execute-phase/steps/regression-gate.md +2 -2
  127. package/gsd-core/workflows/execute-phase.md +30 -37
  128. package/gsd-core/workflows/execute-plan.md +15 -4
  129. package/gsd-core/workflows/fast.md +8 -22
  130. package/gsd-core/workflows/graduation.md +3 -0
  131. package/gsd-core/workflows/health.md +7 -1
  132. package/gsd-core/workflows/help/modes/full.md +6 -2
  133. package/gsd-core/workflows/import.md +8 -2
  134. package/gsd-core/workflows/inbox.md +7 -0
  135. package/gsd-core/workflows/ingest-docs.md +15 -10
  136. package/gsd-core/workflows/manager.md +3 -1
  137. package/gsd-core/workflows/map-codebase.md +4 -4
  138. package/gsd-core/workflows/mvp-phase.md +3 -0
  139. package/gsd-core/workflows/new-milestone.md +69 -21
  140. package/gsd-core/workflows/new-project.md +17 -15
  141. package/gsd-core/workflows/new-workspace.md +3 -1
  142. package/gsd-core/workflows/onboard.md +3 -0
  143. package/gsd-core/workflows/plan-phase.md +14 -5
  144. package/gsd-core/workflows/plan-review-convergence.md +48 -3
  145. package/gsd-core/workflows/plant-seed.md +3 -0
  146. package/gsd-core/workflows/profile-user.md +7 -1
  147. package/gsd-core/workflows/progress.md +33 -5
  148. package/gsd-core/workflows/quick.md +21 -7
  149. package/gsd-core/workflows/remove-workspace.md +3 -0
  150. package/gsd-core/workflows/review.md +123 -68
  151. package/gsd-core/workflows/scan.md +1 -1
  152. package/gsd-core/workflows/secure-phase.md +4 -1
  153. package/gsd-core/workflows/settings-integrations.md +3 -0
  154. package/gsd-core/workflows/settings.md +3 -0
  155. package/gsd-core/workflows/ship.md +58 -5
  156. package/gsd-core/workflows/sketch.md +3 -0
  157. package/gsd-core/workflows/smart-entry.md +3 -0
  158. package/gsd-core/workflows/spec-phase.md +1 -1
  159. package/gsd-core/workflows/spike.md +7 -1
  160. package/gsd-core/workflows/transition.md +1 -1
  161. package/gsd-core/workflows/ui-phase.md +3 -1
  162. package/gsd-core/workflows/ui-review.md +3 -0
  163. package/gsd-core/workflows/undo.md +7 -0
  164. package/gsd-core/workflows/update.md +2 -0
  165. package/gsd-core/workflows/validate-phase.md +3 -0
  166. package/gsd-core/workflows/verify-phase.md +2 -2
  167. package/gsd-core/workflows/verify-work.md +7 -3
  168. package/hooks/dist/gsd-context-monitor.js +27 -9
  169. package/hooks/dist/gsd-statusline.js +252 -17
  170. package/hooks/gsd-context-monitor.js +27 -9
  171. package/hooks/gsd-statusline.js +252 -17
  172. package/package.json +8 -4
  173. package/pi/gsd.cjs +8 -2
  174. package/scripts/changeset/lint.cjs +1 -0
  175. package/scripts/changeset/parse.cjs +26 -0
  176. package/scripts/check-glossary-refs.cjs +220 -0
  177. package/scripts/ci-rebase-check.cjs +48 -4
  178. package/scripts/ci-test-scope.cjs +39 -1
  179. package/scripts/gen-adr-index.cjs +526 -0
  180. package/scripts/gen-golden-install-parity-zcode.cjs +35 -45
  181. package/scripts/gen-install-tree-fixtures.cjs +75 -0
  182. package/scripts/gen-test-timings.cjs +201 -0
  183. package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -1
  184. package/scripts/lint-portable-timeout.cjs +140 -0
  185. package/scripts/lint-table-schema-drift.cjs +157 -0
  186. package/scripts/lint-test-file-count.allowlist.json +1 -0
  187. package/scripts/release-tarball-smoke.cjs +18 -11
  188. package/scripts/run-tests.cjs +420 -58
  189. package/skills/gsd-ai-integration-phase/SKILL.md +1 -1
  190. package/skills/gsd-mempalace-capture/SKILL.md +31 -1
  191. package/skills/gsd-new-milestone/SKILL.md +1 -1
  192. package/skills/gsd-plan-phase/SKILL.md +5 -3
  193. package/skills/gsd-plan-review-convergence/SKILL.md +3 -2
  194. package/skills/gsd-surface/SKILL.md +6 -6
  195. package/vscode/package.json +1 -1
@@ -33,7 +33,10 @@ const roadmapParserMod = require("./roadmap-parser.cjs");
33
33
  const { extractCurrentMilestone, stripShippedMilestones: _stripShippedMilestones, getMilestoneInfo, getMilestonePhaseFilter, getRoadmapPhaseInternal } = roadmapParserMod;
34
34
  // eslint-disable-next-line @typescript-eslint/no-require-imports
35
35
  const modelResolverMod = require("./model-resolver.cjs");
36
- const { resolveModelInternal, resolveEffortInternal, resolveFastModeInternal, resolveEffortForTier, resolveGranularityInternal, assertValidGranularityOverride } = modelResolverMod;
36
+ const { resolveModelInternal, resolveModelForTier, resolveProviderEscalation, resolveEffortInternal, resolveFastModeInternal, resolveEffortForTier, resolveGranularityInternal, assertValidGranularityOverride } = modelResolverMod;
37
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
38
+ const agentCommandRouterMod = require("./agent-command-router.cjs");
39
+ const { AGENT_FAILURE_CLASSES } = agentCommandRouterMod;
37
40
  const model_catalog_cjs_1 = require("./model-catalog.cjs");
38
41
  // eslint-disable-next-line @typescript-eslint/no-require-imports
39
42
  const planningWorkspace = require("./planning-workspace.cjs");
@@ -45,7 +48,46 @@ const { extractFrontmatter } = frontmatter;
45
48
  const modelProfiles = require("./model-profiles.cjs");
46
49
  const { MODEL_PROFILES, VALID_PHASE_TYPES } = modelProfiles;
47
50
  const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
51
+ const clock_cjs_1 = require("./clock.cjs");
48
52
  // ─── Phase Status ─────────────────────────────────────────────────────────────
53
+ /**
54
+ * Phase-status precedence ladder — furthest-along wins (#2408).
55
+ *
56
+ * `cmdStats` builds `phasesByNumber` by scanning on-disk phase directories.
57
+ * When two directories normalize to the same phase key (e.g. `05-real/` and
58
+ * `05-real-stray/`), the status field must be folded by precedence rather
59
+ * than overwritten last-write-wins — otherwise `/gsd-stats` reports whatever
60
+ * directory `fs.readdirSync` happened to yield last, which is non-deterministic
61
+ * across platforms and can silently call a `Complete` phase `Not Started`.
62
+ */
63
+ const PHASE_STATUS_PRECEDENCE = [
64
+ 'Complete',
65
+ 'Needs Review',
66
+ 'Executed',
67
+ 'In Progress',
68
+ 'Planned',
69
+ 'Not Started',
70
+ 'Pending',
71
+ ];
72
+ const PHASE_STATUS_RANK = new Map(PHASE_STATUS_PRECEDENCE.map((s, i) => [s, i]));
73
+ /**
74
+ * Fold two phase statuses by precedence — returns whichever is further along
75
+ * the {@link PHASE_STATUS_PRECEDENCE} ladder. Unrecognized statuses fall behind
76
+ * every recognized one (so a recognized status always wins over an unknown one;
77
+ * two unrecognized statuses favor `a` for determinism).
78
+ */
79
+ function foldPhaseStatus(a, b) {
80
+ const ra = PHASE_STATUS_RANK.get(a);
81
+ const rb = PHASE_STATUS_RANK.get(b);
82
+ if (ra === undefined && rb === undefined)
83
+ return a;
84
+ if (ra === undefined)
85
+ return b;
86
+ if (rb === undefined)
87
+ return a;
88
+ // Lower rank = higher precedence (Complete=0 wins over Not Started=5).
89
+ return ra <= rb ? a : b;
90
+ }
49
91
  /**
50
92
  * Determine phase status by checking plan/summary counts AND verification state.
51
93
  * Introduces "Executed" for phases with all summaries but no passing verification.
@@ -127,6 +169,9 @@ function cmdListTodos(cwd, area, raw) {
127
169
  const createdMatch = content.match(/^created:\s*(.+)$/m);
128
170
  const titleMatch = content.match(/^title:\s*(.+)$/m);
129
171
  const areaMatch = content.match(/^area:\s*(.+)$/m);
172
+ // #2337: surface severity when present. Omit the key entirely for todos
173
+ // with no severity line so existing consumers of this JSON are unaffected.
174
+ const severityMatch = content.match(/^severity:\s*(.+)$/m);
130
175
  const todoArea = areaMatch ? areaMatch[1].trim() : 'general';
131
176
  // Apply area filter if specified
132
177
  if (area && todoArea !== area)
@@ -138,6 +183,7 @@ function cmdListTodos(cwd, area, raw) {
138
183
  title: titleMatch ? titleMatch[1].trim() : 'Untitled',
139
184
  area: todoArea,
140
185
  path: toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(pendingDir, file))),
186
+ ...(severityMatch ? { severity: severityMatch[1].trim() } : {}),
141
187
  });
142
188
  }
143
189
  }
@@ -393,7 +439,8 @@ function cmdResolveGranularity(cwd, phaseType, raw, override) {
393
439
  * { model, profile, effort, effort_rendered, effort_param, effort_propagation,
394
440
  * fast_mode, fast_mode_supported, [unknown_agent] }
395
441
  *
396
- * Flags: --effort <level>, --fast-mode <true|false>, --attempt <n>
442
+ * Flags: --effort <level>, --fast-mode <true|false>, --attempt <n>,
443
+ * --failure-class <class> (#2296)
397
444
  */
398
445
  function cmdResolveExecution(cwd, agentType, raw, opts) {
399
446
  if (!agentType) {
@@ -402,7 +449,29 @@ function cmdResolveExecution(cwd, agentType, raw, opts) {
402
449
  opts = opts || {};
403
450
  const config = loadConfig(cwd);
404
451
  const profile = config['model_profile'] || 'balanced';
405
- const model = resolveModelInternal(cwd, agentType);
452
+ // #2068: resolve the model per-attempt so dynamic_routing escalates the MODEL
453
+ // (heavy tier) alongside effort. Gated on an explicit --attempt exactly like the
454
+ // effort resolution below, so the two fields stay symmetric: with no --attempt
455
+ // the model comes from the classic profile path (unchanged for everyone,
456
+ // including dynamic_routing-enabled users who don't pass --attempt), and only an
457
+ // explicit attempt routes through the tier ladder. resolveModelForTier itself
458
+ // still falls back to resolveModelInternal when dynamic_routing is off.
459
+ let model = (opts.attempt !== undefined && opts.attempt !== null)
460
+ ? resolveModelForTier(cwd, agentType, opts.attempt)
461
+ : resolveModelInternal(cwd, agentType);
462
+ // #2296: when the caller reports WHY the previous attempt failed, consult the
463
+ // provider-escalation ladder. Only a quota/rate-limit class warrants it — a
464
+ // heavier tier on the same throttled provider is still throttled, so this
465
+ // ladder swaps providers instead. Gated on an explicit --failure-class so the
466
+ // JSON contract is byte-identical for every existing caller.
467
+ let escalation;
468
+ if (opts.failureClass !== undefined) {
469
+ const applicable = opts.failureClass === AGENT_FAILURE_CLASSES.QUOTA_EXCEEDED;
470
+ const resolved = resolveProviderEscalation(cwd, agentType, opts.attempt, applicable);
471
+ if (resolved.escalated)
472
+ model = resolved.to;
473
+ escalation = { class: opts.failureClass, ...resolved };
474
+ }
406
475
  const effortOpts = {};
407
476
  if (typeof opts.effortOverride === 'string')
408
477
  effortOpts['override'] = opts.effortOverride;
@@ -429,6 +498,8 @@ function cmdResolveExecution(cwd, agentType, raw, opts) {
429
498
  };
430
499
  if (!agentModels)
431
500
  result['unknown_agent'] = true;
501
+ if (escalation)
502
+ result['escalation'] = escalation;
432
503
  output(result, raw, effort);
433
504
  }
434
505
  /**
@@ -592,6 +663,7 @@ function cmdCommit(cwd, message, files, raw, amend, noVerify) {
592
663
  // Stage files
593
664
  const explicitFiles = files && files.length > 0;
594
665
  const filesToStage = explicitFiles ? files : ['.planning/'];
666
+ const stagedPaths = [];
595
667
  for (const file of filesToStage) {
596
668
  const fullPath = node_path_1.default.join(cwd, file);
597
669
  if (!node_fs_1.default.existsSync(fullPath)) {
@@ -607,12 +679,32 @@ function cmdCommit(cwd, message, files, raw, amend, noVerify) {
607
679
  }
608
680
  else {
609
681
  (0, shell_command_projection_cjs_1.execGit)(['add', file], { cwd });
682
+ stagedPaths.push(file);
610
683
  }
611
684
  }
612
- // Commit (--no-verify skips pre-commit hooks, used by parallel executor agents)
613
- const commitArgs = amend ? ['commit', '--amend', '--no-edit'] : ['commit', '-m', sanitizedMessage];
685
+ // Commit — when the caller declared a scope (--files), append a pathspec so
686
+ // only the declared files land in the commit, not the entire index (#2112).
687
+ // The pathspec uses stagedPaths (not filesToStage) so skipped missing files
688
+ // are excluded — otherwise git would record them as deletions (#2014).
689
+ // During a merge, git refuses partial commits — fall back to a bare commit.
690
+ // --amend is left without a pathspec: amending with -- <paths> is a different
691
+ // operation that rewrites the tip with only those paths.
692
+ if (explicitFiles && stagedPaths.length === 0 && !amend) {
693
+ const result = { committed: false, hash: null, reason: 'nothing_to_commit' };
694
+ output(result, raw, 'nothing');
695
+ return;
696
+ }
697
+ const isMergeInProgress = (0, shell_command_projection_cjs_1.execGit)(['rev-parse', '-q', '--verify', 'MERGE_HEAD'], { cwd }).exitCode === 0;
698
+ const canScope = explicitFiles && stagedPaths.length > 0 && !amend
699
+ && !isMergeInProgress;
700
+ const commitArgs = amend
701
+ ? ['commit', '--amend', '--no-edit']
702
+ : ['commit', '-m', sanitizedMessage];
614
703
  if (noVerify)
615
704
  commitArgs.push('--no-verify');
705
+ if (canScope) {
706
+ commitArgs.push('--', ...stagedPaths);
707
+ }
616
708
  const commitResult = (0, shell_command_projection_cjs_1.execGit)(commitArgs, { cwd });
617
709
  if (commitResult.exitCode !== 0) {
618
710
  if (commitResult.stdout.includes('nothing to commit') || commitResult.stderr.includes('nothing to commit')) {
@@ -710,12 +802,21 @@ function cmdCommitToSubrepo(cwd, message, files, raw) {
710
802
  for (const [repo, repoFiles] of Object.entries(grouped)) {
711
803
  const repoCwd = node_path_1.default.join(cwd, repo);
712
804
  // Stage files (strip sub-repo prefix for paths relative to that repo)
805
+ const stagedRelPaths = [];
713
806
  for (const file of repoFiles) {
714
807
  const relativePath = file.slice(repo.length + 1);
715
- (0, shell_command_projection_cjs_1.execGit)(['add', relativePath], { cwd: repoCwd });
808
+ const addResult = (0, shell_command_projection_cjs_1.execGit)(['add', relativePath], { cwd: repoCwd });
809
+ if (addResult.exitCode === 0) {
810
+ stagedRelPaths.push(relativePath);
811
+ }
716
812
  }
717
- // Commit
718
- const commitResult = (0, shell_command_projection_cjs_1.execGit)(['commit', '-m', message], { cwd: repoCwd });
813
+ // Commit — pathspec limits the commit to the staged files only (#2112)
814
+ const isMergeInProgressSub = (0, shell_command_projection_cjs_1.execGit)(['rev-parse', '-q', '--verify', 'MERGE_HEAD'], { cwd: repoCwd }).exitCode === 0;
815
+ const canScopeSub = stagedRelPaths.length > 0 && !isMergeInProgressSub;
816
+ const commitArgs = canScopeSub
817
+ ? ['commit', '-m', message, '--', ...stagedRelPaths]
818
+ : ['commit', '-m', message];
819
+ const commitResult = (0, shell_command_projection_cjs_1.execGit)(commitArgs, { cwd: repoCwd });
719
820
  if (commitResult.exitCode !== 0) {
720
821
  if (commitResult.stdout.includes('nothing to commit') || commitResult.stderr.includes('nothing to commit')) {
721
822
  repos[repo] = { committed: false, hash: null, files: repoFiles, reason: 'nothing_to_commit' };
@@ -837,8 +938,16 @@ function cmdPrSubrepo(cwd, repo, branch, commitMessage, raw) {
837
938
  error(`Failed to stage ${file} in ${repo}: ${addResult.stderr}`);
838
939
  }
839
940
  }
840
- // 5. Commit
841
- const commitResult = (0, shell_command_projection_cjs_1.execGit)(['commit', '-m', commitMessage], { cwd: repoCwd });
941
+ // 5. Commit — pathspec limits the commit to the staged files only (#2112).
942
+ // changedFiles includes both old and new paths for renames so the full
943
+ // rename is captured atomically (pathspec on newPath alone would leave the
944
+ // deletion of oldPath stranded in the index).
945
+ const isMergeInProgressPr = (0, shell_command_projection_cjs_1.execGit)(['rev-parse', '-q', '--verify', 'MERGE_HEAD'], { cwd: repoCwd }).exitCode === 0;
946
+ const canScopePr = changedFiles.length > 0 && !isMergeInProgressPr;
947
+ const commitArgs = canScopePr
948
+ ? ['commit', '-m', commitMessage, '--', ...changedFiles]
949
+ : ['commit', '-m', commitMessage];
950
+ const commitResult = (0, shell_command_projection_cjs_1.execGit)(commitArgs, { cwd: repoCwd });
842
951
  if (commitResult.exitCode !== 0) {
843
952
  rollback();
844
953
  error(`Failed to commit in ${repo}: ${commitResult.stderr}`);
@@ -1223,7 +1332,7 @@ function cmdTodoComplete(cwd, filename, raw) {
1223
1332
  (0, shell_command_projection_cjs_1.platformEnsureDir)(completedDir);
1224
1333
  // Read, add completion timestamp, move
1225
1334
  let content = node_fs_1.default.readFileSync(sourcePath, 'utf-8');
1226
- const today = new Date().toISOString().split('T')[0];
1335
+ const today = clock_cjs_1.realClock.localToday();
1227
1336
  content = `completed: ${today}\n` + content;
1228
1337
  (0, shell_command_projection_cjs_1.platformWriteSync)(node_path_1.default.join(completedDir, filename), content);
1229
1338
  node_fs_1.default.unlinkSync(sourcePath);
@@ -1232,7 +1341,7 @@ function cmdTodoComplete(cwd, filename, raw) {
1232
1341
  function cmdScaffold(cwd, type, options, raw) {
1233
1342
  const { phase, name } = options;
1234
1343
  const padded = phase ? normalizePhaseName(phase) : '00';
1235
- const today = new Date().toISOString().split('T')[0];
1344
+ const today = clock_cjs_1.realClock.localToday();
1236
1345
  // Find phase directory
1237
1346
  const phaseInfo = phase ? findPhaseInternal(cwd, phase) : null;
1238
1347
  const phaseDir = phaseInfo ? node_path_1.default.join(cwd, phaseInfo['directory']) : null;
@@ -1346,7 +1455,12 @@ function cmdStats(cwd, format, raw) {
1346
1455
  name: existing?.name || phaseName,
1347
1456
  plans: (existing?.plans || 0) + plans,
1348
1457
  summaries: (existing?.summaries || 0) + summaries,
1349
- status,
1458
+ // #2408: fold colliding statuses by precedence rather than overwriting
1459
+ // last-write-wins. fs.readdirSync order is non-deterministic across
1460
+ // platforms, so a naive overwrite can report a Complete phase as Not
1461
+ // Started (or vice versa) depending on read order. The fold picks the
1462
+ // furthest-along status, matching what an operator expects.
1463
+ status: existing ? foldPhaseStatus(existing.status, status) : status,
1350
1464
  });
1351
1465
  }
1352
1466
  }
@@ -1468,6 +1582,8 @@ function cmdCheckCommit(cwd, raw) {
1468
1582
  module.exports = {
1469
1583
  groupFilesBySubrepo,
1470
1584
  determinePhaseStatus,
1585
+ foldPhaseStatus,
1586
+ PHASE_STATUS_PRECEDENCE,
1471
1587
  cmdGenerateSlug,
1472
1588
  cmdCurrentTimestamp,
1473
1589
  cmdListTodos,
@@ -18,7 +18,7 @@
18
18
  * - ./planning-workspace.cjs (planningDir, planningRoot)
19
19
  * - ./shell-command-projection.cjs (execGit, platformWriteSync, platformReadSync)
20
20
  * - ./core-utils.cjs (detectSubRepos)
21
- * - ./model-catalog.cjs (KNOWN_RUNTIMES, KNOWN_PROVIDERS)
21
+ * - ./model-catalog.cjs (KNOWN_RUNTIMES, KNOWN_PROVIDERS, ADAPTIVE_TIER_VALUES)
22
22
  */
23
23
  var __importDefault = (this && this.__importDefault) || function (mod) {
24
24
  return (mod && mod.__esModule) ? mod : { "default": mod };
@@ -158,17 +158,25 @@ const _warnedUnknownConfigKeys = new Set();
158
158
  // ─── Git utilities ────────────────────────────────────────────────────────────
159
159
  const _gitIgnoredCache = new Map();
160
160
  function isGitIgnored(cwd, targetPath) {
161
- const key = cwd + '::' + targetPath;
161
+ // #2206: strip trailing slashes — `git check-ignore` has a quirk where a
162
+ // CRLF .gitignore with blank lines falsely reports a trailing-slash path
163
+ // (e.g. `.planning/`) as ignored. Normalizing here protects every call site.
164
+ const normalized = targetPath.replace(/\/+$/, '');
165
+ const key = cwd + '::' + normalized;
162
166
  if (_gitIgnoredCache.has(key))
163
167
  return _gitIgnoredCache.get(key);
164
168
  // --no-index checks .gitignore rules regardless of whether the file is tracked.
165
- const result = (0, shell_command_projection_cjs_1.execGit)(['check-ignore', '-q', '--no-index', '--', targetPath], { cwd });
169
+ const result = (0, shell_command_projection_cjs_1.execGit)(['check-ignore', '-q', '--no-index', '--', normalized], { cwd });
166
170
  const ignored = result.exitCode === 0;
167
171
  _gitIgnoredCache.set(key, ignored);
168
172
  return ignored;
169
173
  }
170
174
  // ─── Model alias resolution ───────────────────────────────────────────────────
171
- const RUNTIME_OVERRIDE_TIERS = new Set(['opus', 'sonnet', 'haiku']);
175
+ // Catalog-derived (model-catalog.cts) so this vocabulary can never drift from
176
+ // VALID_TIERS in verify.cts — see #2070 "Generative Fix Divergence". Excludes
177
+ // 'inherit' (unlike VALID_TIERS): runtime overrides always resolve to a
178
+ // concrete tier, never the adaptive sentinel.
179
+ const RUNTIME_OVERRIDE_TIERS = model_catalog_cjs_1.ADAPTIVE_TIER_VALUES;
172
180
  const _warnedConfigKeys = new Set();
173
181
  function _warnUnknownProfileOverrides(parsed, configLabel) {
174
182
  if (!parsed || typeof parsed !== 'object')
@@ -683,6 +691,14 @@ function loadConfigResolved(cwd, options = {}) {
683
691
  fast_mode: (globalDefaults['fast_mode']) || null,
684
692
  agent_skills: (globalDefaults['agent_skills']) || {},
685
693
  response_language: (globalDefaults['response_language']) || null,
694
+ // #2069: forward model_policy / model_profile_overrides / runtime so the global-defaults
695
+ // path is at parity with the project-config path (which forwards these three from
696
+ // parsed['…'] at the top of this function). Without these entries, ~/.gsd/defaults.json
697
+ // silently drops them — model_policy/provider/budget etc. are honored when set in a
698
+ // project but ignored when set globally.
699
+ runtime: (globalDefaults['runtime']) || null,
700
+ model_profile_overrides: (globalDefaults['model_profile_overrides']) || null,
701
+ model_policy: (globalDefaults['model_policy']) || null,
686
702
  };
687
703
  // Branch D: global-defaults
688
704
  try {
@@ -67,6 +67,45 @@ const SCHEMA_DEFAULTS = {
67
67
  'executor.stall_threshold_minutes': 10,
68
68
  'git.create_tag': true,
69
69
  };
70
+ /**
71
+ * Resolve a schema-level default for an absent key (#2256). Checks the legacy
72
+ * hardcoded SCHEMA_DEFAULTS first, then the capability-registry configSchema
73
+ * default — the same registry default the runtime's capability-activation
74
+ * resolver (resolveConfigKey Level 4, capability-activation.cts) already honors,
75
+ * so `query config-get` can no longer disagree with the runtime about an absent
76
+ * key's effective value.
77
+ */
78
+ function resolveSchemaDefault(cwd, kp) {
79
+ if (Object.prototype.hasOwnProperty.call(SCHEMA_DEFAULTS, kp)) {
80
+ return { found: true, value: SCHEMA_DEFAULTS[kp] };
81
+ }
82
+ const capSchema = getCapabilityConfigSchema(cwd);
83
+ if (capSchema && typeof capSchema === 'object'
84
+ && Object.prototype.hasOwnProperty.call(capSchema, kp)) {
85
+ const entry = capSchema[kp];
86
+ if (entry && typeof entry === 'object' && !Array.isArray(entry)) {
87
+ const def = entry['default'];
88
+ if (def !== undefined)
89
+ return { found: true, value: def };
90
+ }
91
+ }
92
+ return { found: false, value: undefined };
93
+ }
94
+ /**
95
+ * Emit a schema-resolved default (#2256), applying the same secret-masking
96
+ * invariant the found-key path applies. getCapabilityConfigSchema is a
97
+ * federated, third-party-extensible surface (ADR-1244) — a future key-name
98
+ * collision with a secret key must not leak a declared default in plaintext.
99
+ * Centralizing emission here means masking can't be missed at a call site.
100
+ */
101
+ function emitResolvedDefault(kp, value, raw) {
102
+ if ((0, secrets_cjs_1.isSecretKey)(kp)) {
103
+ const masked = (0, secrets_cjs_1.maskSecret)(value);
104
+ output(masked, raw, masked);
105
+ return;
106
+ }
107
+ output(value, raw, String(value));
108
+ }
70
109
  // ─── Validation helpers ───────────────────────────────────────────────────────
71
110
  function validateKnownConfigKeyPath(keyPath) {
72
111
  const suggested = CONFIG_KEY_SUGGESTIONS[keyPath];
@@ -692,6 +731,22 @@ function cmdConfigSet(cwd, keyPath, value, raw) {
692
731
  const VALID_CONTEXT_POSITIONS = ['front', 'end'];
693
732
  if (kp === 'statusline.context_position')
694
733
  assertEnumValue(parsedValue, val, VALID_CONTEXT_POSITIONS, 'statusline.context_position');
734
+ // statusline.show_context_tokens — boolean only
735
+ if (kp === 'statusline.show_context_tokens') {
736
+ if (typeof parsedValue !== 'boolean') {
737
+ error(`Invalid statusline.show_context_tokens '${val}'. Must be a boolean (true or false).`);
738
+ }
739
+ }
740
+ // Statusline GSD-state format enum validation
741
+ const VALID_STATE_FORMATS = ['full', 'compact'];
742
+ if (kp === 'statusline.state_format')
743
+ assertEnumValue(parsedValue, val, VALID_STATE_FORMATS, 'statusline.state_format');
744
+ // statusline.show_git — boolean only
745
+ if (kp === 'statusline.show_git') {
746
+ if (typeof parsedValue !== 'boolean') {
747
+ error(`Invalid statusline.show_git '${val}'. Must be a boolean (true or false).`);
748
+ }
749
+ }
695
750
  // Fallow scope + profile enum validation (#3424)
696
751
  const VALID_FALLOW_SCOPES = ['phase', 'repo'];
697
752
  if (kp === 'code_quality.fallow.scope')
@@ -813,16 +868,15 @@ function cmdConfigGet(cwd, keyPath, raw, defaultValue) {
813
868
  config = JSON.parse(node_fs_1.default.readFileSync(configPath, 'utf-8'));
814
869
  }
815
870
  else if (hasDefault) {
816
- // eslint-disable-next-line @typescript-eslint/no-base-to-string
817
- output(defaultValue, raw, String(defaultValue));
818
- return;
819
- }
820
- else if (Object.prototype.hasOwnProperty.call(SCHEMA_DEFAULTS, kp)) {
821
- const def = SCHEMA_DEFAULTS[kp];
822
- output(def, raw, String(def));
871
+ emitResolvedDefault(kp, defaultValue, raw);
823
872
  return;
824
873
  }
825
874
  else {
875
+ const sd = resolveSchemaDefault(cwd, kp);
876
+ if (sd.found) {
877
+ emitResolvedDefault(kp, sd.value, raw);
878
+ return;
879
+ }
826
880
  error('No config.json found at ' + configPath, ERROR_REASON.CONFIG_NO_FILE);
827
881
  }
828
882
  }
@@ -836,29 +890,38 @@ function cmdConfigGet(cwd, keyPath, raw, defaultValue) {
836
890
  let current = config;
837
891
  for (const key of keys) {
838
892
  if (current === undefined || current === null || typeof current !== 'object') {
839
- // eslint-disable-next-line @typescript-eslint/no-base-to-string
840
893
  if (hasDefault) {
841
- output(defaultValue, raw, String(defaultValue));
894
+ emitResolvedDefault(kp, defaultValue, raw);
842
895
  return;
843
896
  }
844
- if (Object.prototype.hasOwnProperty.call(SCHEMA_DEFAULTS, kp)) {
845
- const def = SCHEMA_DEFAULTS[kp];
846
- output(def, raw, String(def));
897
+ const sd = resolveSchemaDefault(cwd, kp);
898
+ if (sd.found) {
899
+ emitResolvedDefault(kp, sd.value, raw);
847
900
  return;
848
901
  }
849
902
  error(`Key not found: ${kp}`, ERROR_REASON.CONFIG_KEY_NOT_FOUND);
850
903
  }
851
- current = current[key];
904
+ // Own-property gate: bracket access on a plain object walks the
905
+ // prototype chain, so an unqualified `current[key]` would resolve
906
+ // '__proto__' / 'constructor' / 'hasOwnProperty' (and other
907
+ // Object.prototype members) to their inherited values instead of
908
+ // correctly reporting them as absent. hasOwnProperty.call only
909
+ // returns true for a key JSON.parse actually assigned as data on
910
+ // this object (including a literal "__proto__" JSON key, which
911
+ // JSON.parse defines as an own data property, not the accessor) —
912
+ // never for something inherited from the prototype chain.
913
+ current = Object.prototype.hasOwnProperty.call(current, key)
914
+ ? current[key]
915
+ : undefined;
852
916
  }
853
917
  if (current === undefined) {
854
- // eslint-disable-next-line @typescript-eslint/no-base-to-string
855
918
  if (hasDefault) {
856
- output(defaultValue, raw, String(defaultValue));
919
+ emitResolvedDefault(kp, defaultValue, raw);
857
920
  return;
858
921
  }
859
- if (Object.prototype.hasOwnProperty.call(SCHEMA_DEFAULTS, kp)) {
860
- const def = SCHEMA_DEFAULTS[kp];
861
- output(def, raw, String(def));
922
+ const sd = resolveSchemaDefault(cwd, kp);
923
+ if (sd.found) {
924
+ emitResolvedDefault(kp, sd.value, raw);
862
925
  return;
863
926
  }
864
927
  error(`Key not found: ${kp}`, ERROR_REASON.CONFIG_KEY_NOT_FOUND);
@@ -27,10 +27,16 @@ const { comparePhaseNum } = phaseIdModule;
27
27
  // eslint-disable-next-line @typescript-eslint/no-require-imports
28
28
  const planningWorkspace = require("./planning-workspace.cjs");
29
29
  const { findContextMdIn } = planningWorkspace;
30
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
31
+ const shellCommandProjection = require("./shell-command-projection.cjs");
30
32
  // ─── Path helpers ────────────────────────────────────────────────────────────
31
- /** Normalize a relative path to always use forward slashes (cross-platform). */
33
+ /**
34
+ * Normalize a relative path to always use forward slashes (cross-platform).
35
+ * Delegates to the single separator seam in shell-command-projection so there is
36
+ * exactly one implementation of native→POSIX conversion across the codebase.
37
+ */
32
38
  function toPosixPath(p) {
33
- return p.split(node_path_1.default.sep).join('/');
39
+ return shellCommandProjection.toPosixPath(p);
34
40
  }
35
41
  /**
36
42
  * Scan immediate child directories for separate git repos.
@@ -180,8 +186,13 @@ function extractCanonicalPlanId(filename) {
180
186
  // or a single-digit-plus-letter id ("3A"); a *bare* single digit is a slug word,
181
187
  // so "46-6-rs-…" is not paired into a "46-6" id while "3A-01" stays intact.
182
188
  const tokenRe = /^(?:\d{2,}[A-Z]?|\d[A-Z])(?:\.\d+)*$/i;
189
+ // #2232: the PAIRED plan component is a zero-padded continuation segment
190
+ // (exactly 2 digits), so a ≥3-digit slug word (a year) is not paired into a
191
+ // bogus "14-2026" id. The leading phase component keeps tokenRe's unbounded
192
+ // \d{2,} — phase numbers ≥100 are legitimate; only continuations are capped.
193
+ const planTokenRe = new RegExp(`^(?:${phaseIdModule.PHASE_CONTINUATION_SEGMENT_SOURCE}[A-Z]?|\\d[A-Z])(?:\\.\\d+)*$`, 'i');
183
194
  const phaseIdx = parts.findIndex(p => tokenRe.test(p));
184
- if (phaseIdx >= 0 && phaseIdx + 1 < parts.length && tokenRe.test(parts[phaseIdx + 1])) {
195
+ if (phaseIdx >= 0 && phaseIdx + 1 < parts.length && planTokenRe.test(parts[phaseIdx + 1])) {
185
196
  return `${parts[phaseIdx]}-${parts[phaseIdx + 1]}`;
186
197
  }
187
198
  return base;
@@ -54,6 +54,26 @@ const bulletEmDashRe = /^\s*-\s+\*\*D-([A-Za-z0-9][A-Za-z0-9_-]*)(?:\s*\[([^\]]+
54
54
  * only colon permitted before `**`. (#1639)
55
55
  */
56
56
  const bulletTitledColonRe = /^\s*-\s+\*\*D-([A-Za-z0-9][A-Za-z0-9_-]*)(?:\s*\[([^\]]+)\])?[^:*]*:[^:*]*\*\*\s*(.*)$/;
57
+ /**
58
+ * #2347: format-agnostic evidence that a block/section holds real decision
59
+ * ENTRIES the parser could not read — a bullet whose bold lead-in is an
60
+ * ID-SHAPED token (uppercase prefix, optional digits, hyphen, alnum), whatever
61
+ * the exact ID grammar. The three parser grammars above all require a `D-`
62
+ * prefix; #1365's fail-loud guard reused that same `\bD-` test as its "is this
63
+ * decision-shaped?" evidence, so any other prefix (e.g. `D5-01`) was invisible
64
+ * to BOTH parser and guard, collapsing `could-not-parse` into a clean
65
+ * `none-present` pass.
66
+ *
67
+ * The ID-shape requirement (not "any bold bullet") is deliberate: a decisions
68
+ * block or `### Claude's Discretion` sub-section legitimately contains prose
69
+ * bullets with bold labels (`- **Scope:** …`, `- **Why:** …`, `- **Note:** …`).
70
+ * Those are NOT decision entries and must stay `none-present` — a false
71
+ * `could-not-parse` hard-blocks the plan gate. `[A-Z]+[0-9]*-[A-Za-z0-9]` matches
72
+ * `D-01` / `D5-01` / `DEC-01` but not `Scope:` / `Why:` / `Follow-up:` (mixed
73
+ * case) / `TODO:` (no `-<alnum>` id) — mirroring the parser's own `D-<alnum>`
74
+ * shape without hardcoding the `D`.
75
+ */
76
+ const boldLeadInBulletRe = /^\s*-\s+\*\*[A-Z]+[0-9]*-[A-Za-z0-9]/m;
57
77
  /**
58
78
  * Parse decision lines from a block of text (the inner text of a <decisions>
59
79
  * or markdown-header section body). Returns the extracted decisions and a count
@@ -193,11 +213,13 @@ function extractDecisions(content) {
193
213
  }
194
214
  // FIX A: Block present but 0 extracted and no parse-misses.
195
215
  // Only report could-not-parse when there is genuine evidence of real decisions
196
- // that failed to parse: a \bD- token in the block text, or an unterminated fence.
197
- // An empty scaffold (<decisions></decisions>) or an all-prose block has no such
198
- // evidence — treat as none-present so the gate passes cleanly.
216
+ // that failed to parse: a bold-lead-in bullet (`- **…**`, any ID grammar — #2347),
217
+ // a \bD- token in the block text, or an unterminated fence. An empty scaffold
218
+ // (<decisions></decisions>) or an all-prose block has no such evidence — treat
219
+ // as none-present so the gate passes cleanly.
199
220
  const hasDecisionTokenInBlock = /\bD-[A-Za-z0-9]/m.test(combined);
200
- if (hasDecisionTokenInBlock || unterminatedFence) {
221
+ const hasBoldLeadInBullet = boldLeadInBulletRe.test(combined);
222
+ if (hasDecisionTokenInBlock || hasBoldLeadInBullet || unterminatedFence) {
201
223
  return { decisions: [], outcome: 'could-not-parse' };
202
224
  }
203
225
  return { decisions: [], outcome: 'none-present' };
@@ -217,11 +239,13 @@ function extractDecisions(content) {
217
239
  return { decisions, outcome: 'could-not-parse' };
218
240
  }
219
241
  // FIX A: Heading found but 0 extracted and no parse-misses.
220
- // Only report could-not-parse when the section body contains a D- token.
221
- // A heading with only prose, sub-headings, or all-discretion content
222
- // (no trackable D- tokens) is a legitimate empty/discretion section → none-present.
242
+ // Report could-not-parse when the section body holds a decision-entry-shaped
243
+ // bold-lead-in bullet (`- **…**`, any ID grammar — #2347) or a D- token. A
244
+ // heading with only prose, sub-headings, or all-discretion content (no such
245
+ // evidence) is a legitimate empty/discretion section → none-present.
223
246
  const hasDecisionTokenInSection = /\bD-[A-Za-z0-9]/m.test(section.body);
224
- if (hasDecisionTokenInSection) {
247
+ const hasBoldLeadInBulletInSection = boldLeadInBulletRe.test(section.body);
248
+ if (hasDecisionTokenInSection || hasBoldLeadInBulletInSection) {
225
249
  return { decisions: [], outcome: 'could-not-parse' };
226
250
  }
227
251
  return { decisions: [], outcome: 'none-present' };
@@ -259,6 +259,12 @@ function cmdDocsInit(cwd, raw) {
259
259
  const agentStatus = checkAgentsInstalled();
260
260
  result['agents_installed'] = agentStatus.agents_installed;
261
261
  result['missing_agents'] = agentStatus.missing_agents;
262
+ // #2402: withProjectRoot injects response_language when set; cmdDocsInit predates
263
+ // that helper and never picked it up, so docs-update's orchestrator-owned prompts
264
+ // silently stayed English even with response_language configured.
265
+ if (config.response_language) {
266
+ result['response_language'] = config.response_language;
267
+ }
262
268
  output(result, raw, undefined);
263
269
  }
264
270
  module.exports = { cmdDocsInit };
@@ -69,7 +69,7 @@ const SAFE_PATH_RE = /^(?!.*\.\.)(?:[A-Za-z0-9_.][A-Za-z0-9_.\-]*)(?:\/[A-Za-z0-
69
69
  function classifyFile(file) {
70
70
  if (typeof file !== 'string' || !file)
71
71
  return null;
72
- const norm = file.replace(/\\/g, '/');
72
+ const norm = (0, shell_command_projection_cjs_1.posixNormalize)(file);
73
73
  if (MIGRATION_RES.some((r) => r.test(norm)))
74
74
  return 'migration';
75
75
  if (ROUTE_RES.some((r) => r.test(norm)))
@@ -87,7 +87,7 @@ function classifyFile(file) {
87
87
  * check `structureMd.includes('src/lib')` holds.
88
88
  */
89
89
  function isPathMapped(file, structureMd) {
90
- const norm = file.replace(/\\/g, '/');
90
+ const norm = (0, shell_command_projection_cjs_1.posixNormalize)(file);
91
91
  const parts = norm.split('/');
92
92
  // Check prefixes from longest to shortest; any hit means "mapped".
93
93
  for (let i = parts.length - 1; i >= 1; i--) {
@@ -130,7 +130,7 @@ function detectDrift(input) {
130
130
  const elements = [];
131
131
  const seen = new Map();
132
132
  for (const rawFile of added) {
133
- const file = rawFile.replace(/\\/g, '/');
133
+ const file = (0, shell_command_projection_cjs_1.posixNormalize)(rawFile);
134
134
  const specific = classifyFile(file);
135
135
  let category = specific;
136
136
  if (!category) {
@@ -251,7 +251,7 @@ function chooseAffectedPaths(paths) {
251
251
  for (const raw of paths || []) {
252
252
  if (typeof raw !== 'string' || !raw)
253
253
  continue;
254
- const file = raw.replace(/\\/g, '/');
254
+ const file = (0, shell_command_projection_cjs_1.posixNormalize)(raw);
255
255
  const parts = file.split('/');
256
256
  if (parts.length === 0)
257
257
  continue;
@@ -26,9 +26,21 @@ exports.isPathConfined = isPathConfined;
26
26
  exports.assertDescriptorConfined = assertDescriptorConfined;
27
27
  const node_path_1 = __importDefault(require("node:path"));
28
28
  /**
29
- * Pure path-containment check (cross-platform). `target` is confined to `root`
30
- * iff resolving it relative to `root` yields a path equal to or under `root`.
29
+ * Pure LEXICAL path-containment check (cross-platform). `target` is confined
30
+ * to `root` iff resolving it relative to `root` (via `path.resolve` — string
31
+ * manipulation, no filesystem access) yields a path equal to or under `root`.
31
32
  * Absolute paths outside `root` and `..`-escapes return false.
33
+ *
34
+ * NOT a realpath check: this function never calls `fs.realpathSync` and does
35
+ * not detect a symlink along `target` (or an existing path component of
36
+ * `root`) that would redirect the LEXICALLY-confined path to a physically
37
+ * different, unconfined location on disk. A caller relying on this for a
38
+ * write-confinement guarantee against a symlink-planting attacker must pair
39
+ * it with a symlink check (or refuse to follow symlinks at write time) — see
40
+ * capability-source.cts's install adapters (:491,577,675), which is what
41
+ * currently keeps every caller of this function's callers symlink-safe: they
42
+ * reject symlinks upstream, before a target ever reaches a lexical-only check
43
+ * like this one.
32
44
  */
33
45
  function isPathConfined(target, root) {
34
46
  if (typeof target !== 'string' || typeof root !== 'string' || target.length === 0 || root.length === 0) {