@opengsd/gsd-core 1.9.0 → 1.10.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 (223) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +2 -3
  3. package/.opencode/plugins/gsd-core.js +8 -1
  4. package/agents/gsd-code-fixer.md +131 -34
  5. package/agents/gsd-debugger.md +12 -246
  6. package/agents/gsd-executor.md +7 -5
  7. package/agents/gsd-integration-checker.md +3 -0
  8. package/agents/gsd-plan-checker.md +9 -0
  9. package/agents/gsd-planner.md +5 -8
  10. package/agents/gsd-roadmapper.md +21 -3
  11. package/agents/gsd-verifier.md +14 -70
  12. package/bin/install.js +503 -341
  13. package/commands/gsd/mempalace-capture.md +1 -1
  14. package/commands/gsd/new-milestone.md +1 -1
  15. package/commands/gsd/plan-phase.md +1 -1
  16. package/gsd-core/bin/gsd-tools.cjs +607 -63
  17. package/gsd-core/bin/lib/active-workstream-store.cjs +25 -0
  18. package/gsd-core/bin/lib/agent-install-check.cjs +38 -6
  19. package/gsd-core/bin/lib/api-coverage.cjs +120 -0
  20. package/gsd-core/bin/lib/audit.cjs +89 -1
  21. package/gsd-core/bin/lib/broken-windows.cjs +36 -6
  22. package/gsd-core/bin/lib/capability-registry.cjs +96 -110
  23. package/gsd-core/bin/lib/capability-validator.cjs +12 -2
  24. package/gsd-core/bin/lib/check-command-router.cjs +43 -1
  25. package/gsd-core/bin/lib/command-aliases.cjs +72 -0
  26. package/gsd-core/bin/lib/commands.cjs +26 -25
  27. package/gsd-core/bin/lib/commonjs-marker.cjs +136 -0
  28. package/gsd-core/bin/lib/config-loader.cjs +1 -0
  29. package/gsd-core/bin/lib/config.cjs +12 -1
  30. package/gsd-core/bin/lib/context-composer.cjs +278 -0
  31. package/gsd-core/bin/lib/context-predicates.cjs +506 -0
  32. package/gsd-core/bin/lib/core-utils.cjs +91 -12
  33. package/gsd-core/bin/lib/docs.cjs +3 -2
  34. package/gsd-core/bin/lib/external-job.cjs +19 -4
  35. package/gsd-core/bin/lib/frontmatter.cjs +84 -12
  36. package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +57 -6
  37. package/gsd-core/bin/lib/git-base-branch.cjs +58 -15
  38. package/gsd-core/bin/lib/graphify.cjs +142 -27
  39. package/gsd-core/bin/lib/gsd2-import.cjs +27 -4
  40. package/gsd-core/bin/lib/host-integration.cjs +13 -1
  41. package/gsd-core/bin/lib/init-command-router.cjs +83 -8
  42. package/gsd-core/bin/lib/init.cjs +1021 -57
  43. package/gsd-core/bin/lib/install-engine.cjs +64 -10
  44. package/gsd-core/bin/lib/install-profiles.cjs +27 -1
  45. package/gsd-core/bin/lib/installer-migration-authoring.cjs +3 -1
  46. package/gsd-core/bin/lib/installer-migration-report.cjs +4 -0
  47. package/gsd-core/bin/lib/installer-migrations/007-retire-config-root-commonjs-marker.cjs +149 -0
  48. package/gsd-core/bin/lib/installer-migrations/008-cursor-retire-commands-surface.cjs +55 -0
  49. package/gsd-core/bin/lib/installer-migrations/009-pi-retire-reserved-hooks-dir.cjs +199 -0
  50. package/gsd-core/bin/lib/installer-migrations.cjs +87 -1
  51. package/gsd-core/bin/lib/io.cjs +28 -3
  52. package/gsd-core/bin/lib/markdown-sectionizer.cjs +6 -0
  53. package/gsd-core/bin/lib/mcp-catalog.cjs +518 -0
  54. package/gsd-core/bin/lib/mcp-server.cjs +135 -3
  55. package/gsd-core/bin/lib/milestone.cjs +106 -51
  56. package/gsd-core/bin/lib/phase-id.cjs +63 -0
  57. package/gsd-core/bin/lib/phase-locator.cjs +138 -45
  58. package/gsd-core/bin/lib/phase.cjs +260 -25
  59. package/gsd-core/bin/lib/plan-dependency-graph.cjs +232 -0
  60. package/gsd-core/bin/lib/planning-workspace.cjs +4 -0
  61. package/gsd-core/bin/lib/project-root.cjs +48 -0
  62. package/gsd-core/bin/lib/prompt-budget.cjs +128 -165
  63. package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +80 -0
  64. package/gsd-core/bin/lib/review-lane-descriptor.cjs +99 -0
  65. package/gsd-core/bin/lib/review-lane-runner.cjs +30 -6
  66. package/gsd-core/bin/lib/roadmap-command-router.cjs +42 -9
  67. package/gsd-core/bin/lib/roadmap-parser.cjs +100 -18
  68. package/gsd-core/bin/lib/roadmap.cjs +37 -7
  69. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +195 -62
  70. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +15 -3
  71. package/gsd-core/bin/lib/runtime-homes.cjs +154 -41
  72. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +105 -41
  73. package/gsd-core/bin/lib/section-manifest.cjs +209 -0
  74. package/gsd-core/bin/lib/shell-command-projection.cjs +113 -27
  75. package/gsd-core/bin/lib/smart-entry.cjs +12 -0
  76. package/gsd-core/bin/lib/state-transition.cjs +73 -8
  77. package/gsd-core/bin/lib/state.cjs +151 -62
  78. package/gsd-core/bin/lib/surface.cjs +12 -1
  79. package/gsd-core/bin/lib/uat-predicate.cjs +11 -1
  80. package/gsd-core/bin/lib/uat.cjs +320 -21
  81. package/gsd-core/bin/lib/unusable-input.cjs +9 -0
  82. package/gsd-core/bin/lib/verification.cjs +29 -12
  83. package/gsd-core/bin/lib/verify.cjs +29 -5
  84. package/gsd-core/bin/lib/workflow-fragments.cjs +557 -0
  85. package/gsd-core/bin/lib/workstream-inventory-builder.cjs +181 -18
  86. package/gsd-core/bin/lib/workstream-inventory.cjs +519 -27
  87. package/gsd-core/bin/lib/workstream.cjs +6 -0
  88. package/gsd-core/bin/lib/worktree-base-ref.cjs +50 -6
  89. package/gsd-core/bin/lib/worktree-safety.cjs +276 -118
  90. package/gsd-core/bin/shared/config-schema.manifest.json +2 -0
  91. package/gsd-core/references/artifact-types.md +10 -3
  92. package/gsd-core/references/autonomous-ui-design-contract.md +42 -0
  93. package/gsd-core/references/debugger-techniques.md +255 -0
  94. package/gsd-core/references/research-documentation-lookup.md +5 -3
  95. package/gsd-core/references/specless-probe-fallback.md +7 -6
  96. package/gsd-core/references/verifier-wiring-patterns.md +100 -0
  97. package/gsd-core/references/worktree-branch-check.md +2 -2
  98. package/gsd-core/templates/summary-complex.md +2 -0
  99. package/gsd-core/templates/summary-minimal.md +2 -0
  100. package/gsd-core/templates/summary-standard.md +2 -0
  101. package/gsd-core/templates/summary.md +2 -0
  102. package/gsd-core/workflows/audit-milestone.md +3 -0
  103. package/gsd-core/workflows/autonomous/steps/converge-banner.md +1 -0
  104. package/gsd-core/workflows/autonomous/steps/converge-dispatch-bg.md +11 -0
  105. package/gsd-core/workflows/autonomous/steps/converge-dispatch-inline.md +7 -0
  106. package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +21 -0
  107. package/gsd-core/workflows/autonomous/steps/converge-loop.md +7 -0
  108. package/gsd-core/workflows/autonomous.md +32 -69
  109. package/gsd-core/workflows/code-review/steps/dispatch-fix.md +39 -0
  110. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +83 -0
  111. package/gsd-core/workflows/code-review.md +42 -145
  112. package/gsd-core/workflows/complete-milestone/steps/git-tag.md +29 -0
  113. package/gsd-core/workflows/complete-milestone.md +23 -81
  114. package/gsd-core/workflows/debug.md +9 -12
  115. package/gsd-core/workflows/diagnose-issues.md +22 -0
  116. package/gsd-core/workflows/discovery-phase.md +4 -4
  117. package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +15 -0
  118. package/gsd-core/workflows/discuss-phase-assumptions.md +5 -16
  119. package/gsd-core/workflows/docs-update/steps/dispatch-monorepo-packages.md +51 -0
  120. package/gsd-core/workflows/docs-update.md +8 -51
  121. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +34 -2
  122. package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +50 -0
  123. package/gsd-core/workflows/execute-phase/steps/partial-wave.md +31 -0
  124. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +19 -0
  125. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +42 -0
  126. package/gsd-core/workflows/execute-phase/steps/regression-gate.md +43 -37
  127. package/gsd-core/workflows/execute-phase.md +65 -137
  128. package/gsd-core/workflows/execute-plan.md +1 -1
  129. package/gsd-core/workflows/help/modes/full.md +6 -1
  130. package/gsd-core/workflows/ingest-docs.md +2 -1
  131. package/gsd-core/workflows/new-milestone/steps/project-md-milestone-write.md +16 -0
  132. package/gsd-core/workflows/new-milestone/steps/reset-phase-safety.md +19 -0
  133. package/gsd-core/workflows/new-milestone.md +21 -38
  134. package/gsd-core/workflows/new-project/steps/auto-mode-config.md +176 -0
  135. package/gsd-core/workflows/new-project/steps/auto-mode-detection.md +32 -0
  136. package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +18 -0
  137. package/gsd-core/workflows/new-project.md +13 -226
  138. package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +15 -0
  139. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +110 -0
  140. package/gsd-core/workflows/plan-phase/steps/prd-express-gate.md +8 -0
  141. package/gsd-core/workflows/plan-phase/steps/research-only-early-exit.md +17 -0
  142. package/gsd-core/workflows/plan-phase/steps/research-only-modifiers.md +16 -0
  143. package/gsd-core/workflows/plan-phase/steps/reviews-prerequisite.md +17 -0
  144. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +149 -0
  145. package/gsd-core/workflows/plan-phase.md +49 -193
  146. package/gsd-core/workflows/progress/steps/forensic-audit.md +125 -0
  147. package/gsd-core/workflows/progress/steps/mvp-display.md +18 -0
  148. package/gsd-core/workflows/progress.md +11 -153
  149. package/gsd-core/workflows/quick/steps/discussion-phase.md +124 -0
  150. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +111 -0
  151. package/gsd-core/workflows/quick/steps/quick-verification.md +46 -0
  152. package/gsd-core/workflows/quick/steps/research-phase.md +72 -0
  153. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +37 -0
  154. package/gsd-core/workflows/quick.md +20 -390
  155. package/gsd-core/workflows/resume-project.md +3 -0
  156. package/gsd-core/workflows/review/steps/reviewer-instances-note-1.md +4 -0
  157. package/gsd-core/workflows/review/steps/reviewer-instances-note-2.md +3 -0
  158. package/gsd-core/workflows/review.md +15 -8
  159. package/gsd-core/workflows/section-manifest.json +219 -0
  160. package/gsd-core/workflows/sketch.md +1 -1
  161. package/gsd-core/workflows/spec-phase.md +17 -14
  162. package/gsd-core/workflows/spike-wrap-up.md +20 -5
  163. package/gsd-core/workflows/spike.md +50 -16
  164. package/gsd-core/workflows/sync-skills.md +49 -11
  165. package/gsd-core/workflows/transition/steps/workstream-collision-check.md +17 -0
  166. package/gsd-core/workflows/transition.md +8 -21
  167. package/gsd-core/workflows/ui-phase.md +8 -7
  168. package/gsd-core/workflows/update/steps/channel-banner.md +7 -0
  169. package/gsd-core/workflows/update.md +18 -7
  170. package/gsd-core/workflows/verify-phase.md +4 -7
  171. package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +36 -0
  172. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +21 -0
  173. package/gsd-core/workflows/verify-work.md +8 -58
  174. package/hooks/dist/gsd-agent-isolation-guard.js +428 -0
  175. package/hooks/dist/gsd-check-update-worker.js +14 -5
  176. package/hooks/dist/gsd-cursor-subagent-start.js +532 -26
  177. package/hooks/dist/gsd-read-injection-scanner.js +7 -0
  178. package/hooks/dist/gsd-statusline.js +72 -6
  179. package/hooks/dist/gsd-worktree-path-guard.js +2 -1
  180. package/hooks/dist/gsd-write-guard.js +359 -0
  181. package/hooks/dist/lib/isolation-sentinel.js +268 -0
  182. package/hooks/dist/managed-hooks-registry.cjs +2 -0
  183. package/hooks/gsd-agent-isolation-guard.js +428 -0
  184. package/hooks/gsd-check-update-worker.js +14 -5
  185. package/hooks/gsd-cursor-subagent-start.js +532 -26
  186. package/hooks/gsd-read-injection-scanner.js +7 -0
  187. package/hooks/gsd-statusline.js +72 -6
  188. package/hooks/gsd-worktree-path-guard.js +2 -1
  189. package/hooks/gsd-write-guard.js +359 -0
  190. package/hooks/hooks.json +12 -0
  191. package/hooks/lib/isolation-sentinel.js +268 -0
  192. package/hooks/managed-hooks-registry.cjs +2 -0
  193. package/package.json +14 -5
  194. package/pi/gsd.cjs +57 -12
  195. package/scripts/build-hooks.js +9 -0
  196. package/scripts/changeset/lint.cjs +9 -2
  197. package/scripts/changeset/serialize.cjs +5 -1
  198. package/scripts/gen-capability-matrix.cjs +1 -1
  199. package/scripts/gen-context-index.cjs +448 -0
  200. package/scripts/gen-inventory-manifest.cjs +101 -1
  201. package/scripts/gen-prompt-budget-parity-corpus.cjs +645 -0
  202. package/scripts/gen-registry.cjs +39 -15
  203. package/scripts/gen-section-manifest.cjs +638 -0
  204. package/scripts/generate-package-identity.cjs +4 -2
  205. package/scripts/lint-allow-test-rule-refs.allowlist.json +17 -31
  206. package/scripts/lint-compiled-artifact-sync.cjs +6 -1
  207. package/scripts/lint-docs-command-form.cjs +195 -0
  208. package/scripts/lint-docs-required.cjs +9 -1
  209. package/scripts/lint-emitted-drift-ack.cjs +215 -20
  210. package/scripts/lint-example-parser-parity.cjs +395 -0
  211. package/scripts/lint-test-file-count.allowlist.json +27 -1
  212. package/scripts/mutation-matrix.cjs +13 -0
  213. package/scripts/prompt-injection-scan.sh +27 -6
  214. package/scripts/registry-schema.cjs +323 -94
  215. package/scripts/run-tests.cjs +3 -2
  216. package/scripts/validate-registry.cjs +10 -6
  217. package/skills/gsd-autonomous/SKILL.md +1 -1
  218. package/skills/gsd-execute-phase/SKILL.md +1 -1
  219. package/skills/gsd-mempalace-capture/SKILL.md +1 -1
  220. package/skills/gsd-new-milestone/SKILL.md +1 -1
  221. package/skills/gsd-plan-phase/SKILL.md +2 -2
  222. package/vscode/package.json +1 -1
  223. package/scripts/gen-emitted-baseline.cjs +0 -145
@@ -16,6 +16,15 @@ const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs")
16
16
  // eslint-disable-next-line @typescript-eslint/no-require-imports
17
17
  const capabilityStateMod = require("./capability-state.cjs");
18
18
  const { isCapabilityActive } = capabilityStateMod;
19
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- io.cjs is an export= CommonJS module
20
+ const ioMod = require("./io.cjs");
21
+ const { serializeForOutput } = ioMod;
22
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- prompt-budget.cjs is an export= CommonJS module
23
+ const promptBudget = require("./prompt-budget.cjs");
24
+ // The repo's single token scale (phase-estimation.cts documents the rule: a
25
+ // ratio between two measurement methods measures the methods, not the miss).
26
+ // This module previously carried a private copy of the same chars/4 formula.
27
+ const { estimateTokens } = promptBudget;
19
28
  /**
20
29
  * Return the standard disabled response object.
21
30
  */
@@ -52,8 +61,12 @@ function execGraphify(cwd, args, options = {}) {
52
61
  reason: GRAPHIFY_REASON.ENOENT,
53
62
  };
54
63
  }
55
- // Timeout — seam exposes signal; spawnSync sets SIGTERM when killed by timeout.
56
- if (result.signal === 'SIGTERM') {
64
+ // Timeout — result.timedOut is derived by the shared isSpawnTimeout predicate
65
+ // (shell-command-projection.cts), keyed on error.code === 'ETIMEDOUT' rather
66
+ // than signal === 'SIGTERM': Windows does not reliably report SIGTERM on a
67
+ // timeout kill, and an externally-delivered SIGTERM (error is null) is not
68
+ // a timeout at all.
69
+ if (result.timedOut) {
57
70
  return {
58
71
  exitCode: 124,
59
72
  stdout: result.stdout,
@@ -104,15 +117,28 @@ function checkGraphifyVersion() {
104
117
  }
105
118
  }
106
119
  // Strategy 2: fall back to python3 importlib.metadata
120
+ let pyPackageConfirmed = false;
107
121
  if (!versionStr) {
108
122
  const pyResult = (0, shell_command_projection_cjs_1.execTool)('python3', [
109
123
  '-c',
110
124
  'from importlib.metadata import version; print(version("graphifyy"))',
111
125
  ], { timeout: 5000 });
112
126
  if (!pyResult.error && pyResult.exitCode === 0 && pyResult.stdout) {
113
- versionStr = pyResult.stdout;
127
+ versionStr = pyResult.stdout.trim();
128
+ pyPackageConfirmed = true; // importlib.metadata confirmed the package
114
129
  }
115
130
  }
131
+ else {
132
+ // #3020: verify the `graphify` binary on PATH is actually the graphifyy
133
+ // package — a foreign binary that happens to print a version-like string
134
+ // must not silently report compatible. If importlib.metadata cannot confirm
135
+ // the package, emit an identity warning even if the version looks right.
136
+ const pyVerify = (0, shell_command_projection_cjs_1.execTool)('python3', [
137
+ '-c',
138
+ 'from importlib.metadata import version; print(version("graphifyy"))',
139
+ ], { timeout: 5000 });
140
+ pyPackageConfirmed = !pyVerify.error && pyVerify.exitCode === 0 && !!pyVerify.stdout;
141
+ }
116
142
  if (!versionStr) {
117
143
  return { version: null, compatible: null, warning: 'Could not determine graphify version' };
118
144
  }
@@ -120,9 +146,19 @@ function checkGraphifyVersion() {
120
146
  if (parts.length < 2 || parts.some(isNaN)) {
121
147
  return { version: versionStr, compatible: null, warning: 'Could not parse version: ' + versionStr };
122
148
  }
123
- const compatible = parts[0] === 0 && parts[1] >= 4;
124
- const warning = compatible ? null : 'graphify version ' + versionStr + ' is outside tested range >=0.4.0,<1.0';
125
- return { version: versionStr, compatible, warning };
149
+ const versionInRange = parts[0] === 0 && parts[1] >= 4;
150
+ // #3020: if the `graphify` binary answered --version but the Python package
151
+ // graphifyy could not be confirmed, the tool identity is unverified — emit
152
+ // a warning naming the mismatch regardless of version-range compatibility.
153
+ if (!pyPackageConfirmed) {
154
+ return {
155
+ version: versionStr,
156
+ compatible: false,
157
+ warning: 'graphify version ' + versionStr + ' detected but the graphifyy Python package could not be confirmed — the `graphify` binary on PATH may be a different tool. Verify with: pip show graphifyy',
158
+ };
159
+ }
160
+ const warning = versionInRange ? null : 'graphify version ' + versionStr + ' is outside tested range >=0.4.0,<1.0';
161
+ return { version: versionStr, compatible: versionInRange, warning };
126
162
  }
127
163
  /**
128
164
  * Safely read and parse a JSON file. Returns null on missing file or parse error.
@@ -195,41 +231,118 @@ function seedAndExpand(graph, term, maxHops = 2) {
195
231
  const resultNodes = [...visitedNodes].map(id => nodeMap[id]).filter((n) => Boolean(n));
196
232
  return { nodes: resultNodes, edges: collectedEdges, seeds: new Set(seeds.map(n => n.id)) };
197
233
  }
234
+ /**
235
+ * The single definition of `graphifyQuery`'s wire shape.
236
+ *
237
+ * Both the emitter (`graphifyQuery`'s return) and the budget estimator go
238
+ * through here, so `budget_estimate` measures the object the caller actually
239
+ * receives rather than a private approximation of it (#2738).
240
+ */
241
+ function buildQueryResponse(term, core) {
242
+ return {
243
+ term,
244
+ nodes: core.nodes,
245
+ edges: core.edges,
246
+ total_nodes: core.nodes.length,
247
+ total_edges: core.edges.length,
248
+ trimmed: core.trimmed,
249
+ // Budget outcome (#2738) — only present when a budget was requested
250
+ ...(core.budget ? { budget_met: core.budget.met, budget_estimate: core.budget.estimate } : {}),
251
+ };
252
+ }
198
253
  /**
199
254
  * Apply token budget by dropping edges by confidence tier (D-04, D-05, D-06).
200
- * Token estimation: Math.ceil(JSON.stringify(obj).length / 4).
201
255
  * Drop order: AMBIGUOUS -> INFERRED -> EXTRACTED.
256
+ *
257
+ * The estimate measures the response **as emitted** — `serializeForOutput()`
258
+ * over the same object `graphifyQuery` returns, pretty-printed and including
259
+ * the wrapper keys. Measuring a compact `{nodes, edges}` instead understates
260
+ * the payload the caller receives, which makes `budget_met` a confident claim
261
+ * about a payload nobody is handed (#2738).
262
+ *
263
+ * `term` participates in the emitted bytes, so it is threaded through; the
264
+ * default keeps direct unit calls on the same wire shape, minus those bytes.
202
265
  */
203
- function applyBudget(result, budgetTokens) {
204
- if (!budgetTokens)
266
+ function applyBudget(result, budgetTokens, term = '') {
267
+ // == null (not truthiness): --budget 0 is a valid parsed budget the router
268
+ // forwards, and treating it as "no budget" silently returns the unbounded
269
+ // result — the same silent-non-application defect class as #974/#2738.
270
+ //
271
+ // Number.isFinite additionally keeps NaN out of the comparisons below, where
272
+ // every `estimate <= NaN` is false: the loop would strip all three tiers and
273
+ // return a seeds-only payload indistinguishable from a legitimate aggressive
274
+ // trim. The CLI cannot reach that state — graphify-command-router rejects a
275
+ // non-numeric --budget before this is called — but graphifyQuery and
276
+ // applyBudget are module-level entry points a future caller could reach
277
+ // without that validation. Infinity routes here too, and deliberately: an
278
+ // unbounded budget is not a budget.
279
+ if (budgetTokens == null || !Number.isFinite(budgetTokens))
205
280
  return result;
206
281
  const CONFIDENCE_ORDER = ['AMBIGUOUS', 'INFERRED', 'EXTRACTED'];
207
282
  let edges = [...result.edges];
208
283
  let omitted = 0;
209
- const estimateTokens = (obj) => Math.ceil(JSON.stringify(obj).length / 4);
284
+ // Nodes that survive a given edge set: edge-reachable, plus seeds (always kept)
285
+ const survivingNodes = (edgeSet) => {
286
+ const reachableNodes = new Set();
287
+ for (const edge of edgeSet) {
288
+ reachableNodes.add(edge.source);
289
+ reachableNodes.add(edge.target);
290
+ }
291
+ return result.nodes.filter(n => reachableNodes.has(n.id) || (result.seeds && result.seeds.has(n.id)));
292
+ };
293
+ const trimmedLabel = (dropped, unreachable) => dropped > 0 ? `[${dropped} edges omitted, ${unreachable} nodes unreachable]` : null;
294
+ /**
295
+ * Tokens of the response as `output()` will emit it.
296
+ *
297
+ * Self-referential by construction: `budget_estimate` is itself one of the
298
+ * emitted fields, so its own digit width counts toward the total. Resolved by
299
+ * iterating to a fixed point — the sequence is non-decreasing (a wider number,
300
+ * and `false` over `true`, can only add characters), so it settles in a couple
301
+ * of passes. The cap is a guard rather than an expectation, and it exits on the
302
+ * larger value: over-reporting is the safe direction for a budget signal;
303
+ * under-reporting is the defect this fixes.
304
+ */
305
+ const wireEstimate = (candidateNodes, candidateEdges, trimmed) => {
306
+ let est = 0;
307
+ for (let i = 0; i < 8; i++) {
308
+ const next = estimateTokens(serializeForOutput(buildQueryResponse(term, {
309
+ nodes: candidateNodes,
310
+ edges: candidateEdges,
311
+ trimmed,
312
+ budget: { met: est <= budgetTokens, estimate: est },
313
+ })));
314
+ if (next === est)
315
+ break;
316
+ est = next;
317
+ }
318
+ return est;
319
+ };
320
+ // Estimate against the post-pruning node set after each tier removal, so a
321
+ // removal that already fits (once orphaned nodes are excluded) stops the loop
322
+ // instead of dropping the next, higher-confidence tier too (#2738).
323
+ let nodes = survivingNodes(edges);
324
+ let estimate = wireEstimate(nodes, edges, trimmedLabel(omitted, result.nodes.length - nodes.length));
210
325
  for (const tier of CONFIDENCE_ORDER) {
211
- if (estimateTokens({ nodes: result.nodes, edges }) <= budgetTokens)
326
+ if (estimate <= budgetTokens)
212
327
  break;
213
328
  const before = edges.length;
214
329
  // Check both confidence and confidence_score field names (Open Question 1)
215
330
  edges = edges.filter(e => (e.confidence || e.confidence_score) !== tier);
216
331
  omitted += before - edges.length;
332
+ nodes = survivingNodes(edges);
333
+ estimate = wireEstimate(nodes, edges, trimmedLabel(omitted, result.nodes.length - nodes.length));
217
334
  }
218
- // Find unreachable nodes after edge removal
219
- const reachableNodes = new Set();
220
- for (const edge of edges) {
221
- reachableNodes.add(edge.source);
222
- reachableNodes.add(edge.target);
223
- }
224
- // Always keep seed nodes
225
- const nodes = result.nodes.filter(n => reachableNodes.has(n.id) || (result.seeds && result.seeds.has(n.id)));
226
335
  const unreachable = result.nodes.length - nodes.length;
227
336
  return {
228
337
  nodes,
229
338
  edges,
230
- trimmed: omitted > 0 ? `[${omitted} edges omitted, ${unreachable} nodes unreachable]` : null,
339
+ trimmed: trimmedLabel(omitted, unreachable),
231
340
  total_nodes: nodes.length,
232
341
  total_edges: edges.length,
342
+ // Seeds are retained unconditionally, so the seed set is a floor the
343
+ // reduction cannot go below — report the outcome instead of hiding a miss (#2738)
344
+ budget_met: estimate <= budgetTokens,
345
+ budget_estimate: estimate,
233
346
  };
234
347
  }
235
348
  // ─── Public API ──────────────────────────────────────────────────────────────
@@ -303,17 +416,19 @@ function graphifyQuery(cwd, term, options = {}) {
303
416
  return { error: 'Failed to parse graph.json' };
304
417
  }
305
418
  let result = seedAndExpand(graph, term);
306
- if (options.budget) {
307
- result = applyBudget(result, options.budget);
419
+ if (options.budget != null) {
420
+ result = applyBudget(result, options.budget, term);
308
421
  }
309
- return {
310
- term,
422
+ // Same builder the estimator measured, so budget_estimate describes exactly
423
+ // these bytes (#2738).
424
+ return buildQueryResponse(term, {
311
425
  nodes: result.nodes,
312
426
  edges: result.edges,
313
- total_nodes: result.nodes.length,
314
- total_edges: result.edges.length,
315
427
  trimmed: 'trimmed' in result ? (result.trimmed || null) : null,
316
- };
428
+ budget: 'budget_met' in result
429
+ ? { met: result.budget_met, estimate: result.budget_estimate }
430
+ : undefined,
431
+ });
317
432
  }
318
433
  /**
319
434
  * Return status information about the knowledge graph (STAT-01, STAT-02).
@@ -27,9 +27,15 @@ const node_path_1 = __importDefault(require("node:path"));
27
27
  const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
28
28
  const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
29
29
  const clock_cjs_1 = require("./clock.cjs");
30
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- core-utils.cjs is an export= CommonJS module
31
+ const coreUtilsMod = require("./core-utils.cjs");
30
32
  // eslint-disable-next-line @typescript-eslint/no-require-imports
31
33
  const ioMod = require("./io.cjs");
34
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- frontmatter.cjs is an export= CommonJS module
35
+ const frontmatterMod = require("./frontmatter.cjs");
32
36
  const { output } = ioMod;
37
+ const { transliterateForSlug } = coreUtilsMod;
38
+ const { stripFrontmatter } = frontmatterMod;
33
39
  // ─── Utilities ──────────────────────────────────────────────────────────────
34
40
  function readOptional(filePath) {
35
41
  try {
@@ -43,7 +49,11 @@ function zeroPad(n, width = 2) {
43
49
  return String(n).padStart(width, '0');
44
50
  }
45
51
  function slugify(title) {
46
- return title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
52
+ // #2848: transliterate Cyrillic to ASCII before the filter so a non-Latin
53
+ // title does not collapse to an empty slug. The shared primitive keeps this
54
+ // in sync with generateSlugInternal. slugify's DISTINCT contract is preserved:
55
+ // single leading/trailing hyphen strip (/^-|-$/), and NO 60-char truncation.
56
+ return transliterateForSlug(title).replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
47
57
  }
48
58
  // ─── GSD-2 Parser ───────────────────────────────────────────────────────────
49
59
  /**
@@ -218,9 +228,22 @@ function buildPlanMd(task, phasePrefix, planPrefix, phaseSlug, milestoneTitle) {
218
228
  */
219
229
  function buildSummaryMd(task, phasePrefix, planPrefix) {
220
230
  const raw = task.summary || '';
221
- // Strip GSD-2 frontmatter block (--- ... ---) if present
222
- const bodyMatch = raw.match(/^---[\s\S]*?---\n+([\s\S]*)$/);
223
- const body = bodyMatch ? bodyMatch[1].trim() : raw.trim();
231
+ // Strip the GSD-2 frontmatter block via the canonical primitive (#2703). The
232
+ // previous local regex required a bare `\n` after the closing `---`, so a
233
+ // CRLF-authored summary never matched, fell through to the untouched-raw
234
+ // branch, and had its frontmatter emitted a second time inside the body of
235
+ // the document this function then wrapped in a fresh v1 block.
236
+ //
237
+ // `extractFrontmatter` — which the issue names — returns only the parsed
238
+ // object and never the body, so it cannot serve this call site;
239
+ // `stripFrontmatter` is the same module's canonical body primitive.
240
+ //
241
+ // `once` is load-bearing. A GSD-2 summary is an arbitrary user-authored
242
+ // document, not a GSD artefact with a known frontmatter-doubling failure
243
+ // mode, so a body opening with a thematic-break-delimited section
244
+ // (`---` / heading / `---`) is far likelier than a corrupt second header —
245
+ // and the default greedy loop would delete it without a trace.
246
+ const body = stripFrontmatter(raw, { once: true }).trim();
224
247
  return [
225
248
  '---',
226
249
  `phase: "${phasePrefix}"`,
@@ -464,8 +464,20 @@ function negotiateHostCapabilities(host, engine = DEFAULT_ENGINE) {
464
464
  function shouldFlattenDispatch(dispatch) {
465
465
  if (!dispatch || typeof dispatch !== 'object')
466
466
  return true;
467
+ // Can background at all: both background flags must be explicitly true.
467
468
  const canBackground = dispatch.background === true && dispatch.backgroundDispatch === true;
468
- return !canBackground;
469
+ if (!canBackground)
470
+ return true;
471
+ // #2939: can background a NESTING orchestrator with room to delegate. A depth budget of 1
472
+ // is consumed by the backgrounded orchestrator itself; it needs > 1 (or unbounded, maxDepth < 0)
473
+ // to host a delegated leaf at depth 2. Non-finite/missing maxDepth fails closed (no budget →
474
+ // flatten), mirroring degradationFor's treatment of non-finite depth as 0.
475
+ const canNest = dispatch.nested === true && dispatch.subagentToolkit === 'full';
476
+ if (!canNest)
477
+ return true;
478
+ const depth = typeof dispatch.maxDepth === 'number' && Number.isFinite(dispatch.maxDepth) ? dispatch.maxDepth : 0;
479
+ const depthSufficient = depth < 0 || depth > 1;
480
+ return !depthSufficient;
469
481
  }
470
482
  // ---------------------------------------------------------------------------
471
483
  // resolveDispatchType — ADR-1239 / epic #2505 Phase 4 (Option A)
@@ -29,32 +29,107 @@ function routeInitCommand({ init, args, cwd, raw, error }) {
29
29
  error,
30
30
  unknownMessage: (_subcommand, available) => `Unknown init workflow: ${_subcommand}\nAvailable: ${available.join(', ')}`,
31
31
  handlers: {
32
+ // #2932/#2992: `parseNamedArgs` never yields `undefined` for an absent
33
+ // flag (value-flags default to `null`, booleanFlags default to `false`);
34
+ // `buildSectionManifestField`'s flags-Set builder (src/init.cts) is the
35
+ // single source of truth for flag ABSENCE and gates on value truthiness,
36
+ // so `namedArgs` is passed through here uncoerced.
32
37
  'execute-phase': () => {
33
- const namedArgs = (0, command_arg_projection_cjs_1.parseNamedArgs)(args, [], ['validate', 'tdd']);
34
- init.cmdInitExecutePhase(cwd, args[2], raw, { validate: namedArgs['validate'], tdd: namedArgs['tdd'] });
38
+ const namedArgs = (0, command_arg_projection_cjs_1.parseNamedArgs)(args, [], ['validate', 'tdd', 'wave']);
39
+ init.cmdInitExecutePhase(cwd, args[2], raw, {
40
+ validate: namedArgs['validate'],
41
+ tdd: namedArgs['tdd'],
42
+ wave: namedArgs['wave'],
43
+ });
35
44
  },
36
45
  'plan-phase': () => {
37
- const namedArgs = (0, command_arg_projection_cjs_1.parseNamedArgs)(args, ['granularity'], ['validate', 'tdd']);
38
- init.cmdInitPlanPhase(cwd, args[2], raw, { validate: namedArgs['validate'], tdd: namedArgs['tdd'], granularity: namedArgs['granularity'] });
46
+ const namedArgs = (0, command_arg_projection_cjs_1.parseNamedArgs)(args, ['granularity', 'prd', 'ingest', 'research-phase'], ['validate', 'tdd', 'reviews', 'chunked']);
47
+ init.cmdInitPlanPhase(cwd, args[2], raw, {
48
+ validate: namedArgs['validate'],
49
+ tdd: namedArgs['tdd'],
50
+ granularity: namedArgs['granularity'],
51
+ prd: namedArgs['prd'],
52
+ ingest: namedArgs['ingest'],
53
+ 'research-phase': namedArgs['research-phase'],
54
+ reviews: namedArgs['reviews'],
55
+ chunked: namedArgs['chunked'],
56
+ });
57
+ },
58
+ 'new-project': () => {
59
+ const namedArgs = (0, command_arg_projection_cjs_1.parseNamedArgs)(args, [], ['auto']);
60
+ init.cmdInitNewProject(cwd, raw, { auto: namedArgs['auto'] });
61
+ },
62
+ 'new-milestone': () => {
63
+ const namedArgs = (0, command_arg_projection_cjs_1.parseNamedArgs)(args, [], ['reset-phase-numbers']);
64
+ init.cmdInitNewMilestone(cwd, raw, {
65
+ 'reset-phase-numbers': namedArgs['reset-phase-numbers'],
66
+ });
39
67
  },
40
- 'new-project': () => init.cmdInitNewProject(cwd, raw),
41
- 'new-milestone': () => init.cmdInitNewMilestone(cwd, raw),
42
68
  onboard: () => {
43
69
  const namedArgs = (0, command_arg_projection_cjs_1.parseNamedArgs)(args, [], ['fast', 'text']);
44
70
  init.cmdInitOnboard(cwd, raw, { fast: namedArgs['fast'], text: namedArgs['text'] });
45
71
  },
46
- quick: () => init.cmdInitQuick(cwd, args.slice(2).join(' '), raw),
72
+ quick: () => {
73
+ const namedArgs = (0, command_arg_projection_cjs_1.parseNamedArgs)(args, [], ['discuss', 'research', 'validate', 'full']);
74
+ // #2994: `args.slice(2)` is the free-text description, but section-manifest
75
+ // gating (buildSectionManifestField, src/init.cts) now requires forwarding
76
+ // --discuss/--research/--validate/--full alongside it — a plain `.join(' ')`
77
+ // would otherwise fold those recognized flag tokens straight into the
78
+ // description text. Strip them before joining so the description stays
79
+ // exactly what it was before this workflow started forwarding flags.
80
+ const quickFlagTokens = new Set(['--discuss', '--research', '--validate', '--full']);
81
+ const description = args
82
+ .slice(2)
83
+ .filter((token) => !quickFlagTokens.has(token))
84
+ .join(' ');
85
+ init.cmdInitQuick(cwd, description, raw, {
86
+ discuss: namedArgs['discuss'],
87
+ research: namedArgs['research'],
88
+ validate: namedArgs['validate'],
89
+ full: namedArgs['full'],
90
+ });
91
+ },
47
92
  'ingest-docs': () => init.cmdInitIngestDocs(cwd, raw),
48
93
  resume: () => init.cmdInitResume(cwd, raw),
49
94
  'verify-work': () => init.cmdInitVerifyWork(cwd, args[2], raw),
50
95
  'phase-op': () => init.cmdInitPhaseOp(cwd, args[2], raw),
96
+ 'code-review': () => {
97
+ const namedArgs = (0, command_arg_projection_cjs_1.parseNamedArgs)(args, [], ['fix']);
98
+ init.cmdInitCodeReview(cwd, args[2], raw, { fix: namedArgs['fix'] });
99
+ },
100
+ review: () => init.cmdInitReview(cwd, args[2], raw, {}),
101
+ 'discuss-phase-assumptions': () => {
102
+ const namedArgs = (0, command_arg_projection_cjs_1.parseNamedArgs)(args, [], ['auto']);
103
+ init.cmdInitDiscussPhaseAssumptions(cwd, args[2], raw, { auto: namedArgs['auto'] });
104
+ },
51
105
  todos: () => init.cmdInitTodos(cwd, args[2], raw),
52
106
  'milestone-op': () => init.cmdInitMilestoneOp(cwd, raw),
53
107
  'map-codebase': () => init.cmdInitMapCodebase(cwd, raw),
54
- progress: () => init.cmdInitProgress(cwd, raw),
108
+ progress: () => {
109
+ const namedArgs = (0, command_arg_projection_cjs_1.parseNamedArgs)(args, [], ['forensic']);
110
+ init.cmdInitProgress(cwd, raw, { forensic: namedArgs['forensic'] });
111
+ },
55
112
  // Keep manager on CJS for now so runtime-specific command rendering
56
113
  // (e.g. $gsd-* for codex) stays consistent with runtime-slash helpers.
57
114
  manager: () => init.cmdInitManager(cwd, raw),
115
+ 'complete-milestone': () => init.cmdInitCompleteMilestone(cwd, raw),
116
+ autonomous: () => {
117
+ const namedArgs = (0, command_arg_projection_cjs_1.parseNamedArgs)(args, [], ['converge', 'cross-ai']);
118
+ init.cmdInitAutonomous(cwd, raw, {
119
+ converge: namedArgs['converge'],
120
+ 'cross-ai': namedArgs['cross-ai'],
121
+ });
122
+ },
123
+ 'docs-update': () => init.cmdInitDocsUpdate(cwd, raw, {}),
124
+ update: () => {
125
+ const namedArgs = (0, command_arg_projection_cjs_1.parseNamedArgs)(args, [], ['next', 'rc']);
126
+ init.cmdInitUpdate(cwd, raw, { next: namedArgs['next'], rc: namedArgs['rc'] });
127
+ },
128
+ transition: () => init.cmdInitTransition(cwd, raw, {}),
129
+ debug: () => {
130
+ const namedArgs = (0, command_arg_projection_cjs_1.parseNamedArgs)(args, [], ['diagnose']);
131
+ init.cmdInitDebug(cwd, raw, { diagnose: namedArgs['diagnose'] });
132
+ },
58
133
  'new-workspace': () => init.cmdInitNewWorkspace(cwd, raw),
59
134
  'list-workspaces': () => init.cmdInitListWorkspaces(cwd, raw),
60
135
  'remove-workspace': () => init.cmdInitRemoveWorkspace(cwd, args[2], raw),