@opengsd/gsd-core 1.8.0 → 1.9.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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.opencode/plugins/gsd-core.js +31 -1
- package/agents/gsd-code-fixer.md +107 -34
- package/agents/gsd-codebase-mapper.md +1 -1
- package/agents/gsd-debug-session-manager.md +36 -0
- package/agents/gsd-executor.md +20 -7
- package/agents/gsd-intel-updater.md +3 -3
- package/agents/gsd-phase-researcher.md +4 -2
- package/agents/gsd-plan-checker.md +20 -0
- package/agents/gsd-planner.md +15 -23
- package/agents/gsd-project-researcher.md +2 -2
- package/agents/gsd-ui-auditor.md +0 -40
- package/bin/install.js +236 -107
- package/commands/gsd/plan-review-convergence.md +5 -1
- package/gsd-core/bin/gsd-tools.cjs +882 -4
- package/gsd-core/bin/lib/api-coverage.cjs +22 -8
- package/gsd-core/bin/lib/audit.cjs +8 -8
- package/gsd-core/bin/lib/capability-consent.cjs +40 -1
- package/gsd-core/bin/lib/capability-lifecycle.cjs +58 -0
- package/gsd-core/bin/lib/capability-loader.cjs +23 -1
- package/gsd-core/bin/lib/capability-registry.cjs +1353 -132
- package/gsd-core/bin/lib/capability-trust.cjs +468 -33
- package/gsd-core/bin/lib/capability-validator.cjs +882 -6
- package/gsd-core/bin/lib/check-command-router.cjs +12 -2
- package/gsd-core/bin/lib/cjs-command-router-adapter.cjs +15 -0
- package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +102 -12
- package/gsd-core/bin/lib/claude-orchestration.cjs +125 -22
- package/gsd-core/bin/lib/commands.cjs +246 -18
- package/gsd-core/bin/lib/config-loader.cjs +200 -28
- package/gsd-core/bin/lib/config.cjs +90 -5
- package/gsd-core/bin/lib/estimate-cli.cjs +336 -0
- package/gsd-core/bin/lib/frontmatter.cjs +125 -15
- package/gsd-core/bin/lib/host-integration.cjs +215 -8
- package/gsd-core/bin/lib/init.cjs +44 -19
- package/gsd-core/bin/lib/install-engine.cjs +1 -0
- package/gsd-core/bin/lib/milestone.cjs +36 -9
- package/gsd-core/bin/lib/model-catalog.cjs +51 -1
- package/gsd-core/bin/lib/observability/logger.cjs +7 -2
- package/gsd-core/bin/lib/phase-command-router.cjs +10 -1
- package/gsd-core/bin/lib/phase-estimation.cjs +398 -0
- package/gsd-core/bin/lib/phase-id.cjs +278 -5
- package/gsd-core/bin/lib/phase.cjs +61 -6
- package/gsd-core/bin/lib/plan-drift-guard.cjs +1 -1
- package/gsd-core/bin/lib/plan-scan.cjs +1 -1
- package/gsd-core/bin/lib/planning-workspace.cjs +9 -2
- package/gsd-core/bin/lib/profile-output.cjs +34 -8
- package/gsd-core/bin/lib/project-root.cjs +48 -0
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +927 -0
- package/gsd-core/bin/lib/review-lane-invocation.cjs +348 -0
- package/gsd-core/bin/lib/review-lane-runner.cjs +594 -0
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +114 -32
- package/gsd-core/bin/lib/roadmap-parser.cjs +54 -6
- package/gsd-core/bin/lib/roadmap.cjs +10 -4
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +31 -4
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +1 -1
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +140 -0
- package/gsd-core/bin/lib/runtime-name-policy.cjs +15 -2
- package/gsd-core/bin/lib/smart-entry.cjs +1 -1
- package/gsd-core/bin/lib/state-document.cjs +164 -20
- package/gsd-core/bin/lib/state-transition.cjs +28 -10
- package/gsd-core/bin/lib/state.cjs +141 -21
- package/gsd-core/bin/lib/uat-predicate.cjs +6 -4
- package/gsd-core/bin/lib/uat.cjs +9 -7
- package/gsd-core/bin/lib/ui-consideration-probe.cjs +2 -2
- package/gsd-core/bin/lib/unusable-input.cjs +216 -0
- package/gsd-core/bin/lib/validate.cjs +32 -0
- package/gsd-core/bin/lib/verification.cjs +51 -14
- package/gsd-core/bin/lib/verify.cjs +146 -22
- package/gsd-core/bin/lib/worktree-safety.cjs +360 -15
- package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +1 -13
- package/gsd-core/bin/shared/model-catalog.json +5 -0
- package/gsd-core/bin/shared/runtime-aliases.manifest.json +5 -0
- package/gsd-core/references/context-budget.md +40 -0
- package/gsd-core/references/gate-prompts.md +6 -3
- package/gsd-core/references/model-profile-resolution.md +64 -13
- package/gsd-core/references/offer-next.md +88 -0
- package/gsd-core/references/planning-config.md +2 -1
- package/gsd-core/references/reviewer-instances.md +28 -21
- package/gsd-core/references/runtime-aware-dispatch.md +42 -0
- package/gsd-core/references/ui-consideration-probe.md +2 -2
- package/gsd-core/references/worktree-branch-check.md +4 -4
- package/gsd-core/templates/summary-minimal.md +4 -0
- package/gsd-core/templates/summary-standard.md +4 -0
- package/gsd-core/templates/summary.md +7 -0
- package/gsd-core/workflows/ai-integration-phase.md +4 -4
- package/gsd-core/workflows/audit-fix.md +4 -0
- package/gsd-core/workflows/audit-milestone.md +8 -0
- package/gsd-core/workflows/autonomous.md +19 -15
- package/gsd-core/workflows/check-todos.md +2 -2
- package/gsd-core/workflows/code-review-fix.md +14 -6
- package/gsd-core/workflows/code-review.md +93 -21
- package/gsd-core/workflows/debug.md +10 -2
- package/gsd-core/workflows/diagnose-issues.md +4 -0
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +2 -4
- package/gsd-core/workflows/discuss-phase/modes/auto.md +0 -6
- package/gsd-core/workflows/discuss-phase-assumptions.md +15 -9
- package/gsd-core/workflows/discuss-phase.md +2 -2
- package/gsd-core/workflows/docs-update.md +8 -0
- package/gsd-core/workflows/eval-review.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +4 -0
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +160 -0
- package/gsd-core/workflows/execute-phase.md +85 -115
- package/gsd-core/workflows/execute-plan.md +5 -4
- package/gsd-core/workflows/explore.md +4 -0
- package/gsd-core/workflows/extract-learnings.md +21 -0
- package/gsd-core/workflows/help/modes/full.md +3 -3
- package/gsd-core/workflows/import.md +4 -1
- package/gsd-core/workflows/ingest-docs.md +4 -0
- package/gsd-core/workflows/map-codebase.md +13 -6
- package/gsd-core/workflows/new-milestone.md +10 -2
- package/gsd-core/workflows/new-project.md +11 -4
- package/gsd-core/workflows/next.md +5 -2
- package/gsd-core/workflows/plan-phase.md +42 -46
- package/gsd-core/workflows/plan-review-convergence.md +18 -14
- package/gsd-core/workflows/progress.md +1 -1
- package/gsd-core/workflows/quick.md +14 -3
- package/gsd-core/workflows/review.md +146 -575
- package/gsd-core/workflows/scan.md +9 -1
- package/gsd-core/workflows/secure-phase.md +10 -2
- package/gsd-core/workflows/ship.md +41 -11
- package/gsd-core/workflows/smart-entry.md +1 -1
- package/gsd-core/workflows/ui-phase.md +8 -1
- package/gsd-core/workflows/ui-review.md +8 -1
- package/gsd-core/workflows/update.md +104 -5
- package/gsd-core/workflows/validate-phase.md +10 -2
- package/gsd-core/workflows/verify-work.md +8 -1
- package/hooks/dist/gsd-cursor-session-start.js +6 -2
- package/hooks/dist/gsd-cursor-stop.js +6 -2
- package/hooks/dist/gsd-cursor-subagent-start.js +6 -2
- package/hooks/dist/gsd-graphify-update.sh +9 -0
- package/hooks/dist/gsd-phase-boundary.sh +14 -2
- package/hooks/dist/gsd-prompt-guard.js +101 -2
- package/hooks/dist/gsd-read-guard.js +100 -2
- package/hooks/dist/gsd-read-injection-scanner.js +109 -2
- package/hooks/dist/gsd-statusline.js +9 -6
- package/hooks/dist/gsd-workflow-guard.js +110 -6
- package/hooks/dist/gsd-worktree-path-guard.js +132 -8
- package/hooks/dist/lib/cursor-workspace.js +74 -0
- package/hooks/gsd-cursor-session-start.js +6 -2
- package/hooks/gsd-cursor-stop.js +6 -2
- package/hooks/gsd-cursor-subagent-start.js +6 -2
- package/hooks/gsd-graphify-update.sh +9 -0
- package/hooks/gsd-phase-boundary.sh +14 -2
- package/hooks/gsd-prompt-guard.js +101 -2
- package/hooks/gsd-read-guard.js +100 -2
- package/hooks/gsd-read-injection-scanner.js +109 -2
- package/hooks/gsd-statusline.js +9 -6
- package/hooks/gsd-workflow-guard.js +110 -6
- package/hooks/gsd-worktree-path-guard.js +132 -8
- package/hooks/lib/cursor-workspace.js +74 -0
- package/package.json +7 -7
- package/pi/gsd.cjs +26 -1
- package/scripts/check-coverage-gate.cjs +51 -0
- package/scripts/check-glossary-refs.cjs +24 -0
- package/scripts/ci-test-scope.cjs +67 -17
- package/scripts/gen-adr-index.cjs +6 -4
- package/scripts/gen-capability-matrix.cjs +26 -2
- package/scripts/gen-capability-registry.cjs +132 -34
- package/scripts/gen-emitted-baseline.cjs +145 -0
- package/scripts/gen-registry.cjs +39 -15
- package/scripts/lint-compiled-artifact-sync.cjs +146 -0
- package/scripts/lint-emitted-drift-ack.cjs +149 -0
- package/scripts/lint-fix-has-regression-test.cjs +131 -0
- package/scripts/lint-resolution-provenance.cjs +9 -0
- package/scripts/mutation-matrix.cjs +4 -0
- package/scripts/prompt-injection-scan.sh +6 -0
- package/scripts/registry-schema.cjs +372 -94
- package/scripts/release-notes/conventional-title.cjs +19 -1
- package/scripts/release-notes/format-github-release-notes.cjs +7 -3
- package/scripts/validate-registry.cjs +10 -6
- package/scripts/workflow-size.cjs +16 -8
- package/skills/gsd-plan-review-convergence/SKILL.md +5 -1
- package/vscode/package.json +1 -1
- package/scripts/gen-golden-install-parity-zcode.cjs +0 -77
- package/scripts/update-size-baseline.cjs +0 -68
|
@@ -0,0 +1,594 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Reviewer Lane Runner (ADR-2782 Phase 5b, #2799).
|
|
4
|
+
*
|
|
5
|
+
* Executes an invocation plan from `review-lane-invocation.cts`: probes the lane, spawns the binary
|
|
6
|
+
* or calls the HTTP endpoint, applies the empty-output policy, and dispatches the three first-party
|
|
7
|
+
* handlers D6 names. This is the module that replaces ~640 lines of hand-authored per-CLI bash.
|
|
8
|
+
*
|
|
9
|
+
* THREE RUNTIME DEPENDENCIES DISAPPEAR HERE, and that is a correctness win rather than a tidy-up:
|
|
10
|
+
* - `jq` — five legs needed it on PATH; it is absent on stock Windows/Git-Bash (#2589). Parsing is
|
|
11
|
+
* `JSON.parse` now.
|
|
12
|
+
* - `curl` — the three OpenAI-compatible legs shelled out to it. Node's `fetch` replaces it, and
|
|
13
|
+
* the raw response body (where an OpenAI-compatible server puts its error JSON on a 4xx/5xx
|
|
14
|
+
* while curl still exits 0) is no longer discarded by a pipe.
|
|
15
|
+
* - external `timeout` / `gtimeout` — the Antigravity leg probed for one because
|
|
16
|
+
* `--print-timeout` cannot fire before a session exists, and STOCK MACOS SHIPS NEITHER, so on a
|
|
17
|
+
* stock Mac that leg ran unbounded. `spawnSync`'s native `timeout` is always available, so D7's
|
|
18
|
+
* "skip the probe where no bounding mechanism exists" carve-out is no longer needed.
|
|
19
|
+
*
|
|
20
|
+
* EVERY subprocess call in this file passes `timeout` + `killSignal` + `maxBuffer`. This repo has a
|
|
21
|
+
* named `DEFECT.UNBOUNDED-SUBPROCESS` class (`CONTEXT.md:772`) and an unbounded sync spawn is not a
|
|
22
|
+
* slow test, it is a hung one: `--test-force-exit` cannot interrupt a synchronous call, so a frozen
|
|
23
|
+
* spawn hangs the whole CI chunk to its 10-minute kill with `# fail 0` and no `not ok` (#2099).
|
|
24
|
+
*
|
|
25
|
+
* TOTAL. No function here throws for a lane-level failure; every one returns a typed outcome. A
|
|
26
|
+
* reviewer lane that cannot run must produce a diagnosable artifact, because the ambiguity between
|
|
27
|
+
* "failed" and "ran cleanly with nothing to report" IS the defect this epic closes (#2494/#2605).
|
|
28
|
+
*/
|
|
29
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
30
|
+
exports.checkEgressHost = checkEgressHost;
|
|
31
|
+
exports.probeLane = probeLane;
|
|
32
|
+
exports.writeReviewOrStub = writeReviewOrStub;
|
|
33
|
+
exports.handleOpencodeOutput = handleOpencodeOutput;
|
|
34
|
+
exports.antigravityWatermark = antigravityWatermark;
|
|
35
|
+
exports.antigravityTranscriptFallback = antigravityTranscriptFallback;
|
|
36
|
+
exports.antigravityPrompt = antigravityPrompt;
|
|
37
|
+
exports.antigravityArgv = antigravityArgv;
|
|
38
|
+
exports.antigravityDiagnostic = antigravityDiagnostic;
|
|
39
|
+
exports.stampBlindReview = stampBlindReview;
|
|
40
|
+
exports.runOpenAiCompatible = runOpenAiCompatible;
|
|
41
|
+
exports.runLane = runLane;
|
|
42
|
+
const review_lane_invocation_cjs_1 = require("./review-lane-invocation.cjs");
|
|
43
|
+
/**
|
|
44
|
+
* Compare a lane's re-resolved egress destination against the one the user consented to.
|
|
45
|
+
*
|
|
46
|
+
* ADR-2782 D5 puts this on the invocation path deliberately. `hostConfigKey` names a key in
|
|
47
|
+
* `.planning/config.json`, which is the ONE consent-bound value living outside the SHA-pinned
|
|
48
|
+
* bundle: user- and CI-editable at any time, with no re-install and no integrity check. Without
|
|
49
|
+
* this check a lane consented against `http://localhost:8080` could be silently redirected to a
|
|
50
|
+
* remote host by an ordinary pull request touching a JSON file, and every subsequent review would
|
|
51
|
+
* egress plans, requirements, research and decisions to the new destination with no re-prompt.
|
|
52
|
+
*
|
|
53
|
+
* ABSENCE IS NOT A MISMATCH, and this is the part that must not be "hardened" later by someone who
|
|
54
|
+
* reads only the rule name:
|
|
55
|
+
* - No consent record at all ⇒ ALLOW. First-party lanes (`ollama`, `lm_studio`, `llama_cpp`) ship
|
|
56
|
+
* inside the SHA-pinned distribution and are never consent-gated, so blocking on a missing
|
|
57
|
+
* record would break every existing local-model user on upgrade.
|
|
58
|
+
* - A consent record predating this feature ⇒ ALLOW. Those records were written before a host was
|
|
59
|
+
* recorded; treating the absence as a change would force spurious re-consent across every
|
|
60
|
+
* installed capability, which ADR-2782 D4 rule 5 explicitly forbids.
|
|
61
|
+
*
|
|
62
|
+
* Hosts are compared NORMALIZED, so a trailing slash or an explicit `:80` is not a "destination
|
|
63
|
+
* change". A warning that fires on cosmetic edits is a warning users learn to dismiss.
|
|
64
|
+
*/
|
|
65
|
+
function checkEgressHost(consentedHost, currentHost) {
|
|
66
|
+
const consented = typeof consentedHost === 'string' ? (0, review_lane_invocation_cjs_1.normalizeHost)(consentedHost) : '';
|
|
67
|
+
if (!consented)
|
|
68
|
+
return { allowed: true };
|
|
69
|
+
const current = (0, review_lane_invocation_cjs_1.normalizeHost)(currentHost);
|
|
70
|
+
if (consented === current)
|
|
71
|
+
return { allowed: true, consentedHost: consented, currentHost: current };
|
|
72
|
+
return { allowed: false, consentedHost: consented, currentHost: current };
|
|
73
|
+
}
|
|
74
|
+
/* ------------------------------------------------------------------ *
|
|
75
|
+
* Probe (D7) — every probe bounded, and only for a SELECTED lane
|
|
76
|
+
* ------------------------------------------------------------------ */
|
|
77
|
+
async function probeLane(plan, deps) {
|
|
78
|
+
// Prerequisites first: a missing `jq`-style helper is a clearer failure than whatever the tool
|
|
79
|
+
// does without it, and reporting it costs nothing.
|
|
80
|
+
for (const bin of plan.requiresBinaries) {
|
|
81
|
+
if (!deps.hasBinary(bin)) {
|
|
82
|
+
return {
|
|
83
|
+
available: false,
|
|
84
|
+
reason: review_lane_invocation_cjs_1.LANE_UNAVAILABLE.MISSING_REQUIRED_BINARY,
|
|
85
|
+
detail: `lane '${plan.slug}' requires '${bin}' on PATH`,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const probe = plan.probe;
|
|
90
|
+
if (!probe || typeof probe !== 'object') {
|
|
91
|
+
return { available: false, reason: review_lane_invocation_cjs_1.LANE_UNAVAILABLE.PROBE_FAILED, detail: 'lane declares no probe' };
|
|
92
|
+
}
|
|
93
|
+
switch (probe.kind) {
|
|
94
|
+
case 'command-exists':
|
|
95
|
+
return deps.hasBinary(probe.binary)
|
|
96
|
+
? { available: true }
|
|
97
|
+
: {
|
|
98
|
+
available: false,
|
|
99
|
+
reason: review_lane_invocation_cjs_1.LANE_UNAVAILABLE.MISSING_BINARY,
|
|
100
|
+
detail: `'${probe.binary}' not found on PATH`,
|
|
101
|
+
};
|
|
102
|
+
case 'command-capability': {
|
|
103
|
+
// Existence alone is structurally insufficient here: `kimi` is claimed by BOTH the Kimi Code
|
|
104
|
+
// CLI and the legacy Python kimi-cli, and an existence-only probe registers the wrong tool.
|
|
105
|
+
if (!deps.hasBinary(probe.binary)) {
|
|
106
|
+
return {
|
|
107
|
+
available: false,
|
|
108
|
+
reason: review_lane_invocation_cjs_1.LANE_UNAVAILABLE.MISSING_BINARY,
|
|
109
|
+
detail: `'${probe.binary}' not found on PATH`,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
const out = deps.spawn(probe.binary, ['--help'], { timeoutMs: probe.timeoutMs });
|
|
113
|
+
if (out.errorCode === 'ETIMEDOUT') {
|
|
114
|
+
return {
|
|
115
|
+
available: false,
|
|
116
|
+
reason: review_lane_invocation_cjs_1.LANE_UNAVAILABLE.PROBE_TIMEOUT,
|
|
117
|
+
detail: `'${probe.binary} --help' exceeded ${probe.timeoutMs}ms`,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
const help = `${out.stdout}\n${out.stderr}`;
|
|
121
|
+
return help.includes(probe.needle)
|
|
122
|
+
? { available: true }
|
|
123
|
+
: {
|
|
124
|
+
available: false,
|
|
125
|
+
reason: review_lane_invocation_cjs_1.LANE_UNAVAILABLE.PROBE_FAILED,
|
|
126
|
+
detail: `'${probe.binary}' is on PATH but its --help lacks '${probe.needle}' — this is a different tool sharing the name`,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
case 'http-reachable': {
|
|
130
|
+
// Only a STRING config value names a host. `String(unknown)` would turn an object into the
|
|
131
|
+
// literal '[object Object]' and probe that as a URL — a nonsense destination reported as
|
|
132
|
+
// "unreachable" rather than as the misconfiguration it is.
|
|
133
|
+
const configured = deps.configGet(probe.hostConfigKey);
|
|
134
|
+
const base = (0, review_lane_invocation_cjs_1.normalizeHost)((typeof configured === 'string' ? configured : '') ||
|
|
135
|
+
(plan.transport === 'openai-http' ? plan.host : ''));
|
|
136
|
+
if (!base) {
|
|
137
|
+
return { available: false, reason: review_lane_invocation_cjs_1.LANE_UNAVAILABLE.HOST_UNREACHABLE, detail: 'no host resolved' };
|
|
138
|
+
}
|
|
139
|
+
const r = await deps.httpJson(`${base}${probe.path}`, { method: 'GET', timeoutMs: probe.timeoutMs });
|
|
140
|
+
return r.ok
|
|
141
|
+
? { available: true }
|
|
142
|
+
: {
|
|
143
|
+
available: false,
|
|
144
|
+
reason: review_lane_invocation_cjs_1.LANE_UNAVAILABLE.HOST_UNREACHABLE,
|
|
145
|
+
detail: `${base}${probe.path} unreachable: ${r.error ?? `HTTP ${r.status}`}`,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
default:
|
|
149
|
+
return {
|
|
150
|
+
available: false,
|
|
151
|
+
reason: review_lane_invocation_cjs_1.LANE_UNAVAILABLE.PROBE_FAILED,
|
|
152
|
+
detail: `unknown probe kind '${String(probe.kind)}'`,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/* ------------------------------------------------------------------ *
|
|
157
|
+
* Empty-output policy (#2494 / #2605 / #2794)
|
|
158
|
+
* ------------------------------------------------------------------ */
|
|
159
|
+
/**
|
|
160
|
+
* Write the review, or a diagnostic stub when the lane produced nothing usable.
|
|
161
|
+
*
|
|
162
|
+
* The stub is not cosmetic. Before #2494/#2605 a failed lane left a zero-byte file, `write_reviews`
|
|
163
|
+
* rendered it as "a reviewer that ran cleanly with nothing to report", and the lane vanished from
|
|
164
|
+
* the cross-AI consensus while `present_results` reported success — a review blind in one eye. The
|
|
165
|
+
* stub keeps its "failed or returned empty output" header precisely so it can never be mistaken for
|
|
166
|
+
* a real review.
|
|
167
|
+
*
|
|
168
|
+
* `extraDiagnostics` carries the raw HTTP response body for the OpenAI-compatible lanes: an error
|
|
169
|
+
* from such a server arrives with HTTP 4xx/5xx and the JSON in the BODY, so stderr alone is empty
|
|
170
|
+
* and the body is the only evidence.
|
|
171
|
+
*/
|
|
172
|
+
function writeReviewOrStub(plan, content, deps, extraDiagnostics) {
|
|
173
|
+
if (!(0, review_lane_invocation_cjs_1.isEmptyReview)(content)) {
|
|
174
|
+
deps.writeFile(plan.reviewPath, content.endsWith('\n') ? content : `${content}\n`);
|
|
175
|
+
return { stubbed: false };
|
|
176
|
+
}
|
|
177
|
+
const stderr = deps.exists(plan.errPath) ? deps.readFile(plan.errPath) : '';
|
|
178
|
+
const parts = [`${plan.slug} review failed or returned empty output. stderr:`, stderr];
|
|
179
|
+
if (extraDiagnostics)
|
|
180
|
+
parts.push('Raw response body:', extraDiagnostics);
|
|
181
|
+
deps.writeFile(plan.reviewPath, `${parts.join('\n')}\n`);
|
|
182
|
+
return { stubbed: true };
|
|
183
|
+
}
|
|
184
|
+
/* ------------------------------------------------------------------ *
|
|
185
|
+
* Handlers (D6) — named first-party code, never conditionals in data
|
|
186
|
+
* ------------------------------------------------------------------ */
|
|
187
|
+
/**
|
|
188
|
+
* `opencode` — reconstruct the review from the assistant `text` parts of a `--format json` stream.
|
|
189
|
+
*
|
|
190
|
+
* `--format json` is the PRIMARY invocation, not a fallback: OpenCode's default `build` agent is an
|
|
191
|
+
* agentic coder, and on a large review prompt it may run a few `read` tool calls then end its turn
|
|
192
|
+
* with ZERO output tokens, so `--format default` yields empty stdout and the reviewer is silently
|
|
193
|
+
* lost (#1936). When no assistant text was emitted, surface the stop reason and output-token count
|
|
194
|
+
* so the failure is diagnosable rather than a generic empty stub.
|
|
195
|
+
*
|
|
196
|
+
* The stream is JSONL-ish: one JSON value per line. A line that does not parse is SKIPPED rather
|
|
197
|
+
* than failing the lane — a partial stream still carries usable review text, and losing the whole
|
|
198
|
+
* review to one malformed line would be strictly worse than the bug this handler exists to fix.
|
|
199
|
+
*/
|
|
200
|
+
function handleOpencodeOutput(rawStdout) {
|
|
201
|
+
const texts = [];
|
|
202
|
+
let stopReason = '?';
|
|
203
|
+
let outputTokens = '?';
|
|
204
|
+
for (const line of String(rawStdout ?? '').split(/\r?\n/)) {
|
|
205
|
+
const trimmed = line.trim();
|
|
206
|
+
if (!trimmed)
|
|
207
|
+
continue;
|
|
208
|
+
let evt;
|
|
209
|
+
try {
|
|
210
|
+
evt = JSON.parse(trimmed);
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
if (evt === null || typeof evt !== 'object')
|
|
216
|
+
continue;
|
|
217
|
+
const e = evt;
|
|
218
|
+
const part = (e.part ?? {});
|
|
219
|
+
if (e.type === 'text' && typeof part.text === 'string') {
|
|
220
|
+
// An EMPTY string is kept, not skipped. The shipped jq was `.part.text // empty`, and `//`
|
|
221
|
+
// only substitutes for `false`/`null` — an empty string is truthy in jq, so it survived and
|
|
222
|
+
// contributed a blank line. Dropping it here would quietly close up blank lines between
|
|
223
|
+
// parts, which is a real (if cosmetic) divergence from the behaviour being ported.
|
|
224
|
+
texts.push(part.text);
|
|
225
|
+
}
|
|
226
|
+
else if (e.type === 'step_finish') {
|
|
227
|
+
if (typeof part.reason === 'string')
|
|
228
|
+
stopReason = part.reason;
|
|
229
|
+
const tokens = (part.tokens ?? {});
|
|
230
|
+
if (typeof tokens.output === 'number')
|
|
231
|
+
outputTokens = String(tokens.output);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return {
|
|
235
|
+
review: texts.join('\n'),
|
|
236
|
+
diagnostic: `stop reason=${stopReason}, output tokens=${outputTokens}`,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* `antigravity` — three layers, a two-level timeout, and a stale-response watermark.
|
|
241
|
+
*
|
|
242
|
+
* Layer 1 is stdout, which works on macOS/Linux/WSL. On native Windows `agy -p` silently produces
|
|
243
|
+
* no stdout despite the API call succeeding (an upstream `text_drip.go` non-TTY flush bug), so
|
|
244
|
+
* layer 2 reads the transcript `agy` always persists to disk. Layer 3 is a diagnostic stub.
|
|
245
|
+
*
|
|
246
|
+
* THE WATERMARK IS THE SUBTLE PART. Without it, layer 2 reads the last `PLANNER_RESPONSE` in the
|
|
247
|
+
* transcript regardless of when it was written — including one from a PREVIOUS invocation in the
|
|
248
|
+
* same workspace, silently presenting a stale review as this run's. So the transcript line count is
|
|
249
|
+
* snapshotted BEFORE the spawn and only lines appended after it are considered. If the conversation
|
|
250
|
+
* id changed, `agy` started a fresh session and every line is new (skip 0).
|
|
251
|
+
*
|
|
252
|
+
* `takeWatermark` must therefore be called before `runAntigravity`; the plan's outer timeout is the
|
|
253
|
+
* external wall-clock cap that `--print-timeout` cannot provide (it cannot fire before a session
|
|
254
|
+
* exists — a process can stall pre-session and outlive its own native timeout, #2073 mode 3).
|
|
255
|
+
*
|
|
256
|
+
* KNOWN LIMIT — the watermark is sequential, not concurrent. `last_conversations.json` is keyed by
|
|
257
|
+
* WORKSPACE, so two `/gsd:review` runs against the same repo at the same time resolve the same
|
|
258
|
+
* conversation id and share one transcript. The fallback then takes "the latest DONE
|
|
259
|
+
* PLANNER_RESPONSE after the watermark" with nothing to tell the two runs apart, so one run could
|
|
260
|
+
* read the other's response. This cannot be closed here: `agy` exposes no per-invocation id to
|
|
261
|
+
* filter on, and the transcript carries none. It is stated rather than silently tolerated because
|
|
262
|
+
* the guarantee this function advertises ("never stale") holds only for sequential use, and a
|
|
263
|
+
* future reader deserves to know which half is actually guaranteed.
|
|
264
|
+
*/
|
|
265
|
+
function antigravityWatermark(workspace, deps) {
|
|
266
|
+
const cachePath = `${deps.homeDir}/.gemini/antigravity-cli/cache/last_conversations.json`;
|
|
267
|
+
if (!deps.exists(cachePath))
|
|
268
|
+
return { convId: '', lines: 0 };
|
|
269
|
+
let cache;
|
|
270
|
+
try {
|
|
271
|
+
cache = JSON.parse(deps.readFile(cachePath));
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
return { convId: '', lines: 0 };
|
|
275
|
+
}
|
|
276
|
+
const convId = resolveConvId(cache, workspace);
|
|
277
|
+
if (!convId)
|
|
278
|
+
return { convId: '', lines: 0 };
|
|
279
|
+
const tx = transcriptPath(deps.homeDir, convId);
|
|
280
|
+
if (!deps.exists(tx))
|
|
281
|
+
return { convId, lines: 0 };
|
|
282
|
+
try {
|
|
283
|
+
return { convId, lines: deps.readFile(tx).split(/\r?\n/).filter((l) => l.trim()).length };
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
return { convId, lines: 0 };
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
/** Workspace lookup is case-insensitive — the leg's jq did `ascii_downcase` on both sides. */
|
|
290
|
+
function resolveConvId(cache, workspace) {
|
|
291
|
+
if (Object.prototype.hasOwnProperty.call(cache, workspace)) {
|
|
292
|
+
const direct = cache[workspace];
|
|
293
|
+
if (typeof direct === 'string' && direct)
|
|
294
|
+
return direct;
|
|
295
|
+
}
|
|
296
|
+
const target = workspace.toLowerCase();
|
|
297
|
+
for (const [k, v] of Object.entries(cache)) {
|
|
298
|
+
if (k.toLowerCase() === target && typeof v === 'string' && v)
|
|
299
|
+
return v;
|
|
300
|
+
}
|
|
301
|
+
return '';
|
|
302
|
+
}
|
|
303
|
+
function transcriptPath(homeDir, convId) {
|
|
304
|
+
return `${homeDir}/.gemini/antigravity-cli/brain/${convId}/.system_generated/logs/transcript.jsonl`;
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Layer 2: the newest `PLANNER_RESPONSE` written AFTER the watermark, or `''`.
|
|
308
|
+
*
|
|
309
|
+
* Returning `''` rather than the newest entry overall is the whole point — an empty result lets
|
|
310
|
+
* layer 3 fire with an honest diagnostic, where a stale one would be indistinguishable from a
|
|
311
|
+
* successful review.
|
|
312
|
+
*/
|
|
313
|
+
function antigravityTranscriptFallback(workspace, mark, deps) {
|
|
314
|
+
const cachePath = `${deps.homeDir}/.gemini/antigravity-cli/cache/last_conversations.json`;
|
|
315
|
+
if (!deps.exists(cachePath))
|
|
316
|
+
return '';
|
|
317
|
+
let cache;
|
|
318
|
+
try {
|
|
319
|
+
cache = JSON.parse(deps.readFile(cachePath));
|
|
320
|
+
}
|
|
321
|
+
catch {
|
|
322
|
+
return '';
|
|
323
|
+
}
|
|
324
|
+
const convId = resolveConvId(cache, workspace);
|
|
325
|
+
if (!convId)
|
|
326
|
+
return '';
|
|
327
|
+
const tx = transcriptPath(deps.homeDir, convId);
|
|
328
|
+
if (!deps.exists(tx))
|
|
329
|
+
return '';
|
|
330
|
+
let lines;
|
|
331
|
+
try {
|
|
332
|
+
lines = deps.readFile(tx).split(/\r?\n/).filter((l) => l.trim());
|
|
333
|
+
}
|
|
334
|
+
catch {
|
|
335
|
+
return '';
|
|
336
|
+
}
|
|
337
|
+
// Same conv-id ⇒ only lines beyond the watermark are this run's. Different id ⇒ fresh session,
|
|
338
|
+
// so every line is new.
|
|
339
|
+
const skip = convId === mark.convId ? mark.lines : 0;
|
|
340
|
+
let latest = '';
|
|
341
|
+
for (const line of lines.slice(skip)) {
|
|
342
|
+
let entry;
|
|
343
|
+
try {
|
|
344
|
+
entry = JSON.parse(line);
|
|
345
|
+
}
|
|
346
|
+
catch {
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
if (entry.source === 'MODEL' &&
|
|
350
|
+
entry.status === 'DONE' &&
|
|
351
|
+
entry.type === 'PLANNER_RESPONSE' &&
|
|
352
|
+
typeof entry.content === 'string') {
|
|
353
|
+
latest = entry.content;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return latest;
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Antigravity's prompt variant (#2176).
|
|
360
|
+
*
|
|
361
|
+
* Differs from the standard `argv-file-ref` text by one clause, and that clause is load-bearing:
|
|
362
|
+
* it REQUIRES the reviewer to self-report when it cannot read the repo. Without it a blind review
|
|
363
|
+
* is indistinguishable from a grounded one — the reviewer happily reviews the plan text in
|
|
364
|
+
* isolation and its verdict is counted at full weight in the consensus. `stampBlindReview` reads
|
|
365
|
+
* this self-report back out.
|
|
366
|
+
*/
|
|
367
|
+
function antigravityPrompt(promptPath, repoRoot) {
|
|
368
|
+
return (`Read the file at ${promptPath} in full and carry out the review request it contains. ` +
|
|
369
|
+
`The repository under review is at ${repoRoot} — resolve every relative file path in the ` +
|
|
370
|
+
`review request against that absolute root and verify claims against those files. ` +
|
|
371
|
+
`If you cannot read files under ${repoRoot}, begin your output with the exact line ` +
|
|
372
|
+
`REVIEWED-WITHOUT-REPO-ACCESS before the review. Output only the resulting markdown review. ` +
|
|
373
|
+
`Do not edit any files.`);
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Antigravity's argv adjustments — the two things data cannot express for this lane.
|
|
377
|
+
*
|
|
378
|
+
* 1. `--add-dir <repo>`, CAPABILITY-PROBED. Without it agy's permission context never receives the
|
|
379
|
+
* cwd repo: the agent anchors on its own `~/.gemini/antigravity-cli/scratch` dir and reviews the
|
|
380
|
+
* plan text in isolation, which is exactly what the Review Instructions forbid (#2176). It is
|
|
381
|
+
* probed rather than assumed because older agy builds reject the unknown flag outright, and a
|
|
382
|
+
* lane that fails to start is worse than one that runs on the prompt anchor alone.
|
|
383
|
+
* 2. The self-report prompt variant above, swapped in for the standard file-ref text.
|
|
384
|
+
*
|
|
385
|
+
* Both are argv shape, so they belong here rather than in the descriptor: expressing "add this flag
|
|
386
|
+
* only if the binary's --help mentions it" as data would need a conditional, which is precisely
|
|
387
|
+
* what the named-handler seam exists to absorb (ADR-2782 D6).
|
|
388
|
+
*/
|
|
389
|
+
function antigravityArgv(argv, promptPath, repoRoot, deps) {
|
|
390
|
+
const standard = (0, review_lane_invocation_cjs_1.fileRefPrompt)(promptPath, repoRoot);
|
|
391
|
+
const out = argv.map((a) => (a === standard ? antigravityPrompt(promptPath, repoRoot) : a));
|
|
392
|
+
let supportsAddDir = false;
|
|
393
|
+
try {
|
|
394
|
+
const help = deps.spawn('agy', ['--help'], { timeoutMs: 5_000 });
|
|
395
|
+
supportsAddDir = `${help.stdout}\n${help.stderr}`.includes('--add-dir');
|
|
396
|
+
}
|
|
397
|
+
catch {
|
|
398
|
+
supportsAddDir = false;
|
|
399
|
+
}
|
|
400
|
+
if (!supportsAddDir)
|
|
401
|
+
return out;
|
|
402
|
+
// Insert before the trailing `-p <prompt>` pair so the prompt stays last, as the leg had it.
|
|
403
|
+
const pIdx = out.lastIndexOf('-p');
|
|
404
|
+
if (pIdx === -1)
|
|
405
|
+
return [...out, '--add-dir', repoRoot];
|
|
406
|
+
return [...out.slice(0, pIdx), '--add-dir', repoRoot, ...out.slice(pIdx)];
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Layer 3's diagnostic: what `agy` itself logged, when both stdout and the transcript were empty.
|
|
410
|
+
*
|
|
411
|
+
* #2073 mode 2 is invisible without this. A pinned model that 404s server-side exits **0** with
|
|
412
|
+
* empty stdout AND an empty transcript — every signal the other two layers read says "clean run,
|
|
413
|
+
* nothing to report". The only evidence is in `agy`'s own log, so a bare generic stub would leave
|
|
414
|
+
* the user with a silently missing reviewer and nothing to diagnose.
|
|
415
|
+
*
|
|
416
|
+
* Mode 3 (a pre-session stall, which `--print-timeout` cannot bound because it cannot fire before a
|
|
417
|
+
* session exists) leaves no log line at all, so its tell is stated rather than searched for.
|
|
418
|
+
*/
|
|
419
|
+
function antigravityDiagnostic(deps) {
|
|
420
|
+
const lines = [
|
|
421
|
+
'Antigravity review failed or returned empty output.',
|
|
422
|
+
];
|
|
423
|
+
const logPath = `${deps.homeDir}/.gemini/antigravity-cli/cli.log`;
|
|
424
|
+
if (deps.exists(logPath)) {
|
|
425
|
+
try {
|
|
426
|
+
const hits = deps
|
|
427
|
+
.readFile(logPath)
|
|
428
|
+
.split(/\r?\n/)
|
|
429
|
+
.filter((l) => /agent executor error|NOT_FOUND|Publisher model/i.test(l))
|
|
430
|
+
.slice(-3);
|
|
431
|
+
if (hits.length) {
|
|
432
|
+
lines.push("agy log hint (pinned model may be unavailable — run 'agy models' and set review.models.agy):", ...hits);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
catch {
|
|
436
|
+
/* an unreadable log is not worth failing the lane over */
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
lines.push('If no agy run started, that is the pre-session-stall case: check whether a new ' +
|
|
440
|
+
'~/.gemini/antigravity-cli/brain/<conv-id>/ dir appeared within ~30s of launch.');
|
|
441
|
+
return lines.join('\n');
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Stamp a machine-readable marker when the reviewer plainly ran without repo access (#2176).
|
|
445
|
+
*
|
|
446
|
+
* BOTH tells are ANCHORED, and that anchoring is load-bearing: a grounded review that merely QUOTES
|
|
447
|
+
* `REVIEWED-WITHOUT-REPO-ACCESS` — reviewing this very file, say — must never be mis-stamped. So
|
|
448
|
+
* the self-report is only honoured in the first five lines, and the scratch-dir tell only in a
|
|
449
|
+
* workspace-DECLARATION phrasing.
|
|
450
|
+
*/
|
|
451
|
+
function stampBlindReview(review) {
|
|
452
|
+
if ((0, review_lane_invocation_cjs_1.isEmptyReview)(review))
|
|
453
|
+
return review;
|
|
454
|
+
const head = review.split(/\r?\n/).slice(0, 5).join('\n');
|
|
455
|
+
const selfReported = head.includes('REVIEWED-WITHOUT-REPO-ACCESS');
|
|
456
|
+
const scratchTell = /(workspace|working) (directory|dir)[\s\S]{0,40}antigravity-cli\/scratch/i.test(review);
|
|
457
|
+
if (!selfReported && !scratchTell)
|
|
458
|
+
return review;
|
|
459
|
+
return ('> [reviewed-without-repo-access] This reviewer ran without visibility into the repo under ' +
|
|
460
|
+
'review — down-weight its verdict in the Consensus Summary.\n\n' +
|
|
461
|
+
review);
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* `openai-compatible` — model discovery, the chat-completions round trip, and the served-model
|
|
465
|
+
* mismatch warning.
|
|
466
|
+
*
|
|
467
|
+
* The raw body is returned alongside the content because an OpenAI-compatible server reports errors
|
|
468
|
+
* with an HTTP 4xx/5xx and the JSON in the BODY. The bash piped the response straight into `jq`,
|
|
469
|
+
* which discarded exactly that evidence; the stub appends it now.
|
|
470
|
+
*/
|
|
471
|
+
async function runOpenAiCompatible(plan, promptText, deps) {
|
|
472
|
+
let model = plan.model;
|
|
473
|
+
if (!model && plan.modelsUrl) {
|
|
474
|
+
const listed = await deps.httpJson(plan.modelsUrl, { method: 'GET', timeoutMs: 2_000 });
|
|
475
|
+
if (listed.ok) {
|
|
476
|
+
try {
|
|
477
|
+
const parsed = JSON.parse(listed.body);
|
|
478
|
+
const first = Array.isArray(parsed.data) ? parsed.data[0] : undefined;
|
|
479
|
+
if (first && typeof first.id === 'string' && first.id)
|
|
480
|
+
model = first.id;
|
|
481
|
+
}
|
|
482
|
+
catch {
|
|
483
|
+
/* fall through to the declared fallback */
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
if (!model)
|
|
488
|
+
model = plan.fallbackModel;
|
|
489
|
+
const body = JSON.stringify({ model, messages: [{ role: 'user', content: promptText }] });
|
|
490
|
+
const res = await deps.httpJson(plan.url, { method: 'POST', body, timeoutMs: plan.timeoutMs });
|
|
491
|
+
if (!res.ok && !res.body) {
|
|
492
|
+
return { review: '', rawBody: res.error ?? `HTTP ${res.status}` };
|
|
493
|
+
}
|
|
494
|
+
let review = '';
|
|
495
|
+
try {
|
|
496
|
+
const parsed = JSON.parse(res.body);
|
|
497
|
+
// A server that quietly serves a different model than requested produces a review the user
|
|
498
|
+
// will attribute to the wrong system. Warn rather than fail — the review is still real.
|
|
499
|
+
if (typeof parsed.model === 'string' && parsed.model && parsed.model !== model) {
|
|
500
|
+
deps.warn(`${plan.slug} served model '${parsed.model}' but '${model}' was requested. ` +
|
|
501
|
+
`Review may be from a different model.`);
|
|
502
|
+
}
|
|
503
|
+
const content = parsed.choices?.[0]?.message?.content;
|
|
504
|
+
if (typeof content === 'string')
|
|
505
|
+
review = content;
|
|
506
|
+
}
|
|
507
|
+
catch {
|
|
508
|
+
/* leave review empty — the raw body carries the diagnosis */
|
|
509
|
+
}
|
|
510
|
+
return { review, rawBody: res.body };
|
|
511
|
+
}
|
|
512
|
+
/* ------------------------------------------------------------------ *
|
|
513
|
+
* Orchestration
|
|
514
|
+
* ------------------------------------------------------------------ */
|
|
515
|
+
/**
|
|
516
|
+
* Run one resolved lane end to end.
|
|
517
|
+
*
|
|
518
|
+
* Order is deliberate: egress re-verification happens BEFORE the probe and before any spawn, so a
|
|
519
|
+
* lane whose destination changed never receives the plan text even once.
|
|
520
|
+
*/
|
|
521
|
+
async function runLane(plan, deps, opts) {
|
|
522
|
+
const base = { slug: plan.slug, stubbed: false };
|
|
523
|
+
if (plan.transport === 'openai-http') {
|
|
524
|
+
const egress = checkEgressHost(opts.consentedHost, plan.host);
|
|
525
|
+
if (!egress.allowed) {
|
|
526
|
+
const detail = `lane '${plan.slug}' was consented to send plans to ${egress.consentedHost} but ` +
|
|
527
|
+
`${plan.hostConfigKey} now resolves to ${egress.currentHost}. Re-consent to allow the new ` +
|
|
528
|
+
`destination — this lane will not run against a host you did not approve.`;
|
|
529
|
+
deps.warn(detail);
|
|
530
|
+
return { ...base, ok: false, reason: review_lane_invocation_cjs_1.LANE_UNAVAILABLE.EGRESS_HOST_CHANGED, detail };
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
const probed = await probeLane(plan, deps);
|
|
534
|
+
if (!probed.available) {
|
|
535
|
+
// D4's carve-out: not finding a lane nobody asked for is normal; failing to run a lane somebody
|
|
536
|
+
// asked for is an ERROR. Both are visible; only the second is fatal to the run.
|
|
537
|
+
if (opts.explicitlyRequested)
|
|
538
|
+
deps.warn(`explicitly requested reviewer '${plan.slug}': ${probed.detail}`);
|
|
539
|
+
return { ...base, ok: false, reason: probed.reason, detail: probed.detail };
|
|
540
|
+
}
|
|
541
|
+
return plan.transport === 'spawn'
|
|
542
|
+
? runSpawnLane(plan, deps, opts.repoRoot)
|
|
543
|
+
: runHttpLane(plan, deps);
|
|
544
|
+
}
|
|
545
|
+
function runSpawnLane(plan, deps, repoRoot) {
|
|
546
|
+
const input = plan.stdin && deps.exists(plan.stdin) ? deps.readFile(plan.stdin) : undefined;
|
|
547
|
+
const mark = plan.handler === 'antigravity' ? antigravityWatermark(repoRoot, deps) : { convId: '', lines: 0 };
|
|
548
|
+
const argv = plan.handler === 'antigravity'
|
|
549
|
+
? antigravityArgv(plan.argv, plan.promptPath, repoRoot, deps)
|
|
550
|
+
: plan.argv;
|
|
551
|
+
const out = deps.spawn(plan.binary, argv, { input, timeoutMs: plan.timeoutMs });
|
|
552
|
+
deps.writeFile(plan.errPath, out.stderr ?? '');
|
|
553
|
+
// `file-arg` lanes write the review themselves and their stdout is deliberately discarded (#1698).
|
|
554
|
+
let review = plan.outputTarget.kind === 'file'
|
|
555
|
+
? deps.exists(plan.outputTarget.path)
|
|
556
|
+
? deps.readFile(plan.outputTarget.path)
|
|
557
|
+
: ''
|
|
558
|
+
: (out.stdout ?? '');
|
|
559
|
+
let extra;
|
|
560
|
+
if (plan.handler === 'opencode') {
|
|
561
|
+
const rebuilt = handleOpencodeOutput(review);
|
|
562
|
+
if (!(0, review_lane_invocation_cjs_1.isEmptyReview)(rebuilt.review)) {
|
|
563
|
+
review = rebuilt.review;
|
|
564
|
+
}
|
|
565
|
+
else {
|
|
566
|
+
review = '';
|
|
567
|
+
extra = `OpenCode review returned no assistant text (#1936: agent ended its turn with no final message).\nDiagnostic: ${rebuilt.diagnostic}`;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
if (plan.handler === 'antigravity') {
|
|
571
|
+
// A non-zero exit (timeout kill, crash) discards partial output so the transcript fallback and
|
|
572
|
+
// the diagnostic stub can take over.
|
|
573
|
+
if (out.status !== 0)
|
|
574
|
+
review = '';
|
|
575
|
+
if ((0, review_lane_invocation_cjs_1.isEmptyReview)(review))
|
|
576
|
+
review = antigravityTranscriptFallback(repoRoot, mark, deps);
|
|
577
|
+
review = stampBlindReview(review);
|
|
578
|
+
// Layer 3. `emptyOutput: 'handler-owned'` means the generic stub does not fire for this lane,
|
|
579
|
+
// so if nothing is written here the lane goes out empty — the #2073 failure itself.
|
|
580
|
+
if ((0, review_lane_invocation_cjs_1.isEmptyReview)(review)) {
|
|
581
|
+
deps.writeFile(plan.reviewPath, `${antigravityDiagnostic(deps)}\n`);
|
|
582
|
+
return { slug: plan.slug, ok: true, stubbed: true };
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
const { stubbed } = writeReviewOrStub(plan, review, deps, extra);
|
|
586
|
+
return { slug: plan.slug, ok: true, stubbed };
|
|
587
|
+
}
|
|
588
|
+
async function runHttpLane(plan, deps) {
|
|
589
|
+
const promptText = deps.exists(plan.promptPath) ? deps.readFile(plan.promptPath) : '';
|
|
590
|
+
const { review, rawBody } = await runOpenAiCompatible(plan, promptText, deps);
|
|
591
|
+
deps.writeFile(plan.errPath, '');
|
|
592
|
+
const { stubbed } = writeReviewOrStub(plan, review, deps, rawBody);
|
|
593
|
+
return { slug: plan.slug, ok: true, stubbed };
|
|
594
|
+
}
|