@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.
Files changed (177) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.opencode/plugins/gsd-core.js +31 -1
  4. package/agents/gsd-code-fixer.md +107 -34
  5. package/agents/gsd-codebase-mapper.md +1 -1
  6. package/agents/gsd-debug-session-manager.md +36 -0
  7. package/agents/gsd-executor.md +20 -7
  8. package/agents/gsd-intel-updater.md +3 -3
  9. package/agents/gsd-phase-researcher.md +4 -2
  10. package/agents/gsd-plan-checker.md +20 -0
  11. package/agents/gsd-planner.md +15 -23
  12. package/agents/gsd-project-researcher.md +2 -2
  13. package/agents/gsd-ui-auditor.md +0 -40
  14. package/bin/install.js +236 -107
  15. package/commands/gsd/plan-review-convergence.md +5 -1
  16. package/gsd-core/bin/gsd-tools.cjs +882 -4
  17. package/gsd-core/bin/lib/api-coverage.cjs +22 -8
  18. package/gsd-core/bin/lib/audit.cjs +8 -8
  19. package/gsd-core/bin/lib/capability-consent.cjs +40 -1
  20. package/gsd-core/bin/lib/capability-lifecycle.cjs +58 -0
  21. package/gsd-core/bin/lib/capability-loader.cjs +23 -1
  22. package/gsd-core/bin/lib/capability-registry.cjs +1353 -132
  23. package/gsd-core/bin/lib/capability-trust.cjs +468 -33
  24. package/gsd-core/bin/lib/capability-validator.cjs +882 -6
  25. package/gsd-core/bin/lib/check-command-router.cjs +12 -2
  26. package/gsd-core/bin/lib/cjs-command-router-adapter.cjs +15 -0
  27. package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +102 -12
  28. package/gsd-core/bin/lib/claude-orchestration.cjs +125 -22
  29. package/gsd-core/bin/lib/commands.cjs +246 -18
  30. package/gsd-core/bin/lib/config-loader.cjs +200 -28
  31. package/gsd-core/bin/lib/config.cjs +90 -5
  32. package/gsd-core/bin/lib/estimate-cli.cjs +336 -0
  33. package/gsd-core/bin/lib/frontmatter.cjs +125 -15
  34. package/gsd-core/bin/lib/host-integration.cjs +215 -8
  35. package/gsd-core/bin/lib/init.cjs +44 -19
  36. package/gsd-core/bin/lib/install-engine.cjs +1 -0
  37. package/gsd-core/bin/lib/milestone.cjs +36 -9
  38. package/gsd-core/bin/lib/model-catalog.cjs +51 -1
  39. package/gsd-core/bin/lib/observability/logger.cjs +7 -2
  40. package/gsd-core/bin/lib/phase-command-router.cjs +10 -1
  41. package/gsd-core/bin/lib/phase-estimation.cjs +398 -0
  42. package/gsd-core/bin/lib/phase-id.cjs +278 -5
  43. package/gsd-core/bin/lib/phase.cjs +61 -6
  44. package/gsd-core/bin/lib/plan-drift-guard.cjs +1 -1
  45. package/gsd-core/bin/lib/plan-scan.cjs +1 -1
  46. package/gsd-core/bin/lib/planning-workspace.cjs +9 -2
  47. package/gsd-core/bin/lib/profile-output.cjs +34 -8
  48. package/gsd-core/bin/lib/project-root.cjs +48 -0
  49. package/gsd-core/bin/lib/review-lane-descriptor.cjs +927 -0
  50. package/gsd-core/bin/lib/review-lane-invocation.cjs +348 -0
  51. package/gsd-core/bin/lib/review-lane-runner.cjs +594 -0
  52. package/gsd-core/bin/lib/review-reviewer-selection.cjs +114 -32
  53. package/gsd-core/bin/lib/roadmap-parser.cjs +54 -6
  54. package/gsd-core/bin/lib/roadmap.cjs +10 -4
  55. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +31 -4
  56. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +1 -1
  57. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +140 -0
  58. package/gsd-core/bin/lib/runtime-name-policy.cjs +15 -2
  59. package/gsd-core/bin/lib/smart-entry.cjs +1 -1
  60. package/gsd-core/bin/lib/state-document.cjs +164 -20
  61. package/gsd-core/bin/lib/state-transition.cjs +28 -10
  62. package/gsd-core/bin/lib/state.cjs +141 -21
  63. package/gsd-core/bin/lib/uat-predicate.cjs +6 -4
  64. package/gsd-core/bin/lib/uat.cjs +9 -7
  65. package/gsd-core/bin/lib/ui-consideration-probe.cjs +2 -2
  66. package/gsd-core/bin/lib/unusable-input.cjs +216 -0
  67. package/gsd-core/bin/lib/validate.cjs +32 -0
  68. package/gsd-core/bin/lib/verification.cjs +51 -14
  69. package/gsd-core/bin/lib/verify.cjs +146 -22
  70. package/gsd-core/bin/lib/worktree-safety.cjs +360 -15
  71. package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
  72. package/gsd-core/bin/shared/config-schema.manifest.json +1 -13
  73. package/gsd-core/bin/shared/model-catalog.json +5 -0
  74. package/gsd-core/bin/shared/runtime-aliases.manifest.json +5 -0
  75. package/gsd-core/references/context-budget.md +40 -0
  76. package/gsd-core/references/gate-prompts.md +6 -3
  77. package/gsd-core/references/model-profile-resolution.md +64 -13
  78. package/gsd-core/references/offer-next.md +88 -0
  79. package/gsd-core/references/planning-config.md +2 -1
  80. package/gsd-core/references/reviewer-instances.md +28 -21
  81. package/gsd-core/references/runtime-aware-dispatch.md +42 -0
  82. package/gsd-core/references/ui-consideration-probe.md +2 -2
  83. package/gsd-core/references/worktree-branch-check.md +4 -4
  84. package/gsd-core/templates/summary-minimal.md +4 -0
  85. package/gsd-core/templates/summary-standard.md +4 -0
  86. package/gsd-core/templates/summary.md +7 -0
  87. package/gsd-core/workflows/ai-integration-phase.md +4 -4
  88. package/gsd-core/workflows/audit-fix.md +4 -0
  89. package/gsd-core/workflows/audit-milestone.md +8 -0
  90. package/gsd-core/workflows/autonomous.md +19 -15
  91. package/gsd-core/workflows/check-todos.md +2 -2
  92. package/gsd-core/workflows/code-review-fix.md +14 -6
  93. package/gsd-core/workflows/code-review.md +93 -21
  94. package/gsd-core/workflows/debug.md +10 -2
  95. package/gsd-core/workflows/diagnose-issues.md +4 -0
  96. package/gsd-core/workflows/discuss-phase/modes/advisor.md +2 -4
  97. package/gsd-core/workflows/discuss-phase/modes/auto.md +0 -6
  98. package/gsd-core/workflows/discuss-phase-assumptions.md +15 -9
  99. package/gsd-core/workflows/discuss-phase.md +2 -2
  100. package/gsd-core/workflows/docs-update.md +8 -0
  101. package/gsd-core/workflows/eval-review.md +1 -1
  102. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +4 -0
  103. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +160 -0
  104. package/gsd-core/workflows/execute-phase.md +85 -115
  105. package/gsd-core/workflows/execute-plan.md +5 -4
  106. package/gsd-core/workflows/explore.md +4 -0
  107. package/gsd-core/workflows/extract-learnings.md +21 -0
  108. package/gsd-core/workflows/help/modes/full.md +3 -3
  109. package/gsd-core/workflows/import.md +4 -1
  110. package/gsd-core/workflows/ingest-docs.md +4 -0
  111. package/gsd-core/workflows/map-codebase.md +13 -6
  112. package/gsd-core/workflows/new-milestone.md +10 -2
  113. package/gsd-core/workflows/new-project.md +11 -4
  114. package/gsd-core/workflows/next.md +5 -2
  115. package/gsd-core/workflows/plan-phase.md +42 -46
  116. package/gsd-core/workflows/plan-review-convergence.md +18 -14
  117. package/gsd-core/workflows/progress.md +1 -1
  118. package/gsd-core/workflows/quick.md +14 -3
  119. package/gsd-core/workflows/review.md +146 -575
  120. package/gsd-core/workflows/scan.md +9 -1
  121. package/gsd-core/workflows/secure-phase.md +10 -2
  122. package/gsd-core/workflows/ship.md +41 -11
  123. package/gsd-core/workflows/smart-entry.md +1 -1
  124. package/gsd-core/workflows/ui-phase.md +8 -1
  125. package/gsd-core/workflows/ui-review.md +8 -1
  126. package/gsd-core/workflows/update.md +104 -5
  127. package/gsd-core/workflows/validate-phase.md +10 -2
  128. package/gsd-core/workflows/verify-work.md +8 -1
  129. package/hooks/dist/gsd-cursor-session-start.js +6 -2
  130. package/hooks/dist/gsd-cursor-stop.js +6 -2
  131. package/hooks/dist/gsd-cursor-subagent-start.js +6 -2
  132. package/hooks/dist/gsd-graphify-update.sh +9 -0
  133. package/hooks/dist/gsd-phase-boundary.sh +14 -2
  134. package/hooks/dist/gsd-prompt-guard.js +101 -2
  135. package/hooks/dist/gsd-read-guard.js +100 -2
  136. package/hooks/dist/gsd-read-injection-scanner.js +109 -2
  137. package/hooks/dist/gsd-statusline.js +9 -6
  138. package/hooks/dist/gsd-workflow-guard.js +110 -6
  139. package/hooks/dist/gsd-worktree-path-guard.js +132 -8
  140. package/hooks/dist/lib/cursor-workspace.js +74 -0
  141. package/hooks/gsd-cursor-session-start.js +6 -2
  142. package/hooks/gsd-cursor-stop.js +6 -2
  143. package/hooks/gsd-cursor-subagent-start.js +6 -2
  144. package/hooks/gsd-graphify-update.sh +9 -0
  145. package/hooks/gsd-phase-boundary.sh +14 -2
  146. package/hooks/gsd-prompt-guard.js +101 -2
  147. package/hooks/gsd-read-guard.js +100 -2
  148. package/hooks/gsd-read-injection-scanner.js +109 -2
  149. package/hooks/gsd-statusline.js +9 -6
  150. package/hooks/gsd-workflow-guard.js +110 -6
  151. package/hooks/gsd-worktree-path-guard.js +132 -8
  152. package/hooks/lib/cursor-workspace.js +74 -0
  153. package/package.json +7 -7
  154. package/pi/gsd.cjs +26 -1
  155. package/scripts/check-coverage-gate.cjs +51 -0
  156. package/scripts/check-glossary-refs.cjs +24 -0
  157. package/scripts/ci-test-scope.cjs +67 -17
  158. package/scripts/gen-adr-index.cjs +6 -4
  159. package/scripts/gen-capability-matrix.cjs +26 -2
  160. package/scripts/gen-capability-registry.cjs +132 -34
  161. package/scripts/gen-emitted-baseline.cjs +145 -0
  162. package/scripts/gen-registry.cjs +39 -15
  163. package/scripts/lint-compiled-artifact-sync.cjs +146 -0
  164. package/scripts/lint-emitted-drift-ack.cjs +149 -0
  165. package/scripts/lint-fix-has-regression-test.cjs +131 -0
  166. package/scripts/lint-resolution-provenance.cjs +9 -0
  167. package/scripts/mutation-matrix.cjs +4 -0
  168. package/scripts/prompt-injection-scan.sh +6 -0
  169. package/scripts/registry-schema.cjs +372 -94
  170. package/scripts/release-notes/conventional-title.cjs +19 -1
  171. package/scripts/release-notes/format-github-release-notes.cjs +7 -3
  172. package/scripts/validate-registry.cjs +10 -6
  173. package/scripts/workflow-size.cjs +16 -8
  174. package/skills/gsd-plan-review-convergence/SKILL.md +5 -1
  175. package/vscode/package.json +1 -1
  176. package/scripts/gen-golden-install-parity-zcode.cjs +0 -77
  177. package/scripts/update-size-baseline.cjs +0 -68
@@ -0,0 +1,348 @@
1
+ "use strict";
2
+ /**
3
+ * Reviewer Lane Invocation Module (ADR-2782 Phase 5b, #2799 — closes #2718).
4
+ *
5
+ * Turns a DECLARED lane (`review-lane-descriptor.cts`) plus resolved configuration into a concrete,
6
+ * inspectable INVOCATION PLAN. Phase 1's module declares; this one resolves; `review-lane-runner`
7
+ * executes. The split exists so the interesting half is pure: a plan is a value, so the twelve
8
+ * shipped lanes can be asserted against a golden table without spawning anything.
9
+ *
10
+ * WHY A GOLDEN TABLE AND NOT A FRESH DESIGN (Gall's Law). The 640 lines of hand-authored bash this
11
+ * replaces is the simple system that worked, and every leg encodes a hard-won fix — #2494 and #2605
12
+ * (empty output), #1698 (Codex stdout teardown noise), #1936 (OpenCode zero-output turns), #2073
13
+ * (Antigravity's three modes), #2176 (repo-root anchoring), #2589 (no jq on stock Windows), #2794
14
+ * (Qwen's missing sidecar). A resolver designed from the descriptor TYPES would throw that away and
15
+ * rebuild the bugs. So each lane's plan was derived from its leg, and `tests/review-lane-invocation`
16
+ * asserts all twelve against a frozen table. Old and new cannot literally run in parallel, so that
17
+ * table is the strangler-fig substitute — it is what makes this cutover safe rather than hopeful.
18
+ *
19
+ * PURE. No filesystem, no network, no subprocess, no clock. Configuration arrives through the
20
+ * `configGet` seam so a test drives it with a plain object and production wires it to the real
21
+ * resolved config. Every function here is total: a malformed lane yields an `unavailable` result,
22
+ * never a throw — a resolver that throws on bad input cannot report on it, and this runs against
23
+ * third-party overlay manifests (ADR-2782 D4).
24
+ */
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.LANE_UNAVAILABLE = void 0;
27
+ exports.normalizeHost = normalizeHost;
28
+ exports.isEmptyReview = isEmptyReview;
29
+ exports.fileRefPrompt = fileRefPrompt;
30
+ exports.resolveLanePlan = resolveLanePlan;
31
+ const review_lane_descriptor_cjs_1 = require("./review-lane-descriptor.cjs");
32
+ /* ------------------------------------------------------------------ *
33
+ * Unavailability — a frozen enum, because the reason is the product
34
+ * ------------------------------------------------------------------ */
35
+ /**
36
+ * Why a lane will not run.
37
+ *
38
+ * Frozen and exhaustive because the bash it replaces had exactly one outcome for every failure —
39
+ * an empty file — and that ambiguity IS the defect class this epic exists to close (#2494/#2605: a
40
+ * failed lane was indistinguishable from a reviewer that ran cleanly with nothing to report). The
41
+ * caller renders these; tests assert on them. Never assert on the rendered prose.
42
+ *
43
+ * Adding a member is three coordinated changes: this enum, the emitting site, and the test locking
44
+ * `Object.keys(...).sort()`.
45
+ */
46
+ exports.LANE_UNAVAILABLE = Object.freeze({
47
+ MALFORMED_LANE: 'malformed_lane',
48
+ UNKNOWN_HANDLER: 'unknown_handler',
49
+ UNKNOWN_TRANSPORT: 'unknown_transport',
50
+ MISSING_BINARY: 'missing_binary',
51
+ MISSING_REQUIRED_BINARY: 'missing_required_binary',
52
+ PROBE_FAILED: 'probe_failed',
53
+ PROBE_TIMEOUT: 'probe_timeout',
54
+ HOST_UNREACHABLE: 'host_unreachable',
55
+ EGRESS_HOST_CHANGED: 'egress_host_changed',
56
+ BUDGET_TOO_SMALL: 'budget_too_small',
57
+ BUDGET_TOOL_FAILED: 'budget_tool_failed',
58
+ });
59
+ /** Every handler name the runner can dispatch. Mirrors `LaneHandler` (D6's closed enum). */
60
+ const KNOWN_HANDLERS = new Set(['antigravity', 'openai-compatible', 'opencode']);
61
+ /* ------------------------------------------------------------------ *
62
+ * Value normalization — the stringly-typed edges the bash lived with
63
+ * ------------------------------------------------------------------ */
64
+ /**
65
+ * A configured string value, or `null` when effectively unset.
66
+ *
67
+ * Four shapes all mean "not configured", and the bash had to handle three of them by hand:
68
+ * - absent / `null` / `undefined`;
69
+ * - `""` — the declared default of every federated `review.models.*` key;
70
+ * - `"null"` — the LITERAL four characters `config-get --raw` prints for a missing key, which
71
+ * every leg tested for (`[ "$X" != "null" ]`). Reading config in-process removes the source of
72
+ * this, but a `.planning/config.json` written by an older workflow can still contain it;
73
+ * - whitespace-only — never a meaningful model name or host.
74
+ *
75
+ * A non-string (number, bool, object, array) is NOT coerced. `String(0)` would put `"0"` into argv
76
+ * as a model name; a wrong model silently reviewed is worse than no model override.
77
+ */
78
+ function configString(raw) {
79
+ if (typeof raw !== 'string')
80
+ return null;
81
+ const trimmed = raw.trim();
82
+ if (trimmed === '' || trimmed === 'null' || trimmed === 'undefined')
83
+ return null;
84
+ return trimmed;
85
+ }
86
+ /**
87
+ * Normalize a base URL for storage and for the D5 consent comparison.
88
+ *
89
+ * Trailing slash, case in the scheme/host, and an explicit default port are all the same
90
+ * destination. Without normalizing, a cosmetic `.planning/config.json` edit — adding a trailing
91
+ * slash — would read as "the egress destination changed" and block the lane, training the user to
92
+ * dismiss the one warning that matters.
93
+ *
94
+ * Returns the input trimmed when it is not parseable as a URL: an unparseable host is compared
95
+ * verbatim rather than silently rewritten.
96
+ */
97
+ function normalizeHost(raw) {
98
+ const trimmed = String(raw ?? '').trim();
99
+ if (!trimmed)
100
+ return '';
101
+ let u;
102
+ try {
103
+ u = new URL(trimmed);
104
+ }
105
+ catch {
106
+ return trimmed.replace(/\/+$/, '');
107
+ }
108
+ // `new URL('localhost:11434')` PARSES — protocol `localhost:`, empty hostname — so a plausible
109
+ // but scheme-less config value would otherwise be rewritten to `localhost://11434` and compared
110
+ // (and requested) as though it were a real destination. No hostname means this is not a URL;
111
+ // return it verbatim so it fails visibly rather than silently becoming something else.
112
+ if (!u.hostname)
113
+ return trimmed.replace(/\/+$/, '');
114
+ const scheme = u.protocol.toLowerCase();
115
+ const isDefaultPort = (scheme === 'http:' && u.port === '80') || (scheme === 'https:' && u.port === '443');
116
+ const port = isDefaultPort ? '' : u.port;
117
+ const host = u.hostname.toLowerCase();
118
+ const pathPart = u.pathname.replace(/\/+$/, '');
119
+ return `${scheme}//${host}${port ? `:${port}` : ''}${pathPart}`;
120
+ }
121
+ /**
122
+ * Classify a lane's output as a review or as empty.
123
+ *
124
+ * WHITESPACE-ONLY COUNTS AS EMPTY, for every lane. The bash tested `[ ! -s file ]`, which counts
125
+ * BYTES — so a reply of three spaces passed as a successful review. Two legs (LM Studio,
126
+ * llama.cpp) closed this locally with a case-glob; gemini, claude, codex, qwen and cursor did not.
127
+ * Making it uniform is a deliberate, disclosed behavior change (a bug fix that breaks a workaround)
128
+ * and is why this phase ships a changeset note.
129
+ *
130
+ * The `-n` / `-e` / `-E` hazard the two printf-using legs guarded against cannot occur here at all:
131
+ * nothing in this path goes through `echo`.
132
+ */
133
+ function isEmptyReview(text) {
134
+ if (typeof text !== 'string')
135
+ return true;
136
+ return text.trim().length === 0;
137
+ }
138
+ /**
139
+ * The standard `argv-file-ref` prompt (#2176).
140
+ *
141
+ * Two lanes take the prompt as an ARGUMENT rather than on stdin, and a full plan set inline would
142
+ * approach the 32,767-character Windows `execFileSync` ceiling — so the argument is a short
143
+ * instruction naming the prompt file. It must also carry the ABSOLUTE repo root: an argv-fed CLI
144
+ * does not reliably inherit the review's working directory, and without the anchor the reviewer
145
+ * reviews the plan text in isolation, which is exactly what the Review Instructions forbid.
146
+ *
147
+ * `antigravity` deliberately does NOT use this text — its handler owns a variant that additionally
148
+ * demands a `REVIEWED-WITHOUT-REPO-ACCESS` self-report.
149
+ */
150
+ function fileRefPrompt(promptPath, repoRoot) {
151
+ return (`Read the file at ${promptPath} in full and carry out the review request it contains. ` +
152
+ `The repository under review is at ${repoRoot} — resolve every relative file path in the ` +
153
+ `review request against that absolute root. Output only the resulting markdown review. ` +
154
+ `Do not edit any files.`);
155
+ }
156
+ /** Run-dir artifact paths. POSIX-joined: these are workflow-visible strings, not OS paths. */
157
+ function artifactPaths(runDir, slug) {
158
+ const base = String(runDir ?? '').replace(/\/+$/, '');
159
+ return {
160
+ promptPath: `${base}/gsd-review-prompt.md`,
161
+ reviewPath: `${base}/gsd-review-${slug}.md`,
162
+ errPath: `${base}/gsd-review-${slug}.err`,
163
+ };
164
+ }
165
+ /* ------------------------------------------------------------------ *
166
+ * Resolution
167
+ * ------------------------------------------------------------------ */
168
+ /**
169
+ * Resolve one declared lane into an executable plan.
170
+ *
171
+ * TOTAL: never throws. Every rejection is a typed `LaneUnavailableReason`, because the caller has to
172
+ * tell three different unavailabilities apart — a lane that is absent, a lane whose probe failed,
173
+ * and a lane blocked on a changed egress destination are not the same event to a user.
174
+ *
175
+ * NOT resolved here: probe execution, prompt-budget trimming, and the D5 egress-host comparison.
176
+ * Those need I/O and live in the runner. This function decides SHAPE.
177
+ */
178
+ function resolveLanePlan(input) {
179
+ const warnings = [];
180
+ const fail = (reason, detail) => ({
181
+ ok: false,
182
+ reason,
183
+ detail,
184
+ warnings,
185
+ });
186
+ // Trust boundary: the declared type says ReviewerLane, but overlay manifests arrive here.
187
+ const raw = input?.lane;
188
+ if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
189
+ return fail(exports.LANE_UNAVAILABLE.MALFORMED_LANE, `lane is not an object: ${String(raw)}`);
190
+ }
191
+ const lane = raw;
192
+ const slug = typeof lane.slug === 'string' ? lane.slug.trim() : '';
193
+ if (!slug) {
194
+ return fail(exports.LANE_UNAVAILABLE.MALFORMED_LANE, 'lane declares no slug');
195
+ }
196
+ // The slug is CONCATENATED into artifact paths below, so the grammar is enforced here rather
197
+ // than trusted from upstream. `checkReviewerLaneParity` and the capability validator both check
198
+ // it too, but neither runs on this path — and this module's whole premise is that it is the
199
+ // trust boundary for third-party overlay manifests. A slug of `../../../tmp/evil` would
200
+ // otherwise produce a reviewPath outside the run dir that `writeReviewOrStub` happily writes to.
201
+ if (!review_lane_descriptor_cjs_1.LANE_SLUG_RE.test(slug)) {
202
+ return fail(exports.LANE_UNAVAILABLE.MALFORMED_LANE, `lane slug '${slug}' is outside the declared grammar ${String(review_lane_descriptor_cjs_1.LANE_SLUG_RE)}`);
203
+ }
204
+ // D4 rule 4: an unknown handler FAILS CLOSED. A lane naming imperative code this GSD version does
205
+ // not have cannot be run "mostly" — the handler is precisely the part data could not express.
206
+ const handler = lane.handler ?? null;
207
+ if (handler !== null && !KNOWN_HANDLERS.has(handler)) {
208
+ return fail(exports.LANE_UNAVAILABLE.UNKNOWN_HANDLER, `lane '${slug}' names handler '${String(handler)}', which this GSD version does not provide`);
209
+ }
210
+ const { promptPath, reviewPath, errPath } = artifactPaths(input.runDir, slug);
211
+ const timeoutMs = typeof lane.timeoutFloorMs === 'number' && Number.isFinite(lane.timeoutFloorMs) && lane.timeoutFloorMs > 0
212
+ ? lane.timeoutFloorMs
213
+ : 900_000;
214
+ const emptyOutput = lane.emptyOutput === 'handler-owned' ? 'handler-owned' : 'stub-with-stderr';
215
+ const requiresBinaries = Array.isArray(lane.requiresBinaries)
216
+ ? lane.requiresBinaries.filter((b) => typeof b === 'string')
217
+ : [];
218
+ const model = configString(typeof lane.modelConfigKey === 'string' ? input.configGet(lane.modelConfigKey) : undefined);
219
+ if (lane.transport === 'openai-http') {
220
+ const rawInvoke = lane.invoke;
221
+ // The spawn branch below guards with `inv?.binary`; this one must too. Without it a lane
222
+ // declaring `transport: 'openai-http'` and no `invoke` THROWS, which breaks this module's
223
+ // documented totality — and a throw here is worse than it looks: the CLI seam resolves every
224
+ // selected lane in one `.map`, so one malformed overlay manifest would abort the whole review
225
+ // rather than dropping its own lane.
226
+ if (rawInvoke === null || typeof rawInvoke !== 'object' || Array.isArray(rawInvoke)) {
227
+ return fail(exports.LANE_UNAVAILABLE.MALFORMED_LANE, `openai-http lane '${slug}' declares no invoke object`);
228
+ }
229
+ const inv = rawInvoke;
230
+ const hostConfigKey = typeof inv.hostConfigKey === 'string' ? inv.hostConfigKey : '';
231
+ const configured = hostConfigKey ? configString(input.configGet(hostConfigKey)) : null;
232
+ // Only a STRING declares a host. Coercing an object would produce the literal
233
+ // '[object Object]' and normalize THAT as the lane's egress destination.
234
+ const declaredDefault = typeof inv.defaultHost === 'string' ? inv.defaultHost : '';
235
+ const host = normalizeHost(configured ?? declaredDefault);
236
+ if (!host) {
237
+ return fail(exports.LANE_UNAVAILABLE.MALFORMED_LANE, `lane '${slug}' resolves no host: '${hostConfigKey}' is unset and it declares no defaultHost`);
238
+ }
239
+ const apiPath = typeof inv.path === 'string' && inv.path ? inv.path : '/v1/chat/completions';
240
+ const discovers = inv.modelDiscovery === 'first-from-models-endpoint';
241
+ return {
242
+ ok: true,
243
+ warnings,
244
+ plan: {
245
+ transport: 'openai-http',
246
+ slug,
247
+ host,
248
+ hostConfigKey,
249
+ url: `${host}${apiPath}`,
250
+ modelsUrl: discovers ? `${host}/v1/models` : null,
251
+ model,
252
+ fallbackModel: typeof inv.fallbackModel === 'string' ? inv.fallbackModel : 'local-model',
253
+ promptPath,
254
+ reviewPath,
255
+ errPath,
256
+ timeoutMs,
257
+ emptyOutput,
258
+ handler,
259
+ requiresBinaries,
260
+ probe: lane.probe,
261
+ },
262
+ };
263
+ }
264
+ if (lane.transport !== 'spawn') {
265
+ return fail(exports.LANE_UNAVAILABLE.UNKNOWN_TRANSPORT, `lane '${slug}' declares transport '${String(lane.transport)}'`);
266
+ }
267
+ const inv = lane.invoke;
268
+ const binary = typeof inv?.binary === 'string' ? inv.binary.trim() : '';
269
+ if (!binary) {
270
+ return fail(exports.LANE_UNAVAILABLE.MALFORMED_LANE, `spawn lane '${slug}' declares no binary`);
271
+ }
272
+ // Output target first: `{{output}}` needs to know the path, and the target is also what tells the
273
+ // runner whether to capture stdout or read a file the tool wrote itself (#1698).
274
+ let outputTarget = { kind: 'stdout' };
275
+ let outputExpansion = [];
276
+ if (inv.outputChannel === 'file-arg') {
277
+ const outputArg = typeof inv.outputArg === 'string' ? inv.outputArg : '';
278
+ if (!outputArg) {
279
+ return fail(exports.LANE_UNAVAILABLE.MALFORMED_LANE, `lane '${slug}' declares outputChannel 'file-arg' with no outputArg naming the argument`);
280
+ }
281
+ outputExpansion = [outputArg, reviewPath];
282
+ outputTarget = { kind: 'file', path: reviewPath };
283
+ }
284
+ let stdin = null;
285
+ let promptExpansion = [];
286
+ switch (inv.promptChannel) {
287
+ case 'stdin':
288
+ stdin = promptPath; // the runner streams this file; the plan names it.
289
+ break;
290
+ case 'argv-file-ref':
291
+ promptExpansion = [fileRefPrompt(promptPath, input.repoRoot)];
292
+ break;
293
+ case 'argv':
294
+ promptExpansion = [promptPath];
295
+ break;
296
+ case 'none':
297
+ break; // CodeRabbit reviews the working tree and is fed nothing (review.md:367).
298
+ default:
299
+ return fail(exports.LANE_UNAVAILABLE.MALFORMED_LANE, `lane '${slug}' declares promptChannel '${String(inv.promptChannel)}'`);
300
+ }
301
+ const modelExpansion = model && typeof inv.modelArg === 'string' && inv.modelArg ? [inv.modelArg, model] : [];
302
+ const effortExpansion = inv.effortChannel === 'argv'
303
+ ? (input.effortArgs ?? []).filter((a) => typeof a === 'string' && a !== '')
304
+ : [];
305
+ // Expand the argv template in declared order. A placeholder with nothing to contribute expands to
306
+ // ZERO elements and disappears — that is what lets one template serve both the configured and the
307
+ // unconfigured case without a conditional in the data.
308
+ const expansions = {
309
+ '{{model}}': modelExpansion,
310
+ '{{effort}}': effortExpansion,
311
+ '{{output}}': outputExpansion,
312
+ '{{prompt}}': promptExpansion,
313
+ };
314
+ const template = Array.isArray(inv.args)
315
+ ? inv.args.filter((a) => typeof a === 'string')
316
+ : [];
317
+ const argv = [];
318
+ for (const token of template) {
319
+ // Own-property lookup: a lane could declare a literal `constructor` argument, and prototype
320
+ // members must never resolve as expansions.
321
+ if (Object.prototype.hasOwnProperty.call(expansions, token)) {
322
+ argv.push(...expansions[token]);
323
+ }
324
+ else {
325
+ argv.push(token);
326
+ }
327
+ }
328
+ return {
329
+ ok: true,
330
+ warnings,
331
+ plan: {
332
+ transport: 'spawn',
333
+ slug,
334
+ binary,
335
+ argv,
336
+ stdin,
337
+ promptPath,
338
+ outputTarget,
339
+ reviewPath,
340
+ errPath,
341
+ timeoutMs,
342
+ emptyOutput,
343
+ handler,
344
+ requiresBinaries,
345
+ probe: lane.probe,
346
+ },
347
+ };
348
+ }