@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +2 -3
- package/.opencode/plugins/gsd-core.js +8 -1
- package/agents/gsd-code-fixer.md +131 -34
- package/agents/gsd-debugger.md +12 -246
- package/agents/gsd-executor.md +7 -5
- package/agents/gsd-integration-checker.md +3 -0
- package/agents/gsd-plan-checker.md +9 -0
- package/agents/gsd-planner.md +5 -8
- package/agents/gsd-roadmapper.md +21 -3
- package/agents/gsd-verifier.md +14 -70
- package/bin/install.js +503 -341
- package/commands/gsd/mempalace-capture.md +1 -1
- package/commands/gsd/new-milestone.md +1 -1
- package/commands/gsd/plan-phase.md +1 -1
- package/gsd-core/bin/gsd-tools.cjs +607 -63
- package/gsd-core/bin/lib/active-workstream-store.cjs +25 -0
- package/gsd-core/bin/lib/agent-install-check.cjs +38 -6
- package/gsd-core/bin/lib/api-coverage.cjs +120 -0
- package/gsd-core/bin/lib/audit.cjs +89 -1
- package/gsd-core/bin/lib/broken-windows.cjs +36 -6
- package/gsd-core/bin/lib/capability-registry.cjs +96 -110
- package/gsd-core/bin/lib/capability-validator.cjs +12 -2
- package/gsd-core/bin/lib/check-command-router.cjs +43 -1
- package/gsd-core/bin/lib/command-aliases.cjs +72 -0
- package/gsd-core/bin/lib/commands.cjs +26 -25
- package/gsd-core/bin/lib/commonjs-marker.cjs +136 -0
- package/gsd-core/bin/lib/config-loader.cjs +1 -0
- package/gsd-core/bin/lib/config.cjs +12 -1
- package/gsd-core/bin/lib/context-composer.cjs +278 -0
- package/gsd-core/bin/lib/context-predicates.cjs +506 -0
- package/gsd-core/bin/lib/core-utils.cjs +91 -12
- package/gsd-core/bin/lib/docs.cjs +3 -2
- package/gsd-core/bin/lib/external-job.cjs +19 -4
- package/gsd-core/bin/lib/frontmatter.cjs +84 -12
- package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +57 -6
- package/gsd-core/bin/lib/git-base-branch.cjs +58 -15
- package/gsd-core/bin/lib/graphify.cjs +142 -27
- package/gsd-core/bin/lib/gsd2-import.cjs +27 -4
- package/gsd-core/bin/lib/host-integration.cjs +13 -1
- package/gsd-core/bin/lib/init-command-router.cjs +83 -8
- package/gsd-core/bin/lib/init.cjs +1021 -57
- package/gsd-core/bin/lib/install-engine.cjs +64 -10
- package/gsd-core/bin/lib/install-profiles.cjs +27 -1
- package/gsd-core/bin/lib/installer-migration-authoring.cjs +3 -1
- package/gsd-core/bin/lib/installer-migration-report.cjs +4 -0
- package/gsd-core/bin/lib/installer-migrations/007-retire-config-root-commonjs-marker.cjs +149 -0
- package/gsd-core/bin/lib/installer-migrations/008-cursor-retire-commands-surface.cjs +55 -0
- package/gsd-core/bin/lib/installer-migrations/009-pi-retire-reserved-hooks-dir.cjs +199 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +87 -1
- package/gsd-core/bin/lib/io.cjs +28 -3
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +6 -0
- package/gsd-core/bin/lib/mcp-catalog.cjs +518 -0
- package/gsd-core/bin/lib/mcp-server.cjs +135 -3
- package/gsd-core/bin/lib/milestone.cjs +106 -51
- package/gsd-core/bin/lib/phase-id.cjs +63 -0
- package/gsd-core/bin/lib/phase-locator.cjs +138 -45
- package/gsd-core/bin/lib/phase.cjs +260 -25
- package/gsd-core/bin/lib/plan-dependency-graph.cjs +232 -0
- package/gsd-core/bin/lib/planning-workspace.cjs +4 -0
- package/gsd-core/bin/lib/project-root.cjs +48 -0
- package/gsd-core/bin/lib/prompt-budget.cjs +128 -165
- package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +80 -0
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +99 -0
- package/gsd-core/bin/lib/review-lane-runner.cjs +30 -6
- package/gsd-core/bin/lib/roadmap-command-router.cjs +42 -9
- package/gsd-core/bin/lib/roadmap-parser.cjs +100 -18
- package/gsd-core/bin/lib/roadmap.cjs +37 -7
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +195 -62
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +15 -3
- package/gsd-core/bin/lib/runtime-homes.cjs +154 -41
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +105 -41
- package/gsd-core/bin/lib/section-manifest.cjs +209 -0
- package/gsd-core/bin/lib/shell-command-projection.cjs +113 -27
- package/gsd-core/bin/lib/smart-entry.cjs +12 -0
- package/gsd-core/bin/lib/state-transition.cjs +73 -8
- package/gsd-core/bin/lib/state.cjs +151 -62
- package/gsd-core/bin/lib/surface.cjs +12 -1
- package/gsd-core/bin/lib/uat-predicate.cjs +11 -1
- package/gsd-core/bin/lib/uat.cjs +320 -21
- package/gsd-core/bin/lib/unusable-input.cjs +9 -0
- package/gsd-core/bin/lib/verification.cjs +29 -12
- package/gsd-core/bin/lib/verify.cjs +29 -5
- package/gsd-core/bin/lib/workflow-fragments.cjs +557 -0
- package/gsd-core/bin/lib/workstream-inventory-builder.cjs +181 -18
- package/gsd-core/bin/lib/workstream-inventory.cjs +519 -27
- package/gsd-core/bin/lib/workstream.cjs +6 -0
- package/gsd-core/bin/lib/worktree-base-ref.cjs +50 -6
- package/gsd-core/bin/lib/worktree-safety.cjs +276 -118
- package/gsd-core/bin/shared/config-schema.manifest.json +2 -0
- package/gsd-core/references/artifact-types.md +10 -3
- package/gsd-core/references/autonomous-ui-design-contract.md +42 -0
- package/gsd-core/references/debugger-techniques.md +255 -0
- package/gsd-core/references/research-documentation-lookup.md +5 -3
- package/gsd-core/references/specless-probe-fallback.md +7 -6
- package/gsd-core/references/verifier-wiring-patterns.md +100 -0
- package/gsd-core/references/worktree-branch-check.md +2 -2
- package/gsd-core/templates/summary-complex.md +2 -0
- package/gsd-core/templates/summary-minimal.md +2 -0
- package/gsd-core/templates/summary-standard.md +2 -0
- package/gsd-core/templates/summary.md +2 -0
- package/gsd-core/workflows/audit-milestone.md +3 -0
- package/gsd-core/workflows/autonomous/steps/converge-banner.md +1 -0
- package/gsd-core/workflows/autonomous/steps/converge-dispatch-bg.md +11 -0
- package/gsd-core/workflows/autonomous/steps/converge-dispatch-inline.md +7 -0
- package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +21 -0
- package/gsd-core/workflows/autonomous/steps/converge-loop.md +7 -0
- package/gsd-core/workflows/autonomous.md +32 -69
- package/gsd-core/workflows/code-review/steps/dispatch-fix.md +39 -0
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +83 -0
- package/gsd-core/workflows/code-review.md +42 -145
- package/gsd-core/workflows/complete-milestone/steps/git-tag.md +29 -0
- package/gsd-core/workflows/complete-milestone.md +23 -81
- package/gsd-core/workflows/debug.md +9 -12
- package/gsd-core/workflows/diagnose-issues.md +22 -0
- package/gsd-core/workflows/discovery-phase.md +4 -4
- package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +15 -0
- package/gsd-core/workflows/discuss-phase-assumptions.md +5 -16
- package/gsd-core/workflows/docs-update/steps/dispatch-monorepo-packages.md +51 -0
- package/gsd-core/workflows/docs-update.md +8 -51
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +34 -2
- package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +50 -0
- package/gsd-core/workflows/execute-phase/steps/partial-wave.md +31 -0
- package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +19 -0
- package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +42 -0
- package/gsd-core/workflows/execute-phase/steps/regression-gate.md +43 -37
- package/gsd-core/workflows/execute-phase.md +65 -137
- package/gsd-core/workflows/execute-plan.md +1 -1
- package/gsd-core/workflows/help/modes/full.md +6 -1
- package/gsd-core/workflows/ingest-docs.md +2 -1
- package/gsd-core/workflows/new-milestone/steps/project-md-milestone-write.md +16 -0
- package/gsd-core/workflows/new-milestone/steps/reset-phase-safety.md +19 -0
- package/gsd-core/workflows/new-milestone.md +21 -38
- package/gsd-core/workflows/new-project/steps/auto-mode-config.md +176 -0
- package/gsd-core/workflows/new-project/steps/auto-mode-detection.md +32 -0
- package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +18 -0
- package/gsd-core/workflows/new-project.md +13 -226
- package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +15 -0
- package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +110 -0
- package/gsd-core/workflows/plan-phase/steps/prd-express-gate.md +8 -0
- package/gsd-core/workflows/plan-phase/steps/research-only-early-exit.md +17 -0
- package/gsd-core/workflows/plan-phase/steps/research-only-modifiers.md +16 -0
- package/gsd-core/workflows/plan-phase/steps/reviews-prerequisite.md +17 -0
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +149 -0
- package/gsd-core/workflows/plan-phase.md +49 -193
- package/gsd-core/workflows/progress/steps/forensic-audit.md +125 -0
- package/gsd-core/workflows/progress/steps/mvp-display.md +18 -0
- package/gsd-core/workflows/progress.md +11 -153
- package/gsd-core/workflows/quick/steps/discussion-phase.md +124 -0
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +111 -0
- package/gsd-core/workflows/quick/steps/quick-verification.md +46 -0
- package/gsd-core/workflows/quick/steps/research-phase.md +72 -0
- package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +37 -0
- package/gsd-core/workflows/quick.md +20 -390
- package/gsd-core/workflows/resume-project.md +3 -0
- package/gsd-core/workflows/review/steps/reviewer-instances-note-1.md +4 -0
- package/gsd-core/workflows/review/steps/reviewer-instances-note-2.md +3 -0
- package/gsd-core/workflows/review.md +15 -8
- package/gsd-core/workflows/section-manifest.json +219 -0
- package/gsd-core/workflows/sketch.md +1 -1
- package/gsd-core/workflows/spec-phase.md +17 -14
- package/gsd-core/workflows/spike-wrap-up.md +20 -5
- package/gsd-core/workflows/spike.md +50 -16
- package/gsd-core/workflows/sync-skills.md +49 -11
- package/gsd-core/workflows/transition/steps/workstream-collision-check.md +17 -0
- package/gsd-core/workflows/transition.md +8 -21
- package/gsd-core/workflows/ui-phase.md +8 -7
- package/gsd-core/workflows/update/steps/channel-banner.md +7 -0
- package/gsd-core/workflows/update.md +18 -7
- package/gsd-core/workflows/verify-phase.md +4 -7
- package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +36 -0
- package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +21 -0
- package/gsd-core/workflows/verify-work.md +8 -58
- package/hooks/dist/gsd-agent-isolation-guard.js +428 -0
- package/hooks/dist/gsd-check-update-worker.js +14 -5
- package/hooks/dist/gsd-cursor-subagent-start.js +532 -26
- package/hooks/dist/gsd-read-injection-scanner.js +7 -0
- package/hooks/dist/gsd-statusline.js +72 -6
- package/hooks/dist/gsd-worktree-path-guard.js +2 -1
- package/hooks/dist/gsd-write-guard.js +359 -0
- package/hooks/dist/lib/isolation-sentinel.js +268 -0
- package/hooks/dist/managed-hooks-registry.cjs +2 -0
- package/hooks/gsd-agent-isolation-guard.js +428 -0
- package/hooks/gsd-check-update-worker.js +14 -5
- package/hooks/gsd-cursor-subagent-start.js +532 -26
- package/hooks/gsd-read-injection-scanner.js +7 -0
- package/hooks/gsd-statusline.js +72 -6
- package/hooks/gsd-worktree-path-guard.js +2 -1
- package/hooks/gsd-write-guard.js +359 -0
- package/hooks/hooks.json +12 -0
- package/hooks/lib/isolation-sentinel.js +268 -0
- package/hooks/managed-hooks-registry.cjs +2 -0
- package/package.json +14 -5
- package/pi/gsd.cjs +57 -12
- package/scripts/build-hooks.js +9 -0
- package/scripts/changeset/lint.cjs +9 -2
- package/scripts/changeset/serialize.cjs +5 -1
- package/scripts/gen-capability-matrix.cjs +1 -1
- package/scripts/gen-context-index.cjs +448 -0
- package/scripts/gen-inventory-manifest.cjs +101 -1
- package/scripts/gen-prompt-budget-parity-corpus.cjs +645 -0
- package/scripts/gen-registry.cjs +39 -15
- package/scripts/gen-section-manifest.cjs +638 -0
- package/scripts/generate-package-identity.cjs +4 -2
- package/scripts/lint-allow-test-rule-refs.allowlist.json +17 -31
- package/scripts/lint-compiled-artifact-sync.cjs +6 -1
- package/scripts/lint-docs-command-form.cjs +195 -0
- package/scripts/lint-docs-required.cjs +9 -1
- package/scripts/lint-emitted-drift-ack.cjs +215 -20
- package/scripts/lint-example-parser-parity.cjs +395 -0
- package/scripts/lint-test-file-count.allowlist.json +27 -1
- package/scripts/mutation-matrix.cjs +13 -0
- package/scripts/prompt-injection-scan.sh +27 -6
- package/scripts/registry-schema.cjs +323 -94
- package/scripts/run-tests.cjs +3 -2
- package/scripts/validate-registry.cjs +10 -6
- package/skills/gsd-autonomous/SKILL.md +1 -1
- package/skills/gsd-execute-phase/SKILL.md +1 -1
- package/skills/gsd-mempalace-capture/SKILL.md +1 -1
- package/skills/gsd-new-milestone/SKILL.md +1 -1
- package/skills/gsd-plan-phase/SKILL.md +2 -2
- package/vscode/package.json +1 -1
- package/scripts/gen-emitted-baseline.cjs +0 -145
|
@@ -10,7 +10,12 @@
|
|
|
10
10
|
*
|
|
11
11
|
* Used by the review pipeline to support small-context models.
|
|
12
12
|
*
|
|
13
|
-
* Trim priority (in order — never violate)
|
|
13
|
+
* Trim priority (in order — never violate). As of #2929 (epic #1671 Phase 2)
|
|
14
|
+
* the ladder itself is executed by the shared `context-composer` seam
|
|
15
|
+
* (`composeWithinBudget`); this module builds the fragment list in this
|
|
16
|
+
* exact order, delegates the decision of what survives/shrinks/truncates,
|
|
17
|
+
* and keeps only the prompt-specific rendering (`renderNote`,
|
|
18
|
+
* `assemblePrompt`) and the hard-fail response shapes here:
|
|
14
19
|
* 1. Instructions: ALWAYS kept verbatim
|
|
15
20
|
* 2. Reserve note tokens FIRST when any trim is anticipated
|
|
16
21
|
* 3. Roadmap: ALWAYS kept verbatim
|
|
@@ -22,9 +27,20 @@
|
|
|
22
27
|
* 9. Hard-fail: if minimum-set exceeds effectiveBudget
|
|
23
28
|
*/
|
|
24
29
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
30
|
+
exports.PLAN_FLOOR_CHARS = void 0;
|
|
25
31
|
exports.estimateTokens = estimateTokens;
|
|
32
|
+
exports.buildBudgetFragments = buildBudgetFragments;
|
|
26
33
|
exports.applyBudget = applyBudget;
|
|
34
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports -- context-composer.cjs is an export= CommonJS module
|
|
35
|
+
const contextComposer = require("./context-composer.cjs");
|
|
27
36
|
const NOTE_RESERVE_TOKENS = 80;
|
|
37
|
+
/**
|
|
38
|
+
* Floor per plan when proportionally truncating and for the minimum-set
|
|
39
|
+
* check. Exported so consumers (notably the load-bearing contract gate,
|
|
40
|
+
* issue #3065) never need to re-declare this value locally.
|
|
41
|
+
*/
|
|
42
|
+
exports.PLAN_FLOOR_CHARS = 1024;
|
|
43
|
+
const MIN_PLAN_BYTES = exports.PLAN_FLOOR_CHARS;
|
|
28
44
|
const DEFAULT_NOTE_TEMPLATE = [
|
|
29
45
|
'<note>',
|
|
30
46
|
'Prompt automatically trimmed to fit a {budget}-token budget.',
|
|
@@ -51,30 +67,6 @@ function renderNote(template, budget, omitted, planTruncationPct) {
|
|
|
51
67
|
.replace('{omittedList}', omittedList)
|
|
52
68
|
.replace('{planTruncationPct}', String(Math.round(planTruncationPct)));
|
|
53
69
|
}
|
|
54
|
-
/**
|
|
55
|
-
* Head-shrink a string to at most `maxLines` lines.
|
|
56
|
-
*/
|
|
57
|
-
function headShrink(text, maxLines) {
|
|
58
|
-
if (maxLines <= 0)
|
|
59
|
-
return '';
|
|
60
|
-
let idx = -1;
|
|
61
|
-
let seen = 0;
|
|
62
|
-
while (seen < maxLines) {
|
|
63
|
-
idx = text.indexOf('\n', idx + 1);
|
|
64
|
-
if (idx === -1)
|
|
65
|
-
return text;
|
|
66
|
-
seen += 1;
|
|
67
|
-
}
|
|
68
|
-
return text.slice(0, idx);
|
|
69
|
-
}
|
|
70
|
-
/**
|
|
71
|
-
* Tail-truncate a string to at most `maxChars` characters.
|
|
72
|
-
*/
|
|
73
|
-
function tailTruncate(text, maxChars) {
|
|
74
|
-
if (text.length <= maxChars)
|
|
75
|
-
return text;
|
|
76
|
-
return text.slice(0, maxChars);
|
|
77
|
-
}
|
|
78
70
|
/**
|
|
79
71
|
* Assemble the final prompt string from its sections.
|
|
80
72
|
*/
|
|
@@ -99,38 +91,92 @@ function assemblePrompt(parts) {
|
|
|
99
91
|
blocks.push('## Requirements\n\n' + requirements);
|
|
100
92
|
return blocks.join('\n\n');
|
|
101
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* Build the `composeWithinBudget` fragment array for a set of review prompt
|
|
96
|
+
* sections, with each fragment's declared trim strategy attached.
|
|
97
|
+
*
|
|
98
|
+
* Extracted (issue #3065) so the load-bearing contract gate
|
|
99
|
+
* (tests/load-bearing-contract-gate.test.cjs) can assert directly over the
|
|
100
|
+
* REAL declared strategies used by `applyBudget`, rather than a hand-copied
|
|
101
|
+
* duplicate array. A duplicate would let production silently diverge from
|
|
102
|
+
* the gate (e.g. flipping `roadmap` from `verbatim` to `drop` would keep the
|
|
103
|
+
* gate green if it read from a copy) — exactly the
|
|
104
|
+
* `DEFECT.GENERATIVE-FIX` divergence class this extraction exists to
|
|
105
|
+
* prevent. Pure; performs no I/O and has no side effects.
|
|
106
|
+
*/
|
|
107
|
+
function buildBudgetFragments(sections, projectMdHeadLines) {
|
|
108
|
+
const { instructions, roadmap, plans, projectMd: projectMdRaw = null, context: contextRaw = null, research: researchRaw = null, requirements: requirementsRaw = null, } = sections;
|
|
109
|
+
// Unique per-plan fragment ids: plain `plan:<file>` unless a filename
|
|
110
|
+
// collision forces disambiguation by index — composeWithinBudget throws on
|
|
111
|
+
// duplicate ids, and legitimate input may repeat a filename across plans.
|
|
112
|
+
const usedPlanIds = new Set();
|
|
113
|
+
const planIds = plans.map((p, idx) => {
|
|
114
|
+
let id = `plan:${p.file}`;
|
|
115
|
+
if (usedPlanIds.has(id))
|
|
116
|
+
id = `plan:${idx}:${p.file}`;
|
|
117
|
+
usedPlanIds.add(id);
|
|
118
|
+
return id;
|
|
119
|
+
});
|
|
120
|
+
const planFragments = plans.map((p, idx) => ({
|
|
121
|
+
id: planIds[idx],
|
|
122
|
+
content: p.content,
|
|
123
|
+
wrapper: `### ${p.file}\n\n`,
|
|
124
|
+
strategy: { kind: 'proportional-truncate', floorChars: MIN_PLAN_BYTES },
|
|
125
|
+
group: 'plans',
|
|
126
|
+
required: true,
|
|
127
|
+
}));
|
|
128
|
+
return [
|
|
129
|
+
{ id: 'instructions', content: instructions, wrapper: '', strategy: { kind: 'verbatim' }, required: true },
|
|
130
|
+
{ id: 'roadmap', content: roadmap, wrapper: '## Roadmap\n\n', strategy: { kind: 'verbatim' }, required: true },
|
|
131
|
+
{
|
|
132
|
+
id: 'projectMd',
|
|
133
|
+
content: projectMdRaw ?? '',
|
|
134
|
+
wrapper: '## Project\n\n',
|
|
135
|
+
strategy: { kind: 'head-shrink', maxLines: projectMdHeadLines },
|
|
136
|
+
},
|
|
137
|
+
{ id: 'plans-header', content: '', wrapper: '## Plans\n\n', strategy: { kind: 'verbatim' }, required: true },
|
|
138
|
+
...planFragments,
|
|
139
|
+
{ id: 'context', content: contextRaw ?? '', wrapper: '## Context\n\n', strategy: { kind: 'drop' } },
|
|
140
|
+
{ id: 'research', content: researchRaw ?? '', wrapper: '## Research\n\n', strategy: { kind: 'drop' } },
|
|
141
|
+
{ id: 'requirements', content: requirementsRaw ?? '', wrapper: '## Requirements\n\n', strategy: { kind: 'drop' } },
|
|
142
|
+
];
|
|
143
|
+
}
|
|
102
144
|
/**
|
|
103
145
|
* Apply a token budget to a set of review prompt sections.
|
|
104
146
|
* Returns the trimmed prompt and structured metadata.
|
|
105
147
|
*/
|
|
106
148
|
function applyBudget({ sections, budget, options = {} }) {
|
|
107
149
|
const { safetyMarginPct = 10, noteTemplate = DEFAULT_NOTE_TEMPLATE, projectMdHeadLines = 40, } = options;
|
|
108
|
-
const
|
|
109
|
-
const
|
|
110
|
-
//
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
150
|
+
const { plans } = sections;
|
|
151
|
+
const fragments = buildBudgetFragments(sections, projectMdHeadLines);
|
|
152
|
+
// Recover the per-plan fragment ids buildBudgetFragments assigned (in
|
|
153
|
+
// `plans` declaration order) rather than re-deriving the id-collision
|
|
154
|
+
// logic here — a single source of truth for id assignment.
|
|
155
|
+
const planIds = fragments.filter((f) => f.group === 'plans').map((f) => f.id);
|
|
156
|
+
const composed = contextComposer.composeWithinBudget({
|
|
157
|
+
fragments,
|
|
158
|
+
budget,
|
|
159
|
+
measure: estimateTokens,
|
|
160
|
+
options: {
|
|
161
|
+
safetyMarginPct,
|
|
162
|
+
reserve: NOTE_RESERVE_TOKENS,
|
|
163
|
+
charsPerUnit: 4,
|
|
164
|
+
// Minimum-set floor: instructions + roadmap (in full) + 1KB per plan.
|
|
165
|
+
// NOTE_RESERVE_TOKENS is intentionally excluded — a note is only
|
|
166
|
+
// injected when trimming actually occurs, and a prompt that fits
|
|
167
|
+
// without any trim needs no note at all. Wrappers are also excluded
|
|
168
|
+
// (this checks the floor, not the steady-state budget).
|
|
169
|
+
minimumFor: (f) => {
|
|
170
|
+
if (f.id === 'instructions' || f.id === 'roadmap')
|
|
171
|
+
return f.content;
|
|
172
|
+
if (f.id.startsWith('plan:'))
|
|
173
|
+
return f.content.slice(0, MIN_PLAN_BYTES);
|
|
174
|
+
return null;
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
const effectiveBudget = composed.metadata.effectiveBudget;
|
|
179
|
+
if (composed.metadata.hardFailed) {
|
|
134
180
|
return {
|
|
135
181
|
prompt: '',
|
|
136
182
|
metadata: {
|
|
@@ -145,127 +191,45 @@ function applyBudget({ sections, budget, options = {} }) {
|
|
|
145
191
|
},
|
|
146
192
|
};
|
|
147
193
|
}
|
|
148
|
-
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
const
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
:
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
const getCurrentBaseTokens = () => staticBaseTokens +
|
|
175
|
-
projectTokens +
|
|
176
|
-
planContentTokens +
|
|
177
|
-
contextTokens +
|
|
178
|
-
researchTokens +
|
|
179
|
-
requirementsTokens;
|
|
180
|
-
let currentBaseTokens = getCurrentBaseTokens();
|
|
181
|
-
// Detect budget pressure: is ANY trim needed?
|
|
182
|
-
// Pressure exists when the current base tokens already exceed the effective
|
|
183
|
-
// budget. Only when pressure is real do we reserve NOTE_RESERVE_TOKENS so
|
|
184
|
-
// the note itself fits after trimming. Checking against
|
|
185
|
-
// effectiveBudget - NOTE_RESERVE_TOKENS (the old threshold) would cause
|
|
186
|
-
// spurious pressure 80 tokens early, dropping sections that fit fine.
|
|
187
|
-
const baseTokens = currentBaseTokens;
|
|
188
|
-
const budgetUnderPressure = baseTokens > effectiveBudget;
|
|
189
|
-
// Available for content (reserve note slot when under pressure)
|
|
190
|
-
const contentBudget = budgetUnderPressure
|
|
191
|
-
? effectiveBudget - NOTE_RESERVE_TOKENS
|
|
192
|
-
: effectiveBudget;
|
|
193
|
-
// ── Trim step 1: head-shrink PROJECT.md ───────────────────────────────────
|
|
194
|
-
if (currentBaseTokens > contentBudget && projectMd) {
|
|
195
|
-
const shrunk = headShrink(projectMd, projectMdHeadLines);
|
|
196
|
-
if (shrunk !== projectMd) {
|
|
197
|
-
projectMd = shrunk;
|
|
198
|
-
projectMdShrunk = true;
|
|
199
|
-
projectTokens = TOKENS_PROJECT_HEADER + estimateTokens(projectMd);
|
|
200
|
-
currentBaseTokens = getCurrentBaseTokens();
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
// ── Trim step 2: proportional plan truncation ─────────────────────────────
|
|
204
|
-
if (currentBaseTokens > contentBudget) {
|
|
205
|
-
// Compute tokens available for plan content only
|
|
206
|
-
const overhead = staticBaseTokens +
|
|
207
|
-
projectTokens +
|
|
208
|
-
contextTokens +
|
|
209
|
-
researchTokens +
|
|
210
|
-
requirementsTokens;
|
|
211
|
-
const planBudgetTokens = contentBudget - overhead;
|
|
212
|
-
const totalPlanTokens = planContentTokens;
|
|
213
|
-
if (planBudgetTokens > 0 && planBudgetTokens < totalPlanTokens) {
|
|
214
|
-
// Proportional share per plan (at least 1KB per plan)
|
|
215
|
-
const totalOriginalChars = plans.reduce((sum, p) => sum + p.content.length, 0);
|
|
216
|
-
const totalPlanCharsBudget = planBudgetTokens * 4;
|
|
217
|
-
workingPlans = workingPlans.map((p) => {
|
|
218
|
-
const proportionalShare = totalOriginalChars > 0
|
|
219
|
-
? Math.floor((p.content.length / totalOriginalChars) * totalPlanCharsBudget)
|
|
220
|
-
: 0;
|
|
221
|
-
const maxChars = Math.max(proportionalShare, MIN_PLAN_BYTES);
|
|
222
|
-
return { file: p.file, content: tailTruncate(p.content, maxChars) };
|
|
223
|
-
});
|
|
224
|
-
const newTotalChars = workingPlans.reduce((sum, p) => sum + p.content.length, 0);
|
|
225
|
-
if (totalOriginalChars > 0) {
|
|
226
|
-
planTruncationPct =
|
|
227
|
-
((totalOriginalChars - newTotalChars) / totalOriginalChars) * 100;
|
|
228
|
-
}
|
|
229
|
-
planContentTokens = workingPlans.reduce((sum, p) => sum + estimateTokens(p.content), 0);
|
|
230
|
-
currentBaseTokens = getCurrentBaseTokens();
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
// ── Trim step 3: drop context ─────────────────────────────────────────────
|
|
234
|
-
if (currentBaseTokens > contentBudget && context) {
|
|
235
|
-
context = null;
|
|
236
|
-
omitted.push('context');
|
|
237
|
-
contextTokens = 0;
|
|
238
|
-
currentBaseTokens = getCurrentBaseTokens();
|
|
239
|
-
}
|
|
240
|
-
// ── Trim step 4: drop research ────────────────────────────────────────────
|
|
241
|
-
if (currentBaseTokens > contentBudget && research) {
|
|
242
|
-
research = null;
|
|
243
|
-
omitted.push('research');
|
|
244
|
-
researchTokens = 0;
|
|
245
|
-
currentBaseTokens = getCurrentBaseTokens();
|
|
246
|
-
}
|
|
247
|
-
// ── Trim step 5: drop requirements (last resort) ──────────────────────────
|
|
248
|
-
if (currentBaseTokens > contentBudget && requirements) {
|
|
249
|
-
requirements = null;
|
|
250
|
-
omitted.push('requirements');
|
|
251
|
-
requirementsTokens = 0;
|
|
252
|
-
currentBaseTokens = getCurrentBaseTokens();
|
|
253
|
-
}
|
|
194
|
+
const byId = new Map(composed.fragments.map((f) => [f.id, f]));
|
|
195
|
+
const get = (id) => {
|
|
196
|
+
const found = byId.get(id);
|
|
197
|
+
if (!found)
|
|
198
|
+
throw new Error(`applyBudget: missing composed fragment "${id}"`);
|
|
199
|
+
return found;
|
|
200
|
+
};
|
|
201
|
+
const instructionsOut = get('instructions').content;
|
|
202
|
+
const roadmapOut = get('roadmap').content;
|
|
203
|
+
const projectMdFragment = get('projectMd');
|
|
204
|
+
const projectMd = projectMdFragment.present ? projectMdFragment.content : null;
|
|
205
|
+
const contextFragment = get('context');
|
|
206
|
+
const context = contextFragment.present ? contextFragment.content : null;
|
|
207
|
+
const researchFragment = get('research');
|
|
208
|
+
const research = researchFragment.present ? researchFragment.content : null;
|
|
209
|
+
const requirementsFragment = get('requirements');
|
|
210
|
+
const requirements = requirementsFragment.present ? requirementsFragment.content : null;
|
|
211
|
+
// Plans are never dropped — only their content may be truncated — so map
|
|
212
|
+
// deliberately by original index/file rather than relying on `present`.
|
|
213
|
+
const workingPlans = plans.map((p, idx) => ({
|
|
214
|
+
file: p.file,
|
|
215
|
+
content: get(planIds[idx]).content,
|
|
216
|
+
}));
|
|
217
|
+
const omitted = composed.metadata.omitted;
|
|
218
|
+
const projectMdShrunk = composed.metadata.shrunk.includes('projectMd');
|
|
219
|
+
const planTruncationPct = composed.metadata.truncationPct;
|
|
254
220
|
// ── Decide whether note is actually needed ────────────────────────────────
|
|
255
221
|
const anyTrimOccurred = omitted.length > 0 || projectMdShrunk || planTruncationPct > 0;
|
|
256
222
|
let note = null;
|
|
223
|
+
let noteInjected = false;
|
|
257
224
|
if (anyTrimOccurred) {
|
|
258
225
|
note = renderNote(noteTemplate, budget, omitted, planTruncationPct);
|
|
259
226
|
noteInjected = true;
|
|
260
227
|
}
|
|
261
|
-
// Suppress unused variable warning — currentBaseTokens is used via the
|
|
262
|
-
// closure in getCurrentBaseTokens(); the final value is not used directly.
|
|
263
|
-
void currentBaseTokens;
|
|
264
228
|
// ── Assemble ──────────────────────────────────────────────────────────────
|
|
265
229
|
const prompt = assemblePrompt({
|
|
266
|
-
instructions,
|
|
230
|
+
instructions: instructionsOut,
|
|
267
231
|
note,
|
|
268
|
-
roadmap,
|
|
232
|
+
roadmap: roadmapOut,
|
|
269
233
|
projectMd,
|
|
270
234
|
plans: workingPlans,
|
|
271
235
|
context,
|
|
@@ -274,7 +238,6 @@ function applyBudget({ sections, budget, options = {} }) {
|
|
|
274
238
|
});
|
|
275
239
|
const estimatedTokens = estimateTokens(prompt);
|
|
276
240
|
if (estimatedTokens > effectiveBudget) {
|
|
277
|
-
hardFailed = true;
|
|
278
241
|
return {
|
|
279
242
|
prompt: '',
|
|
280
243
|
metadata: {
|
|
@@ -284,7 +247,7 @@ function applyBudget({ sections, budget, options = {} }) {
|
|
|
284
247
|
omitted,
|
|
285
248
|
projectMdShrunk,
|
|
286
249
|
planTruncationPct,
|
|
287
|
-
hardFailed,
|
|
250
|
+
hardFailed: true,
|
|
288
251
|
noteInjected,
|
|
289
252
|
},
|
|
290
253
|
};
|
|
@@ -298,7 +261,7 @@ function applyBudget({ sections, budget, options = {} }) {
|
|
|
298
261
|
omitted,
|
|
299
262
|
projectMdShrunk,
|
|
300
263
|
planTruncationPct,
|
|
301
|
-
hardFailed,
|
|
264
|
+
hardFailed: false,
|
|
302
265
|
noteInjected,
|
|
303
266
|
},
|
|
304
267
|
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Conservative cleanup for artifact surfaces retired by a runtime descriptor.
|
|
4
|
+
*
|
|
5
|
+
* A descriptor may stop materializing an artifact kind while old installs
|
|
6
|
+
* still contain files from that surface. Install and profile/surface apply
|
|
7
|
+
* both call this helper so either path converges an existing installation.
|
|
8
|
+
* Only files proven managed by the previous install manifest are removed;
|
|
9
|
+
* modified and unknown files are preserved for the journaled installer
|
|
10
|
+
* migration (or the user) to handle without data loss.
|
|
11
|
+
*/
|
|
12
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
13
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
14
|
+
};
|
|
15
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
16
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
17
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
18
|
+
const installerMigrations = require("./installer-migrations.cjs");
|
|
19
|
+
const external_descriptor_trust_cjs_1 = require("./external-descriptor-trust.cjs");
|
|
20
|
+
// Generated at build time, so no TypeScript source declaration exists.
|
|
21
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
22
|
+
const capabilityRegistry = require('./capability-registry.cjs');
|
|
23
|
+
function retiredArtifactsFor(runtime) {
|
|
24
|
+
const configured = capabilityRegistry.runtimes?.[runtime]?.runtime?.hostBehaviors?.retiredArtifacts;
|
|
25
|
+
return Array.isArray(configured) ? configured : [];
|
|
26
|
+
}
|
|
27
|
+
function pruneRetiredRuntimeArtifacts(runtime, configDir) {
|
|
28
|
+
const result = { removed: [], preserved: [] };
|
|
29
|
+
const declarations = retiredArtifactsFor(runtime);
|
|
30
|
+
if (declarations.length === 0)
|
|
31
|
+
return result;
|
|
32
|
+
const manifest = installerMigrations.readInstallManifest(configDir);
|
|
33
|
+
for (const declaration of declarations) {
|
|
34
|
+
const destSubpath = declaration.destSubpath;
|
|
35
|
+
const prefix = declaration.prefix;
|
|
36
|
+
const suffix = declaration.suffix;
|
|
37
|
+
if (typeof destSubpath !== 'string' || destSubpath.length === 0 ||
|
|
38
|
+
typeof prefix !== 'string' || prefix.length === 0 ||
|
|
39
|
+
typeof suffix !== 'string' || suffix.length === 0 ||
|
|
40
|
+
!(0, external_descriptor_trust_cjs_1.isPathConfined)(destSubpath, configDir)) {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const destDir = node_path_1.default.resolve(configDir, destSubpath);
|
|
44
|
+
let entries;
|
|
45
|
+
try {
|
|
46
|
+
if (!node_fs_1.default.existsSync(destDir) || node_fs_1.default.lstatSync(destDir).isSymbolicLink())
|
|
47
|
+
continue;
|
|
48
|
+
entries = node_fs_1.default.readdirSync(destDir, { withFileTypes: true });
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
for (const entry of entries) {
|
|
54
|
+
if (!entry.isFile() || !entry.name.startsWith(prefix) || !entry.name.endsWith(suffix))
|
|
55
|
+
continue;
|
|
56
|
+
const relPath = node_path_1.default.posix.join(destSubpath.replace(/\\/g, '/'), entry.name);
|
|
57
|
+
const artifact = installerMigrations.classifyArtifact(configDir, relPath, manifest);
|
|
58
|
+
if (artifact.classification !== 'managed-pristine') {
|
|
59
|
+
result.preserved.push(relPath);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
node_fs_1.default.unlinkSync(node_path_1.default.join(destDir, entry.name));
|
|
64
|
+
result.removed.push(relPath);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
result.preserved.push(relPath);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
if (node_fs_1.default.readdirSync(destDir).length === 0)
|
|
72
|
+
node_fs_1.default.rmdirSync(destDir);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
// Non-empty, unreadable, or concurrently changed: preserve the directory.
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
module.exports = { pruneRetiredRuntimeArtifacts, retiredArtifactsFor };
|
|
@@ -52,6 +52,7 @@
|
|
|
52
52
|
*/
|
|
53
53
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
54
54
|
exports.DOCS_PARITY_VIOLATION = exports.LANE_SLUG_RE = exports.PARITY_VIOLATION = exports.REVIEWER_LANES = exports.ARGV_PLACEHOLDER = void 0;
|
|
55
|
+
exports.mergeReviewerLanes = mergeReviewerLanes;
|
|
55
56
|
exports.checkReviewerLaneParity = checkReviewerLaneParity;
|
|
56
57
|
exports.checkReviewerDocsParity = checkReviewerDocsParity;
|
|
57
58
|
/**
|
|
@@ -423,6 +424,104 @@ exports.REVIEWER_LANES = Object.freeze([
|
|
|
423
424
|
handler: null,
|
|
424
425
|
},
|
|
425
426
|
].map((lane) => Object.freeze(lane)));
|
|
427
|
+
/**
|
|
428
|
+
* Merge the first-party lane set with installed overlay `reviewer` bodies
|
|
429
|
+
* (ADR-2782 D8: first-party ∪ overlay, first-party wins on slug collision).
|
|
430
|
+
*
|
|
431
|
+
* `routeReviewLane` (gsd-core/bin/gsd-tools.cjs) consumes this to build the lane
|
|
432
|
+
* map its `sections`/`flags`/`plan`/`invoke` subcommands resolve against, so an
|
|
433
|
+
* installed, consented third-party reviewer capability (`role:"reviewer"` with a
|
|
434
|
+
* declared `reviewer` body) becomes selectable, plannable, and invocable through
|
|
435
|
+
* the same surface as a built-in lane — closing #2927, where the overlay was
|
|
436
|
+
* roster-visible (`deriveReviewerSlugs`) and disclosed at install
|
|
437
|
+
* (`collectReviewerLaneSurfaces`) but never reached the invocation path.
|
|
438
|
+
*
|
|
439
|
+
* ADR-2782 D1 deliberately made the manifest `reviewer` body field-identical to
|
|
440
|
+
* `ReviewerLaneCommon` so "Phase 2 harvests the shape into the capability manifest
|
|
441
|
+
* with no translation layer" (CONTEXT.md reviewer-lane-descriptor predicate). The
|
|
442
|
+
* overlay body IS already `ReviewerLane`-shaped; this function MERGES, it does not
|
|
443
|
+
* PROJECT — there is no field renaming, no vocabulary translation. That is the
|
|
444
|
+
* design decision the bug violated by simply never calling anything.
|
|
445
|
+
*
|
|
446
|
+
* Pure and TOTAL — never throws on any input, because third-party overlay
|
|
447
|
+
* manifests are untrusted data reaching this seam at load time. A cap whose
|
|
448
|
+
* `reviewer` body is absent, non-object, or carries an empty/grammar-invalid slug
|
|
449
|
+
* is SKIPPED (contributes no lane) rather than crashing, so one malformed overlay
|
|
450
|
+
* cannot take the whole `review-lane` surface (and every first-party lane with it)
|
|
451
|
+
* down. The slug grammar (`LANE_SLUG_RE`) is enforced HERE even though
|
|
452
|
+
* `resolveLanePlan`'s trust boundary re-checks it, because `sections`/`flags` read
|
|
453
|
+
* lane fields before reaching `resolveLanePlan` — defense in depth against the
|
|
454
|
+
* path-traversal class the invocation boundary exists for.
|
|
455
|
+
*
|
|
456
|
+
* First-party wins: an overlay declaring a slug that already names a first-party
|
|
457
|
+
* lane is silently superseded (the first-party entry is kept, identity-stable),
|
|
458
|
+
* never overwritten. The first-party object is returned by reference, not copied.
|
|
459
|
+
*
|
|
460
|
+
* @param firstParty The frozen first-party lane set (`REVIEWER_LANES`).
|
|
461
|
+
* @param registry The merged capability registry
|
|
462
|
+
* (`loadRegistry({ includeInstalled: true })`); only its
|
|
463
|
+
* `capabilities` map is read. A cap contributes a lane iff it
|
|
464
|
+
* carries an object `reviewer` body with a non-empty,
|
|
465
|
+
* grammar-valid `slug`. The `role:"runtime"` legacy
|
|
466
|
+
* `reviewerCli` alias contributes NO lane here — it has no lane
|
|
467
|
+
* descriptor, and the selection roster (`deriveReviewerSlugs`)
|
|
468
|
+
* is a separate surface.
|
|
469
|
+
* @returns A NEW array: first-party lanes (in order) followed by accepted overlay
|
|
470
|
+
* lanes (in registry iteration order). Callers must not mutate it.
|
|
471
|
+
*/
|
|
472
|
+
function mergeReviewerLanes(firstParty, registry) {
|
|
473
|
+
// First-party always wins and is returned by reference (identity-stable), so a
|
|
474
|
+
// collision cannot perturb the first-party entry a consumer already holds.
|
|
475
|
+
const bySlug = new Map();
|
|
476
|
+
const ordered = [];
|
|
477
|
+
for (const lane of firstParty) {
|
|
478
|
+
const slug = typeof lane.slug === 'string' ? lane.slug.trim() : '';
|
|
479
|
+
if (!slug)
|
|
480
|
+
continue; // unreachable for the shipped frozen set, but this is exported
|
|
481
|
+
if (!bySlug.has(slug)) {
|
|
482
|
+
bySlug.set(slug, lane);
|
|
483
|
+
ordered.push(lane);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
const capabilities = (registry && registry.capabilities) || {};
|
|
487
|
+
for (const cap of Object.values(capabilities)) {
|
|
488
|
+
if (cap === null || typeof cap !== 'object' || Array.isArray(cap))
|
|
489
|
+
continue;
|
|
490
|
+
const body = cap.reviewer;
|
|
491
|
+
// C1/C2 (mirroring collectReviewerLaneSurfaces): a missing, null, or
|
|
492
|
+
// non-object reviewer body declares no lane — never an error at this layer.
|
|
493
|
+
if (body === null || typeof body !== 'object' || Array.isArray(body))
|
|
494
|
+
continue;
|
|
495
|
+
const rawSlug = body.slug;
|
|
496
|
+
const slug = typeof rawSlug === 'string' ? rawSlug.trim() : '';
|
|
497
|
+
// Empty/whitespace slug: nothing to key on. Grammar-invalid slug: the
|
|
498
|
+
// path-traversal class — skip rather than admit, even though resolveLanePlan
|
|
499
|
+
// would reject it downstream. Both are skips, not throws.
|
|
500
|
+
if (!slug || !exports.LANE_SLUG_RE.test(slug))
|
|
501
|
+
continue;
|
|
502
|
+
if (bySlug.has(slug))
|
|
503
|
+
continue; // D8: first-party (or an earlier overlay) wins
|
|
504
|
+
// The body is already ReviewerLane-shaped per ADR-2782 D1. We do NOT
|
|
505
|
+
// deep-validate every field here: the invocation trust boundary
|
|
506
|
+
// (resolveLanePlan) is the seam that re-validates a lane before it runs, and
|
|
507
|
+
// `sections`/`flags` are both tolerant of a partial body (flags filters by
|
|
508
|
+
// shape; sections reads slug/reviewsSection which a non-string cap simply
|
|
509
|
+
// renders as undefined). Admitting the body keeps the merge a pure merge.
|
|
510
|
+
//
|
|
511
|
+
// The slug is normalized to its trimmed value on the stored lane so the merge
|
|
512
|
+
// and the selection roster (`deriveReviewerSlugs`, which trims before adding)
|
|
513
|
+
// agree on the canonical key — otherwise a body declaring `slug: ' x '`
|
|
514
|
+
// would key the map on `'x'` but emit the raw `' x '` in `sections` and fail
|
|
515
|
+
// to match a `--selected` value coming from the roster. (The capability
|
|
516
|
+
// validator's grammar check rejects surrounding whitespace before this is
|
|
517
|
+
// reachable through a real install, so this is belt-and-braces parity with the
|
|
518
|
+
// roster, not a live-input guard.)
|
|
519
|
+
const lane = { ...body, slug };
|
|
520
|
+
bySlug.set(slug, lane);
|
|
521
|
+
ordered.push(lane);
|
|
522
|
+
}
|
|
523
|
+
return ordered;
|
|
524
|
+
}
|
|
426
525
|
/* ------------------------------------------------------------------ *
|
|
427
526
|
* DEFECT.GENERATIVE-FIX parity (CONTEXT.md:797)
|
|
428
527
|
* ------------------------------------------------------------------ */
|
|
@@ -283,18 +283,32 @@ function antigravityWatermark(workspace, deps) {
|
|
|
283
283
|
return { convId, lines: deps.readFile(tx).split(/\r?\n/).filter((l) => l.trim()).length };
|
|
284
284
|
}
|
|
285
285
|
catch {
|
|
286
|
-
|
|
286
|
+
// #3118: this conv-id pre-dates this run, so its transcript exists but this run cannot verify
|
|
287
|
+
// its line count. Reporting `lines: 0` would assert a fact we could not check — flag it instead
|
|
288
|
+
// so the fallback can decline rather than silently skip zero and replay a stale response.
|
|
289
|
+
return { convId, lines: 0, unreadable: true };
|
|
287
290
|
}
|
|
288
291
|
}
|
|
289
|
-
/**
|
|
292
|
+
/**
|
|
293
|
+
* Workspace lookup is case-insensitive — the leg's jq did `ascii_downcase` on both sides.
|
|
294
|
+
*
|
|
295
|
+
* #3118: a successful `JSON.parse` does not by itself make the payload a usable object —
|
|
296
|
+
* `JSON.parse('null')` succeeds and returns `null`, so a truncated/zeroed cache file slips past
|
|
297
|
+
* the callers' parse-only try/catch. `typeof null === 'object'`, so the guard below must exclude
|
|
298
|
+
* `null` explicitly. Arrays are excluded too (not a workspace map), which also falls out of the
|
|
299
|
+
* `Object.entries`/`hasOwnProperty` lookups below returning nothing for array input.
|
|
300
|
+
*/
|
|
290
301
|
function resolveConvId(cache, workspace) {
|
|
291
|
-
if (
|
|
292
|
-
|
|
302
|
+
if (cache === null || typeof cache !== 'object')
|
|
303
|
+
return '';
|
|
304
|
+
const record = cache;
|
|
305
|
+
if (Object.prototype.hasOwnProperty.call(record, workspace)) {
|
|
306
|
+
const direct = record[workspace];
|
|
293
307
|
if (typeof direct === 'string' && direct)
|
|
294
308
|
return direct;
|
|
295
309
|
}
|
|
296
310
|
const target = workspace.toLowerCase();
|
|
297
|
-
for (const [k, v] of Object.entries(
|
|
311
|
+
for (const [k, v] of Object.entries(record)) {
|
|
298
312
|
if (k.toLowerCase() === target && typeof v === 'string' && v)
|
|
299
313
|
return v;
|
|
300
314
|
}
|
|
@@ -324,6 +338,10 @@ function antigravityTranscriptFallback(workspace, mark, deps) {
|
|
|
324
338
|
const convId = resolveConvId(cache, workspace);
|
|
325
339
|
if (!convId)
|
|
326
340
|
return '';
|
|
341
|
+
// #3118: the watermark could not read this conversation's transcript, so there is no trustworthy
|
|
342
|
+
// skip for it. Declining is the fail-closed answer; skipping 0 would replay a prior run's review.
|
|
343
|
+
if (mark.unreadable === true && convId === mark.convId)
|
|
344
|
+
return '';
|
|
327
345
|
const tx = transcriptPath(deps.homeDir, convId);
|
|
328
346
|
if (!deps.exists(tx))
|
|
329
347
|
return '';
|
|
@@ -549,7 +567,13 @@ function runSpawnLane(plan, deps, repoRoot) {
|
|
|
549
567
|
? antigravityArgv(plan.argv, plan.promptPath, repoRoot, deps)
|
|
550
568
|
: plan.argv;
|
|
551
569
|
const out = deps.spawn(plan.binary, argv, { input, timeoutMs: plan.timeoutMs });
|
|
552
|
-
|
|
570
|
+
// #3086: surface spawn errors (ENOENT, ETIMEDOUT, etc.) that would otherwise
|
|
571
|
+
// be silently dropped — the review path read only stdout/stderr and treated
|
|
572
|
+
// an empty-stderr spawn failure as "the model had nothing to say".
|
|
573
|
+
const errContent = out.errorCode
|
|
574
|
+
? `${out.stderr ?? ''}\n[spawn error: ${out.errorCode}]\n`
|
|
575
|
+
: (out.stderr ?? '');
|
|
576
|
+
deps.writeFile(plan.errPath, errContent);
|
|
553
577
|
// `file-arg` lanes write the review themselves and their stdout is deliberately discarded (#1698).
|
|
554
578
|
let review = plan.outputTarget.kind === 'file'
|
|
555
579
|
? deps.exists(plan.outputTarget.path)
|