@opengsd/gsd-core 1.7.0 → 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 (165) 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 +29 -2
  8. package/agents/gsd-planner.md +29 -36
  9. package/agents/gsd-verifier.md +2 -2
  10. package/bin/install.js +1152 -80
  11. package/commands/gsd/ai-integration-phase.md +1 -1
  12. package/commands/gsd/mempalace-capture.md +9 -5
  13. package/commands/gsd/new-milestone.md +1 -1
  14. package/commands/gsd/plan-phase.md +5 -3
  15. package/commands/gsd/plan-review-convergence.md +3 -2
  16. package/gsd-core/bin/gsd-tools.cjs +1878 -2507
  17. package/gsd-core/bin/lib/adapter-imperative.cjs +8 -1
  18. package/gsd-core/bin/lib/agent-command-router.cjs +20 -5
  19. package/gsd-core/bin/lib/api-coverage.cjs +338 -45
  20. package/gsd-core/bin/lib/broken-windows.cjs +716 -0
  21. package/gsd-core/bin/lib/capability-command-router.cjs +733 -0
  22. package/gsd-core/bin/lib/capability-registry.cjs +155 -86
  23. package/gsd-core/bin/lib/capability-writer.cjs +6 -1
  24. package/gsd-core/bin/lib/check-command-router.cjs +128 -25
  25. package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +115 -27
  26. package/gsd-core/bin/lib/claude-orchestration.cjs +84 -9
  27. package/gsd-core/bin/lib/command-aliases.cjs +14 -0
  28. package/gsd-core/bin/lib/commands.cjs +81 -4
  29. package/gsd-core/bin/lib/config-loader.cjs +14 -2
  30. package/gsd-core/bin/lib/config.cjs +69 -18
  31. package/gsd-core/bin/lib/core-utils.cjs +6 -1
  32. package/gsd-core/bin/lib/decisions.cjs +32 -8
  33. package/gsd-core/bin/lib/docs.cjs +6 -0
  34. package/gsd-core/bin/lib/external-descriptor-trust.cjs +14 -2
  35. package/gsd-core/bin/lib/gap-checker.cjs +17 -2
  36. package/gsd-core/bin/lib/init.cjs +111 -47
  37. package/gsd-core/bin/lib/install-engine.cjs +298 -23
  38. package/gsd-core/bin/lib/install-profiles.cjs +239 -1
  39. package/gsd-core/bin/lib/installer-migrations/005-opencode-baseline-commands-dir.cjs +146 -0
  40. package/gsd-core/bin/lib/installer-migrations/006-pi-extension-cjs-to-js.cjs +91 -0
  41. package/gsd-core/bin/lib/installer-migrations.cjs +44 -5
  42. package/gsd-core/bin/lib/markdown-sectionizer.cjs +107 -0
  43. package/gsd-core/bin/lib/milestone.cjs +246 -12
  44. package/gsd-core/bin/lib/model-catalog.cjs +19 -4
  45. package/gsd-core/bin/lib/model-resolver.cjs +189 -7
  46. package/gsd-core/bin/lib/onboard-projection.cjs +11 -8
  47. package/gsd-core/bin/lib/phase-id.cjs +26 -4
  48. package/gsd-core/bin/lib/phase.cjs +201 -12
  49. package/gsd-core/bin/lib/plan-scan.cjs +70 -2
  50. package/gsd-core/bin/lib/roadmap-parser.cjs +7 -4
  51. package/gsd-core/bin/lib/roadmap.cjs +13 -3
  52. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +7 -1
  53. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +22 -8
  54. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +16 -0
  55. package/gsd-core/bin/lib/smart-entry.cjs +69 -4
  56. package/gsd-core/bin/lib/state-document.cjs +7 -4
  57. package/gsd-core/bin/lib/state-transition.cjs +22 -1
  58. package/gsd-core/bin/lib/state.cjs +65 -11
  59. package/gsd-core/bin/lib/surface.cjs +51 -9
  60. package/gsd-core/bin/lib/uat.cjs +420 -5
  61. package/gsd-core/bin/lib/validate.cjs +12 -8
  62. package/gsd-core/bin/lib/verification.cjs +112 -17
  63. package/gsd-core/bin/lib/verify.cjs +220 -22
  64. package/gsd-core/bin/shared/config-schema.manifest.json +3 -2
  65. package/gsd-core/references/api-coverage.md +37 -7
  66. package/gsd-core/references/checkpoints.md +1 -1
  67. package/gsd-core/references/common-bug-patterns.md +13 -0
  68. package/gsd-core/references/debugger-bug-taxonomy.md +111 -0
  69. package/gsd-core/references/debugger-fix-acceptance.md +157 -0
  70. package/gsd-core/references/debugger-philosophy.md +1 -0
  71. package/gsd-core/references/debugger-prevention.md +98 -0
  72. package/gsd-core/references/debugger-rca-branching.md +98 -0
  73. package/gsd-core/references/debugger-repro-hardening.md +130 -0
  74. package/gsd-core/references/debugger-sbfl.md +110 -0
  75. package/gsd-core/references/debugger-semantic-recall.md +81 -0
  76. package/gsd-core/references/execute-phase-quota-recovery.md +55 -0
  77. package/gsd-core/references/execute-phase-requirement-revert.md +8 -0
  78. package/gsd-core/references/execute-phase-response-language.md +7 -0
  79. package/gsd-core/references/planner-antipatterns.md +6 -0
  80. package/gsd-core/references/planner-mvp-mode.md +12 -13
  81. package/gsd-core/references/planner-preconditions.md +156 -0
  82. package/gsd-core/references/planner-reversibility.md +132 -0
  83. package/gsd-core/references/reviewer-instances.md +9 -7
  84. package/gsd-core/references/skeleton-template.md +1 -1
  85. package/gsd-core/references/thinking-models-planning.md +3 -1
  86. package/gsd-core/templates/DEBUG.md +5 -3
  87. package/gsd-core/workflows/add-phase.md +2 -0
  88. package/gsd-core/workflows/add-tests.md +3 -1
  89. package/gsd-core/workflows/add-todo.md +32 -1
  90. package/gsd-core/workflows/ai-integration-phase.md +4 -2
  91. package/gsd-core/workflows/audit-fix.md +2 -2
  92. package/gsd-core/workflows/check-todos.md +3 -1
  93. package/gsd-core/workflows/cleanup.md +7 -1
  94. package/gsd-core/workflows/code-review.md +17 -5
  95. package/gsd-core/workflows/complete-milestone.md +3 -0
  96. package/gsd-core/workflows/debug.md +25 -5
  97. package/gsd-core/workflows/diagnose-issues.md +1 -1
  98. package/gsd-core/workflows/discovery-phase.md +7 -0
  99. package/gsd-core/workflows/discuss-phase/templates/context.md +16 -2
  100. package/gsd-core/workflows/discuss-phase-assumptions.md +3 -0
  101. package/gsd-core/workflows/do.md +7 -1
  102. package/gsd-core/workflows/docs-update.md +1 -0
  103. package/gsd-core/workflows/eval-review.md +3 -0
  104. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +4 -4
  105. package/gsd-core/workflows/execute-phase/steps/regression-gate.md +2 -2
  106. package/gsd-core/workflows/execute-phase.md +25 -34
  107. package/gsd-core/workflows/execute-plan.md +15 -4
  108. package/gsd-core/workflows/graduation.md +3 -0
  109. package/gsd-core/workflows/health.md +7 -1
  110. package/gsd-core/workflows/help/modes/full.md +6 -2
  111. package/gsd-core/workflows/import.md +8 -2
  112. package/gsd-core/workflows/inbox.md +7 -0
  113. package/gsd-core/workflows/ingest-docs.md +15 -10
  114. package/gsd-core/workflows/manager.md +3 -1
  115. package/gsd-core/workflows/map-codebase.md +4 -4
  116. package/gsd-core/workflows/mvp-phase.md +3 -0
  117. package/gsd-core/workflows/new-milestone.md +69 -21
  118. package/gsd-core/workflows/new-project.md +17 -15
  119. package/gsd-core/workflows/new-workspace.md +3 -1
  120. package/gsd-core/workflows/onboard.md +3 -0
  121. package/gsd-core/workflows/plan-phase.md +14 -5
  122. package/gsd-core/workflows/plan-review-convergence.md +48 -3
  123. package/gsd-core/workflows/plant-seed.md +3 -0
  124. package/gsd-core/workflows/profile-user.md +7 -1
  125. package/gsd-core/workflows/progress.md +31 -3
  126. package/gsd-core/workflows/quick.md +19 -7
  127. package/gsd-core/workflows/remove-workspace.md +3 -0
  128. package/gsd-core/workflows/review.md +89 -73
  129. package/gsd-core/workflows/scan.md +1 -1
  130. package/gsd-core/workflows/secure-phase.md +3 -0
  131. package/gsd-core/workflows/settings-integrations.md +3 -0
  132. package/gsd-core/workflows/settings.md +3 -0
  133. package/gsd-core/workflows/ship.md +50 -3
  134. package/gsd-core/workflows/sketch.md +3 -0
  135. package/gsd-core/workflows/smart-entry.md +3 -0
  136. package/gsd-core/workflows/spike.md +7 -1
  137. package/gsd-core/workflows/ui-phase.md +3 -1
  138. package/gsd-core/workflows/ui-review.md +3 -0
  139. package/gsd-core/workflows/undo.md +7 -0
  140. package/gsd-core/workflows/update.md +2 -0
  141. package/gsd-core/workflows/validate-phase.md +3 -0
  142. package/gsd-core/workflows/verify-phase.md +2 -2
  143. package/gsd-core/workflows/verify-work.md +7 -3
  144. package/hooks/dist/gsd-context-monitor.js +27 -9
  145. package/hooks/dist/gsd-statusline.js +88 -3
  146. package/hooks/gsd-context-monitor.js +27 -9
  147. package/hooks/gsd-statusline.js +88 -3
  148. package/package.json +6 -4
  149. package/pi/gsd.cjs +8 -2
  150. package/scripts/changeset/lint.cjs +1 -0
  151. package/scripts/changeset/parse.cjs +26 -0
  152. package/scripts/check-glossary-refs.cjs +220 -0
  153. package/scripts/ci-rebase-check.cjs +48 -4
  154. package/scripts/gen-adr-index.cjs +526 -0
  155. package/scripts/gen-test-timings.cjs +201 -0
  156. package/scripts/lint-portable-timeout.cjs +140 -0
  157. package/scripts/lint-test-file-count.allowlist.json +1 -0
  158. package/scripts/release-tarball-smoke.cjs +18 -11
  159. package/scripts/run-tests.cjs +420 -58
  160. package/skills/gsd-ai-integration-phase/SKILL.md +1 -1
  161. package/skills/gsd-mempalace-capture/SKILL.md +9 -5
  162. package/skills/gsd-new-milestone/SKILL.md +1 -1
  163. package/skills/gsd-plan-phase/SKILL.md +5 -3
  164. package/skills/gsd-plan-review-convergence/SKILL.md +3 -2
  165. 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");
@@ -47,6 +50,44 @@ const { MODEL_PROFILES, VALID_PHASE_TYPES } = modelProfiles;
47
50
  const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
48
51
  const clock_cjs_1 = require("./clock.cjs");
49
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
+ }
50
91
  /**
51
92
  * Determine phase status by checking plan/summary counts AND verification state.
52
93
  * Introduces "Executed" for phases with all summaries but no passing verification.
@@ -128,6 +169,9 @@ function cmdListTodos(cwd, area, raw) {
128
169
  const createdMatch = content.match(/^created:\s*(.+)$/m);
129
170
  const titleMatch = content.match(/^title:\s*(.+)$/m);
130
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);
131
175
  const todoArea = areaMatch ? areaMatch[1].trim() : 'general';
132
176
  // Apply area filter if specified
133
177
  if (area && todoArea !== area)
@@ -139,6 +183,7 @@ function cmdListTodos(cwd, area, raw) {
139
183
  title: titleMatch ? titleMatch[1].trim() : 'Untitled',
140
184
  area: todoArea,
141
185
  path: toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(pendingDir, file))),
186
+ ...(severityMatch ? { severity: severityMatch[1].trim() } : {}),
142
187
  });
143
188
  }
144
189
  }
@@ -394,7 +439,8 @@ function cmdResolveGranularity(cwd, phaseType, raw, override) {
394
439
  * { model, profile, effort, effort_rendered, effort_param, effort_propagation,
395
440
  * fast_mode, fast_mode_supported, [unknown_agent] }
396
441
  *
397
- * Flags: --effort <level>, --fast-mode <true|false>, --attempt <n>
442
+ * Flags: --effort <level>, --fast-mode <true|false>, --attempt <n>,
443
+ * --failure-class <class> (#2296)
398
444
  */
399
445
  function cmdResolveExecution(cwd, agentType, raw, opts) {
400
446
  if (!agentType) {
@@ -403,7 +449,29 @@ function cmdResolveExecution(cwd, agentType, raw, opts) {
403
449
  opts = opts || {};
404
450
  const config = loadConfig(cwd);
405
451
  const profile = config['model_profile'] || 'balanced';
406
- 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
+ }
407
475
  const effortOpts = {};
408
476
  if (typeof opts.effortOverride === 'string')
409
477
  effortOpts['override'] = opts.effortOverride;
@@ -430,6 +498,8 @@ function cmdResolveExecution(cwd, agentType, raw, opts) {
430
498
  };
431
499
  if (!agentModels)
432
500
  result['unknown_agent'] = true;
501
+ if (escalation)
502
+ result['escalation'] = escalation;
433
503
  output(result, raw, effort);
434
504
  }
435
505
  /**
@@ -1385,7 +1455,12 @@ function cmdStats(cwd, format, raw) {
1385
1455
  name: existing?.name || phaseName,
1386
1456
  plans: (existing?.plans || 0) + plans,
1387
1457
  summaries: (existing?.summaries || 0) + summaries,
1388
- 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,
1389
1464
  });
1390
1465
  }
1391
1466
  }
@@ -1507,6 +1582,8 @@ function cmdCheckCommit(cwd, raw) {
1507
1582
  module.exports = {
1508
1583
  groupFilesBySubrepo,
1509
1584
  determinePhaseStatus,
1585
+ foldPhaseStatus,
1586
+ PHASE_STATUS_PRECEDENCE,
1510
1587
  cmdGenerateSlug,
1511
1588
  cmdCurrentTimestamp,
1512
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 };
@@ -172,7 +172,11 @@ function isGitIgnored(cwd, targetPath) {
172
172
  return ignored;
173
173
  }
174
174
  // ─── Model alias resolution ───────────────────────────────────────────────────
175
- 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;
176
180
  const _warnedConfigKeys = new Set();
177
181
  function _warnUnknownProfileOverrides(parsed, configLabel) {
178
182
  if (!parsed || typeof parsed !== 'object')
@@ -687,6 +691,14 @@ function loadConfigResolved(cwd, options = {}) {
687
691
  fast_mode: (globalDefaults['fast_mode']) || null,
688
692
  agent_skills: (globalDefaults['agent_skills']) || {},
689
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,
690
702
  };
691
703
  // Branch D: global-defaults
692
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];
@@ -698,6 +737,10 @@ function cmdConfigSet(cwd, keyPath, value, raw) {
698
737
  error(`Invalid statusline.show_context_tokens '${val}'. Must be a boolean (true or false).`);
699
738
  }
700
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');
701
744
  // statusline.show_git — boolean only
702
745
  if (kp === 'statusline.show_git') {
703
746
  if (typeof parsedValue !== 'boolean') {
@@ -825,16 +868,15 @@ function cmdConfigGet(cwd, keyPath, raw, defaultValue) {
825
868
  config = JSON.parse(node_fs_1.default.readFileSync(configPath, 'utf-8'));
826
869
  }
827
870
  else if (hasDefault) {
828
- // eslint-disable-next-line @typescript-eslint/no-base-to-string
829
- output(defaultValue, raw, String(defaultValue));
830
- return;
831
- }
832
- else if (Object.prototype.hasOwnProperty.call(SCHEMA_DEFAULTS, kp)) {
833
- const def = SCHEMA_DEFAULTS[kp];
834
- output(def, raw, String(def));
871
+ emitResolvedDefault(kp, defaultValue, raw);
835
872
  return;
836
873
  }
837
874
  else {
875
+ const sd = resolveSchemaDefault(cwd, kp);
876
+ if (sd.found) {
877
+ emitResolvedDefault(kp, sd.value, raw);
878
+ return;
879
+ }
838
880
  error('No config.json found at ' + configPath, ERROR_REASON.CONFIG_NO_FILE);
839
881
  }
840
882
  }
@@ -848,29 +890,38 @@ function cmdConfigGet(cwd, keyPath, raw, defaultValue) {
848
890
  let current = config;
849
891
  for (const key of keys) {
850
892
  if (current === undefined || current === null || typeof current !== 'object') {
851
- // eslint-disable-next-line @typescript-eslint/no-base-to-string
852
893
  if (hasDefault) {
853
- output(defaultValue, raw, String(defaultValue));
894
+ emitResolvedDefault(kp, defaultValue, raw);
854
895
  return;
855
896
  }
856
- if (Object.prototype.hasOwnProperty.call(SCHEMA_DEFAULTS, kp)) {
857
- const def = SCHEMA_DEFAULTS[kp];
858
- output(def, raw, String(def));
897
+ const sd = resolveSchemaDefault(cwd, kp);
898
+ if (sd.found) {
899
+ emitResolvedDefault(kp, sd.value, raw);
859
900
  return;
860
901
  }
861
902
  error(`Key not found: ${kp}`, ERROR_REASON.CONFIG_KEY_NOT_FOUND);
862
903
  }
863
- 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;
864
916
  }
865
917
  if (current === undefined) {
866
- // eslint-disable-next-line @typescript-eslint/no-base-to-string
867
918
  if (hasDefault) {
868
- output(defaultValue, raw, String(defaultValue));
919
+ emitResolvedDefault(kp, defaultValue, raw);
869
920
  return;
870
921
  }
871
- if (Object.prototype.hasOwnProperty.call(SCHEMA_DEFAULTS, kp)) {
872
- const def = SCHEMA_DEFAULTS[kp];
873
- output(def, raw, String(def));
922
+ const sd = resolveSchemaDefault(cwd, kp);
923
+ if (sd.found) {
924
+ emitResolvedDefault(kp, sd.value, raw);
874
925
  return;
875
926
  }
876
927
  error(`Key not found: ${kp}`, ERROR_REASON.CONFIG_KEY_NOT_FOUND);
@@ -186,8 +186,13 @@ function extractCanonicalPlanId(filename) {
186
186
  // or a single-digit-plus-letter id ("3A"); a *bare* single digit is a slug word,
187
187
  // so "46-6-rs-…" is not paired into a "46-6" id while "3A-01" stays intact.
188
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');
189
194
  const phaseIdx = parts.findIndex(p => tokenRe.test(p));
190
- 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])) {
191
196
  return `${parts[phaseIdx]}-${parts[phaseIdx + 1]}`;
192
197
  }
193
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 };
@@ -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) {
@@ -298,7 +298,16 @@ function runGapAnalysis(cwd, phaseDir, options = {}) {
298
298
  const mismatchMsg = '## Post-Planning Gap Analysis\n\nextracted 0 of N — possible format mismatch in CONTEXT.md decisions block.\n';
299
299
  // If there are also requirement items, include them in the return with the
300
300
  // mismatch summary appended, so the caller still sees requirement coverage.
301
- if (items.length > 0) {
301
+ // #2334 HIGH 1: gate on `ghostReqIds.length > 0` too — identical defect to
302
+ // the one fixed at #2316-6b (~34 lines below, at the `items.length === 0`
303
+ // early return): a phase whose EVERY cited REQ-ID is unregistered has
304
+ // `items.length === 0` (all its requirement items were filtered out at
305
+ // ~line 297) but still has real ghost rows to report. Without this guard,
306
+ // a single malformed `<decisions>` line in CONTEXT.md made an all-ghost
307
+ // phase's ghost rows silently vanish (this could-not-parse branch fell
308
+ // through to the bare `mismatchMsg`-only return below, dropping ghost
309
+ // rows that the general path further down correctly surfaces).
310
+ if (items.length > 0 || ghostReqIds.length > 0) {
302
311
  const rows = sortRows([
303
312
  ...detectCoverage(items, planText),
304
313
  ...ghostReqIds.map(id => ({ source: 'REQUIREMENTS.md', item: id, status: 'Missing from REQUIREMENTS.md' })),
@@ -325,7 +334,13 @@ function runGapAnalysis(cwd, phaseDir, options = {}) {
325
334
  };
326
335
  }
327
336
  // #1365: if no items at all, surface a clean no-check message.
328
- if (items.length === 0) {
337
+ // #2316-6b: this must NOT fire when `ghostReqIds` is non-empty — a phase
338
+ // whose EVERY cited REQ-ID is unregistered has `items.length === 0` (all
339
+ // its requirement items were filtered out at ~line 297) but still has real
340
+ // ghost rows to report below. Without this guard, an all-orphan phase
341
+ // reported LESS than a partially-orphan one (which falls through to the
342
+ // general path further down and correctly surfaces its ghost rows).
343
+ if (items.length === 0 && ghostReqIds.length === 0) {
329
344
  return {
330
345
  enabled: true,
331
346
  rows: [],