@opengsd/gsd-core 1.19.0 → 1.20.1

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 (131) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/agents/gsd-code-fixer.compact.md +5 -5
  4. package/agents/gsd-code-fixer.md +9 -4
  5. package/agents/gsd-code-reviewer.compact.md +49 -8
  6. package/agents/gsd-code-reviewer.md +53 -14
  7. package/agents/gsd-executor.md +8 -4
  8. package/bin/install.js +36 -40
  9. package/commands/gsd/new-milestone.md +2 -2
  10. package/commands/gsd/new-project.md +2 -2
  11. package/commands/gsd/update.md +5 -2
  12. package/gsd-core/bin/gsd-tools.cjs +177 -93
  13. package/gsd-core/bin/lib/capability-lifecycle.cjs +1 -12
  14. package/gsd-core/bin/lib/capability-registry.cjs +284 -93
  15. package/gsd-core/bin/lib/capability-validator.cjs +15 -0
  16. package/gsd-core/bin/lib/commands.cjs +46 -17
  17. package/gsd-core/bin/lib/config-loader.cjs +3 -3
  18. package/gsd-core/bin/lib/core-utils.cjs +11 -0
  19. package/gsd-core/bin/lib/frontmatter.cjs +21 -3
  20. package/gsd-core/bin/lib/gate-api-coverage-verify-pre.cjs +2 -2
  21. package/gsd-core/bin/lib/gate-context-drift.cjs +1 -1
  22. package/gsd-core/bin/lib/gate-phase-context.cjs +7 -8
  23. package/gsd-core/bin/lib/gate-schema-drift.cjs +1 -1
  24. package/gsd-core/bin/lib/git-info-exclude.cjs +1 -6
  25. package/gsd-core/bin/lib/gsd-tool-dir.cjs +134 -0
  26. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +13 -9
  27. package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +9 -7
  28. package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +3 -3
  29. package/gsd-core/bin/lib/init.cjs +181 -68
  30. package/gsd-core/bin/lib/install-engine.cjs +9 -9
  31. package/gsd-core/bin/lib/install-manifest-record.cjs +86 -0
  32. package/gsd-core/bin/lib/install-profiles.cjs +4 -2
  33. package/gsd-core/bin/lib/io.cjs +6 -0
  34. package/gsd-core/bin/lib/markdown-sectionizer.cjs +34 -1
  35. package/gsd-core/bin/lib/markdown-table.cjs +23 -0
  36. package/gsd-core/bin/lib/mcp-server.cjs +8 -5
  37. package/gsd-core/bin/lib/milestone.cjs +18 -50
  38. package/gsd-core/bin/lib/phase-entry-grammar.cjs +119 -0
  39. package/gsd-core/bin/lib/phase-id-display.cjs +2 -0
  40. package/gsd-core/bin/lib/phase-id.cjs +627 -36
  41. package/gsd-core/bin/lib/phase-lifecycle.cjs +9 -74
  42. package/gsd-core/bin/lib/phase-locator.cjs +328 -19
  43. package/gsd-core/bin/lib/phase.cjs +3039 -666
  44. package/gsd-core/bin/lib/planning-document.cjs +26 -5
  45. package/gsd-core/bin/lib/planning-inspect.cjs +27 -35
  46. package/gsd-core/bin/lib/planning-snapshot.cjs +12 -7
  47. package/gsd-core/bin/lib/planning-workspace.cjs +3 -3
  48. package/gsd-core/bin/lib/read-only-git-env.cjs +23 -2
  49. package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +2 -2
  50. package/gsd-core/bin/lib/review-lane-descriptor.cjs +22 -0
  51. package/gsd-core/bin/lib/review-lane-invocation.cjs +125 -18
  52. package/gsd-core/bin/lib/reviewer-step-dispatch.cjs +11 -9
  53. package/gsd-core/bin/lib/roadmap-parser.cjs +759 -144
  54. package/gsd-core/bin/lib/roadmap-progress.cjs +136 -0
  55. package/gsd-core/bin/lib/roadmap-upgrade.cjs +8 -5
  56. package/gsd-core/bin/lib/roadmap.cjs +358 -106
  57. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +310 -130
  58. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +6 -0
  59. package/gsd-core/bin/lib/shell-command-projection.cjs +36 -9
  60. package/gsd-core/bin/lib/smart-entry.cjs +22 -11
  61. package/gsd-core/bin/lib/state-contract.cjs +11 -7
  62. package/gsd-core/bin/lib/state-document.cjs +332 -23
  63. package/gsd-core/bin/lib/state-transition.cjs +51 -15
  64. package/gsd-core/bin/lib/state.cjs +499 -180
  65. package/gsd-core/bin/lib/template.cjs +2 -2
  66. package/gsd-core/bin/lib/validate.cjs +3 -2
  67. package/gsd-core/bin/lib/vendor/re2js.cjs +863 -577
  68. package/gsd-core/bin/lib/vendor/re2js.d.cts +284 -219
  69. package/gsd-core/bin/lib/verify.cjs +2 -2
  70. package/gsd-core/bin/lib/workstream-inventory.cjs +27 -21
  71. package/gsd-core/bin/lib/worktree-base-ref.cjs +32 -2
  72. package/gsd-core/bin/verify-reapply-patches.cjs +411 -10
  73. package/gsd-core/references/cicd-review-patterns.md +45 -0
  74. package/gsd-core/references/necessity-review.md +48 -0
  75. package/gsd-core/workflows/code-review-fix.md +46 -5
  76. package/gsd-core/workflows/code-review.md +3 -0
  77. package/gsd-core/workflows/execute-phase.md +1 -1
  78. package/gsd-core/workflows/execute-plan.md +1 -1
  79. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +13 -9
  80. package/gsd-core/workflows/plan-phase.md +1 -1
  81. package/gsd-core/workflows/pr-branch.md +2 -0
  82. package/gsd-core/workflows/reapply-patches.md +104 -0
  83. package/gsd-core/workflows/review.md +54 -141
  84. package/gsd-core/workflows/secure-phase.md +1 -1
  85. package/gsd-core/workflows/settings-advanced.md +57 -9
  86. package/gsd-core/workflows/ship.md +12 -0
  87. package/hooks/dist/gsd-agent-isolation-guard.js +1 -1
  88. package/hooks/dist/gsd-cursor-subagent-start.js +1 -1
  89. package/hooks/dist/gsd-graphify-update.sh +6 -0
  90. package/hooks/dist/gsd-statusline.js +3 -2
  91. package/hooks/dist/gsd-windsurf-pre-write.js +4 -4
  92. package/hooks/dist/gsd-workflow-guard.js +2 -1
  93. package/hooks/dist/gsd-worktree-path-guard.js +4 -4
  94. package/hooks/dist/lib/git-probe.js +16 -0
  95. package/hooks/dist/lib/isolation-sentinel.js +77 -2
  96. package/hooks/gsd-agent-isolation-guard.js +1 -1
  97. package/hooks/gsd-cursor-subagent-start.js +1 -1
  98. package/hooks/gsd-graphify-update.sh +6 -0
  99. package/hooks/gsd-statusline.js +3 -2
  100. package/hooks/gsd-windsurf-pre-write.js +4 -4
  101. package/hooks/gsd-workflow-guard.js +2 -1
  102. package/hooks/gsd-worktree-path-guard.js +4 -4
  103. package/hooks/lib/git-probe.js +16 -0
  104. package/hooks/lib/isolation-sentinel.js +77 -2
  105. package/package.json +3 -3
  106. package/pi/package-agents/gsd-code-fixer.md +9 -4
  107. package/pi/package-agents/gsd-code-reviewer.md +53 -14
  108. package/pi/package-agents/gsd-executor.md +8 -4
  109. package/pi/package-agents/manifest.json +3 -3
  110. package/pi/package-skills/gsd-new-milestone/SKILL.md +1 -1
  111. package/pi/package-skills/gsd-new-project/SKILL.md +1 -1
  112. package/pi/package-skills/gsd-update/SKILL.md +4 -1
  113. package/scripts/changeset/lint.cjs +28 -3
  114. package/scripts/ci-next-health.cjs +246 -23
  115. package/scripts/docs-guard-registry.cjs +2 -0
  116. package/scripts/gen-features.cjs +14 -2
  117. package/scripts/lib/drift-scan.cjs +27 -0
  118. package/scripts/lib/macos-conformance-tier.generated.cjs +10 -0
  119. package/scripts/lib/platform-conformance-tier.generated.cjs +15 -0
  120. package/scripts/lint-phase-enumeration-drift.cjs +8 -4
  121. package/scripts/lint-roadmap-progress-drift.cjs +421 -0
  122. package/scripts/merge-commit-pr.cjs +186 -0
  123. package/scripts/mutation-matrix.cjs +23 -1
  124. package/scripts/release-ancestry.cjs +86 -0
  125. package/scripts/release-pipeline-pr.cjs +92 -0
  126. package/scripts/require-issue-link-policy.cjs +18 -9
  127. package/scripts/sync-next-version.cjs +48 -7
  128. package/skills/gsd-new-milestone/SKILL.md +1 -1
  129. package/skills/gsd-new-project/SKILL.md +1 -1
  130. package/skills/gsd-update/SKILL.md +5 -2
  131. package/vscode/package.json +1 -1
@@ -23,7 +23,7 @@
23
23
  * third-party overlay manifests (ADR-2782 D4).
24
24
  */
25
25
  Object.defineProperty(exports, "__esModule", { value: true });
26
- exports.LANE_UNAVAILABLE = void 0;
26
+ exports.CONTEXT_WINDOW_HEADROOM = exports.LANE_UNAVAILABLE = void 0;
27
27
  exports.configString = configString;
28
28
  exports.normalizeHost = normalizeHost;
29
29
  exports.resolveLaneEffort = resolveLaneEffort;
@@ -33,7 +33,10 @@ exports.isEmptyReview = isEmptyReview;
33
33
  exports.fileRefPrompt = fileRefPrompt;
34
34
  exports.applyPromptOverride = applyPromptOverride;
35
35
  exports.artifactPaths = artifactPaths;
36
- exports.resolveLaneBudget = resolveLaneBudget;
36
+ exports.resolveLaneCap = resolveLaneCap;
37
+ exports.checkPromptAgainstCap = checkPromptAgainstCap;
38
+ exports.refuseIfOverCap = refuseIfOverCap;
39
+ exports.renderPromptRefusalStub = renderPromptRefusalStub;
37
40
  exports.resolveLanePlan = resolveLanePlan;
38
41
  const review_lane_descriptor_cjs_1 = require("./review-lane-descriptor.cjs");
39
42
  /* ------------------------------------------------------------------ *
@@ -60,8 +63,12 @@ exports.LANE_UNAVAILABLE = Object.freeze({
60
63
  PROBE_TIMEOUT: 'probe_timeout',
61
64
  HOST_UNREACHABLE: 'host_unreachable',
62
65
  EGRESS_HOST_CHANGED: 'egress_host_changed',
63
- BUDGET_TOO_SMALL: 'budget_too_small',
64
- BUDGET_TOOL_FAILED: 'budget_tool_failed',
66
+ /**
67
+ * #4559: the prompt a lane would receive is larger than its prompt cap (an explicit cap, or the
68
+ * default derived from the lane's context window). The lane is REFUSED — never trimmed, because a
69
+ * silently truncated plan set is a review of something other than the plans.
70
+ */
71
+ PROMPT_EXCEEDS_CONTEXT: 'prompt_exceeds_context',
65
72
  });
66
73
  /** Every handler name the runner can dispatch. Mirrors `LaneHandler` (D6's closed enum). */
67
74
  const KNOWN_HANDLERS = new Set(['antigravity', 'openai-compatible', 'opencode']);
@@ -233,7 +240,8 @@ function fileRefPrompt(promptPath, repoRoot) {
233
240
  /**
234
241
  * Re-point a resolved plan at a different prompt file (`review-lane invoke --prompt-file`, #5049).
235
242
  *
236
- * The override carries a budget-trimmed prompt, so it has to land wherever THIS lane actually
243
+ * The override carries a caller-substituted prompt (a section subset, say; nothing trims a prompt
244
+ * to fit a lane since #4559), so it has to land wherever THIS lane actually
237
245
  * reads its prompt, and that differs by channel:
238
246
  * - `stdin`: the runner streams the file `plan.stdin` names.
239
247
  * - `argv-file-ref`: the prompt PATH is embedded in a sentence in argv (`fileRefPrompt`), already
@@ -279,25 +287,124 @@ function artifactPaths(runDir, slug) {
279
287
  };
280
288
  }
281
289
  /**
282
- * Per-lane prompt budget (#2797 semantics, preserved exactly).
290
+ * Fraction of a lane's declared context window a prompt may occupy (#4559). The remaining 35%
291
+ * absorbs the system prompt, the reply, and `estimateTokens` being a chars/4 estimate rather than
292
+ * a tokenizer (code tokenizes denser than prose).
293
+ */
294
+ exports.CONTEXT_WINDOW_HEADROOM = 0.65;
295
+ /**
296
+ * Per-lane prompt cap (#2797 semantics, extended by #4559).
283
297
  *
284
- * `-1` is the UNSET sentinel and falls back to the central `review.max_prompt_tokens`, because
285
- * `0` is a legitimate value meaning "do not trim this lane". Treating 0 as unset would silently
286
- * switch a user who deliberately disabled trimming onto the global budget.
298
+ * Precedence: per-lane `promptBudgetKey` → global `review.max_prompt_tokens` → the default derived
299
+ * from the lane's context window (`floor(window * CONTEXT_WINDOW_HEADROOM)`). `-1` is the UNSET
300
+ * sentinel for the per-lane key, because `0` is a legitimate value meaning "no cap for this lane".
301
+ * An explicit `0` at either explicit level also turns the derived default off.
302
+ *
303
+ * The window is the operator's `contextWindowConfigKey` value when that is a positive finite number
304
+ * (`0` disables the derived cap), else the lane's declared `contextWindowTokens`. Both are narrowed
305
+ * from `unknown` — overlay lanes and config values cross a trust boundary — and anything that is not
306
+ * a positive finite number narrows to "unset".
287
307
  *
288
308
  * Single source of truth: `gsd-core/bin/gsd-tools.cjs`'s `review-lane plan`/`invoke` and
289
- * `src/reviewer-step-dispatch.cts`'s `dispatchReviewerLanes` both resolve a lane's budget through
309
+ * `src/reviewer-step-dispatch.cts`'s `dispatchReviewerLanes` both resolve a lane's cap through
290
310
  * this function rather than each carrying their own copy (#4209 R3 — two verbatim copies drift).
291
311
  */
292
- function resolveLaneBudget(lane, configGet) {
293
- if (!lane.promptBudgetKey)
294
- return null;
295
- const per = configGet(lane.promptBudgetKey);
312
+ function resolveLaneCap(lane, configGet) {
313
+ // A spawn lane whose prompt channel is `none` (coderabbit) is fed no prompt, so there is nothing
314
+ // to cap: no key — explicit, global or derived — may ever refuse it (#4559, design row 18).
315
+ const laneRaw = lane;
316
+ if (laneRaw?.transport === 'spawn' && laneRaw.invoke?.promptChannel === 'none') {
317
+ return { cap: null, source: 'none', contextWindowTokens: null };
318
+ }
296
319
  const isNum = (v) => typeof v === 'number' && Number.isFinite(v);
297
- if (isNum(per) && per !== -1)
298
- return per;
299
- const global = configGet('review.max_prompt_tokens');
300
- return isNum(global) ? global : null;
320
+ const isPositive = (v) => isNum(v) && v > 0;
321
+ const raw = lane;
322
+ const budgetKey = typeof raw?.promptBudgetKey === 'string' && raw.promptBudgetKey ? raw.promptBudgetKey : null;
323
+ if (budgetKey) {
324
+ const per = configGet(budgetKey);
325
+ if (isNum(per) && per !== -1)
326
+ return { cap: per, source: 'lane', contextWindowTokens: null };
327
+ const global = configGet('review.max_prompt_tokens');
328
+ if (isNum(global))
329
+ return { cap: global, source: 'global', contextWindowTokens: null };
330
+ }
331
+ const windowKey = typeof raw?.contextWindowConfigKey === 'string' && raw.contextWindowConfigKey ? raw.contextWindowConfigKey : null;
332
+ const override = windowKey ? configGet(windowKey) : undefined;
333
+ if (override === 0)
334
+ return { cap: null, source: 'none', contextWindowTokens: null };
335
+ const windowTokens = isPositive(override)
336
+ ? override
337
+ : isPositive(raw?.contextWindowTokens)
338
+ ? raw.contextWindowTokens
339
+ : null;
340
+ if (windowTokens === null)
341
+ return { cap: null, source: 'none', contextWindowTokens: null };
342
+ return {
343
+ cap: Math.floor(windowTokens * exports.CONTEXT_WINDOW_HEADROOM),
344
+ source: 'context-window',
345
+ contextWindowTokens: windowTokens,
346
+ };
347
+ }
348
+ /**
349
+ * The ONE refuse-or-run decision (#4559), shared by `review-lane invoke` and `dispatchReviewerLanes`.
350
+ * Refuses only when a positive cap is STRICTLY exceeded: a prompt exactly at the cap runs, and a
351
+ * cap of `0` or `null` never refuses. `detail` names the estimate, the cap, where the cap came from
352
+ * and the key to change, so the refusal is actionable rather than a bare "too big".
353
+ */
354
+ function checkPromptAgainstCap(estimatedTokens, capInfo, lane) {
355
+ const { cap, source, contextWindowTokens } = capInfo;
356
+ if (cap === null || !(cap > 0) || !(estimatedTokens > cap))
357
+ return { ok: true };
358
+ const raw = lane;
359
+ const slug = typeof raw?.slug === 'string' ? raw.slug : '';
360
+ const budgetKey = typeof raw?.promptBudgetKey === 'string' && raw.promptBudgetKey ? raw.promptBudgetKey : null;
361
+ const windowKey = typeof raw?.contextWindowConfigKey === 'string' && raw.contextWindowConfigKey ? raw.contextWindowConfigKey : null;
362
+ // Remedy keys are the lane's DECLARED keys (an overlay lane's differ from the slug convention).
363
+ const remedy = source === 'lane'
364
+ ? `raise ${budgetKey ?? 'the lane prompt cap'} or set it to 0 to remove the cap`
365
+ : source === 'global'
366
+ ? `raise review.max_prompt_tokens${budgetKey ? ` or set ${budgetKey}` : ''}`
367
+ : windowKey
368
+ ? `raise ${windowKey} if this lane's model has a larger window, or set it to 0 to remove the cap`
369
+ : budgetKey
370
+ ? `this lane's context window is fixed by its declaration; set review.max_prompt_tokens or ${budgetKey} to override`
371
+ : `this lane's cap comes from its declared contextWindowTokens and the lane declares no key to override it`;
372
+ const windowNote = contextWindowTokens !== null ? `, ${contextWindowTokens}-token context window` : '';
373
+ return {
374
+ ok: false,
375
+ reason: exports.LANE_UNAVAILABLE.PROMPT_EXCEEDS_CONTEXT,
376
+ detail: `estimated ${estimatedTokens} tokens exceeds the ${cap}-token cap for lane '${slug}' ` +
377
+ `(source: ${source}${windowNote}); ${remedy}`,
378
+ };
379
+ }
380
+ /**
381
+ * Resolve the lane's cap and decide refuse-or-run in one pure step (#4559): `null` = run, otherwise
382
+ * the structured refusal. `review-lane invoke` and `dispatchReviewerLanes` both call this, so the
383
+ * two surfaces cannot disagree about whether, or how, a lane is refused.
384
+ */
385
+ function refuseIfOverCap(input) {
386
+ const capInfo = resolveLaneCap(input.lane, input.configGet);
387
+ const verdict = checkPromptAgainstCap(input.estimatedTokens, capInfo, input.lane);
388
+ if (verdict.ok)
389
+ return null;
390
+ return {
391
+ ok: false,
392
+ reason: verdict.reason,
393
+ detail: verdict.detail,
394
+ refusal: {
395
+ estimatedTokens: input.estimatedTokens,
396
+ cap: capInfo.cap,
397
+ capSource: capInfo.source,
398
+ contextWindowTokens: capInfo.contextWindowTokens,
399
+ },
400
+ };
401
+ }
402
+ /**
403
+ * The text written to a refused lane's review file (#4559). The marker
404
+ * `review did not run: prompt exceeds` is stable: `review.md`'s skip gate and coverage loop key on it.
405
+ */
406
+ function renderPromptRefusalStub(slug, detail) {
407
+ return `${slug} review did not run: prompt exceeds this lane's context cap (${detail})\n`;
301
408
  }
302
409
  /* ------------------------------------------------------------------ *
303
410
  * Resolution
@@ -29,8 +29,9 @@
29
29
  *
30
30
  * The bounded source-review prompt built here is METADATA ONLY — repository root, canonical
31
31
  * file paths, review depth, base SHA, and four fixed prohibitions. It never embeds file
32
- * contents. If the assembled prompt exceeds a lane's resolved budget, that lane's dispatch
33
- * hard-fails before `invoke` runs for it — no silent truncation of the file list.
32
+ * contents. If the assembled prompt exceeds a lane's resolved prompt cap (an explicit cap or the
33
+ * default derived from its context window, #4559), that lane's dispatch hard-fails with reason
34
+ * `prompt_exceeds_context` before `invoke` runs for it — no silent truncation of the file list.
34
35
  */
35
36
  var __importDefault = (this && this.__importDefault) || function (mod) {
36
37
  return (mod && mod.__esModule) ? mod : { "default": mod };
@@ -125,9 +126,8 @@ function validatePaths(repoRoot, paths) {
125
126
  }
126
127
  return { ok: true };
127
128
  }
128
- // `resolveLaneBudget` (review-lane-invocation.cjs) resolves the number; `null` and a resolved
129
- // `0` both mean unbounded (#2797) — the caller's overflow check must test both `!== null` and
130
- // `!== 0`. See the call site below.
129
+ // `resolveLaneCap` (review-lane-invocation.cjs) resolves the cap; `null` and a resolved `0` both
130
+ // mean unbounded (#2797), and `checkPromptAgainstCap` owns that test. See the call site below.
131
131
  /**
132
132
  * One-line depth definition for an external reviewer lane, condensed from `<depth_levels>` in
133
133
  * `agents/gsd-code-reviewer.md` (#4209 review: a bare `quick`/`standard`/`deep` label means
@@ -296,13 +296,15 @@ async function dispatchReviewerLanes(input, deps) {
296
296
  anyFailed = true;
297
297
  continue;
298
298
  }
299
- const budget = (0, review_lane_invocation_cjs_1.resolveLaneBudget)(lane, configGet);
300
- if (budget !== null && budget !== 0 && estimatedTokens > budget) {
299
+ // #4559: refuse-not-trim, through the same check `review-lane invoke` uses.
300
+ const refused = (0, review_lane_invocation_cjs_1.refuseIfOverCap)({ lane, estimatedTokens, configGet });
301
+ if (refused) {
301
302
  results.push({
302
303
  slug,
303
304
  ok: false,
304
- reason: 'budget_exceeded',
305
- detail: `estimated ${estimatedTokens} tokens exceeds resolved budget ${budget} for lane '${slug}'`,
305
+ reason: refused.reason,
306
+ detail: refused.detail,
307
+ refusal: refused.refusal,
306
308
  });
307
309
  anyFailed = true;
308
310
  continue;