github-router 0.3.249 → 0.3.255
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/dist/attribution-settings-CegtE0TY.js +2347 -0
- package/dist/attribution-settings-CegtE0TY.js.map +1 -0
- package/dist/auth-CP4q2CjO.js +44 -0
- package/dist/auth-CP4q2CjO.js.map +1 -0
- package/dist/browser-ext/background.js +8 -1
- package/dist/browser-ext/manifest.json +1 -1
- package/dist/check-usage-6PB5IUFb.js +43 -0
- package/dist/check-usage-6PB5IUFb.js.map +1 -0
- package/dist/claude-BiZxoNWq.js +593 -0
- package/dist/claude-BiZxoNWq.js.map +1 -0
- package/dist/client-CQMfroGV.js +413 -0
- package/dist/client-CQMfroGV.js.map +1 -0
- package/dist/codex-ST21X3WF.js +85 -0
- package/dist/codex-ST21X3WF.js.map +1 -0
- package/dist/debug-CjmE9sgG.js +81 -0
- package/dist/debug-CjmE9sgG.js.map +1 -0
- package/dist/engine-DNCX0Hfd.js +2 -0
- package/dist/{lifecycle-DHZsVN8j.js → exec-DTO2oin0.js} +3 -130
- package/dist/exec-DTO2oin0.js.map +1 -0
- package/dist/file-log-reporter-IFQcL5IK.js +158 -0
- package/dist/file-log-reporter-IFQcL5IK.js.map +1 -0
- package/dist/gate-discovery-CepmbZnc.js +635 -0
- package/dist/gate-discovery-CepmbZnc.js.map +1 -0
- package/dist/get-copilot-usage-DD1N0Xlh.js +11 -0
- package/dist/get-copilot-usage-DD1N0Xlh.js.map +1 -0
- package/dist/hook-mcp-client-DBvm9608.js +136 -0
- package/dist/hook-mcp-client-DBvm9608.js.map +1 -0
- package/dist/internal-artifact-open-D2cQ2QCw.js +319 -0
- package/dist/internal-artifact-open-D2cQ2QCw.js.map +1 -0
- package/dist/internal-first-mate-guard-7v2CtMA6.js +2 -0
- package/dist/internal-first-mate-guard-La0tIjTU.js +126 -0
- package/dist/internal-first-mate-guard-La0tIjTU.js.map +1 -0
- package/dist/internal-plan-review-ipnT3bhj.js +114 -0
- package/dist/internal-plan-review-ipnT3bhj.js.map +1 -0
- package/dist/internal-prompt-submit-DOj75_hh.js +121 -0
- package/dist/internal-prompt-submit-DOj75_hh.js.map +1 -0
- package/dist/internal-session-bind-DwK2iGW0.js +133 -0
- package/dist/internal-session-bind-DwK2iGW0.js.map +1 -0
- package/dist/internal-stop-hook-CNPGJm63.js +212 -0
- package/dist/internal-stop-hook-CNPGJm63.js.map +1 -0
- package/dist/internal-stop-review-AUcFssrW.js +122 -0
- package/dist/internal-stop-review-AUcFssrW.js.map +1 -0
- package/dist/internal-worker-guard-Lu8VHj5k.js +75 -0
- package/dist/internal-worker-guard-Lu8VHj5k.js.map +1 -0
- package/dist/internal-workspace-header-BRMz0Yql.js +24 -0
- package/dist/internal-workspace-header-BRMz0Yql.js.map +1 -0
- package/dist/lifecycle-Byw5s1G8.js +134 -0
- package/dist/lifecycle-Byw5s1G8.js.map +1 -0
- package/dist/lifecycle-D80CaWie.js +2 -0
- package/dist/{lifecycle-CUf1UDrH.js → lifecycle-DD88ka3f.js} +3 -3
- package/dist/{lifecycle-CUf1UDrH.js.map → lifecycle-DD88ka3f.js.map} +1 -1
- package/dist/lifecycle-DzqCqDlM.js +2 -0
- package/dist/main.js +43 -12884
- package/dist/main.js.map +1 -1
- package/dist/mcp-workspace-header-CGJbNeHb.js +21 -0
- package/dist/mcp-workspace-header-CGJbNeHb.js.map +1 -0
- package/dist/models-B6UnGyRo.js +158 -0
- package/dist/models-B6UnGyRo.js.map +1 -0
- package/dist/orchestration-hRU4TdWr.js +39 -0
- package/dist/orchestration-hRU4TdWr.js.map +1 -0
- package/dist/{paths-BjTMI_xK.js → paths-C1cLZ7Ga.js} +52 -20
- package/dist/paths-C1cLZ7Ga.js.map +1 -0
- package/dist/paths-CwKwc2pi.js +2 -0
- package/dist/{peer-mcp-personas-DJzLpfDJ.js → peer-mcp-personas-BDet588n.js} +1006 -2488
- package/dist/peer-mcp-personas-BDet588n.js.map +1 -0
- package/dist/plan-review-hook-C8x0uKPf.js +159 -0
- package/dist/plan-review-hook-C8x0uKPf.js.map +1 -0
- package/dist/prompt-submit-hook-BrkkLAAB.js +153 -0
- package/dist/prompt-submit-hook-BrkkLAAB.js.map +1 -0
- package/dist/provision-DSkPa5hY.js +656 -0
- package/dist/provision-DSkPa5hY.js.map +1 -0
- package/dist/serve-sa_7c-yD.js +1511 -0
- package/dist/serve-sa_7c-yD.js.map +1 -0
- package/dist/server-setup-DcO9qvN3.js +4776 -0
- package/dist/server-setup-DcO9qvN3.js.map +1 -0
- package/dist/start-siGxuZhO.js +129 -0
- package/dist/start-siGxuZhO.js.map +1 -0
- package/dist/stop-gate-hook-DQc2KV7B.js +686 -0
- package/dist/stop-gate-hook-DQc2KV7B.js.map +1 -0
- package/dist/stop-gate-policy-CHwWbuF_.js +268 -0
- package/dist/stop-gate-policy-CHwWbuF_.js.map +1 -0
- package/dist/token-R2ocFC3M.js +1054 -0
- package/dist/token-R2ocFC3M.js.map +1 -0
- package/dist/version-_Q1WpsQp.js +32 -0
- package/dist/version-_Q1WpsQp.js.map +1 -0
- package/dist/worker-dispatch-Bj1uYyG9.js +222 -0
- package/dist/worker-dispatch-Bj1uYyG9.js.map +1 -0
- package/package.json +1 -1
- package/dist/engine-BuuHUb4t.js +0 -2
- package/dist/lifecycle-0rXrwjWk.js +0 -2
- package/dist/lifecycle-DHZsVN8j.js.map +0 -1
- package/dist/lifecycle-hzPwXc3I.js +0 -2
- package/dist/paths-BjTMI_xK.js.map +0 -1
- package/dist/paths-D3XgmTcQ.js +0 -2
- package/dist/peer-mcp-personas-DJzLpfDJ.js.map +0 -1
|
@@ -0,0 +1,686 @@
|
|
|
1
|
+
import { r as parseBoolEnv } from "./exec-DTO2oin0.js";
|
|
2
|
+
import { a as isSubagentContext, o as regressions } from "./stop-gate-policy-CHwWbuF_.js";
|
|
3
|
+
import { promises } from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
//#region src/lib/orchestration/gate-runner.ts
|
|
7
|
+
async function runGateChecks(checks, cwd, exec) {
|
|
8
|
+
const results = await Promise.all(checks.map(async (c) => {
|
|
9
|
+
try {
|
|
10
|
+
const r = await exec({
|
|
11
|
+
command: c.command,
|
|
12
|
+
cwd
|
|
13
|
+
});
|
|
14
|
+
return {
|
|
15
|
+
id: c.id,
|
|
16
|
+
passed: r.exitCode === 0
|
|
17
|
+
};
|
|
18
|
+
} catch {
|
|
19
|
+
return {
|
|
20
|
+
id: c.id,
|
|
21
|
+
passed: false
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
}));
|
|
25
|
+
const passed = /* @__PURE__ */ new Set();
|
|
26
|
+
const ran = /* @__PURE__ */ new Set();
|
|
27
|
+
for (const r of results) {
|
|
28
|
+
ran.add(r.id);
|
|
29
|
+
if (r.passed) passed.add(r.id);
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
passed,
|
|
33
|
+
ran
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
//#endregion
|
|
37
|
+
//#region src/lib/orchestration/gate-registry.ts
|
|
38
|
+
/**
|
|
39
|
+
* Built-in sealed gates. Commands follow this repo's TS/Bun conventions (the
|
|
40
|
+
* `bun run <script>` indirection means a repo without that script simply fails
|
|
41
|
+
* the check, which the selector treats as not-passed rather than a crash). New
|
|
42
|
+
* ecosystems get a new sealed id here, never a caller-supplied command.
|
|
43
|
+
*/
|
|
44
|
+
const SEALED_GATES = {
|
|
45
|
+
"default-ci": [
|
|
46
|
+
{
|
|
47
|
+
id: "typecheck",
|
|
48
|
+
command: "bun run typecheck"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: "test",
|
|
52
|
+
command: "bun test"
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: "lint",
|
|
56
|
+
command: "bun run lint"
|
|
57
|
+
}
|
|
58
|
+
],
|
|
59
|
+
"typecheck-test": [{
|
|
60
|
+
id: "typecheck",
|
|
61
|
+
command: "bun run typecheck"
|
|
62
|
+
}, {
|
|
63
|
+
id: "test",
|
|
64
|
+
command: "bun test"
|
|
65
|
+
}],
|
|
66
|
+
"typecheck-only": [{
|
|
67
|
+
id: "typecheck",
|
|
68
|
+
command: "bun run typecheck"
|
|
69
|
+
}]
|
|
70
|
+
};
|
|
71
|
+
/** The set of sealed gate ids, used as the kernel's `knownGateIds` so the IR
|
|
72
|
+
* verifier rejects an executable gate that references an unregistered id. */
|
|
73
|
+
function sealedGateIds() {
|
|
74
|
+
return new Set(Object.keys(SEALED_GATES));
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Resolve a sealed gate by id. Returns a DEFENSIVE CLONE (fresh objects) so a
|
|
78
|
+
* caller can never mutate the registry's command set. `undefined` for an
|
|
79
|
+
* unknown id, which `run_workflow` rejects before executing anything.
|
|
80
|
+
*/
|
|
81
|
+
function resolveSealedGate(gateId) {
|
|
82
|
+
const checks = SEALED_GATES[gateId];
|
|
83
|
+
if (!checks) return void 0;
|
|
84
|
+
return {
|
|
85
|
+
id: gateId,
|
|
86
|
+
checks: checks.map((c) => ({
|
|
87
|
+
id: c.id,
|
|
88
|
+
command: c.command
|
|
89
|
+
}))
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
//#endregion
|
|
93
|
+
//#region src/lib/orchestration/gate-immutability.ts
|
|
94
|
+
/** Patterns that weaken a gate regardless of language (test exclusivity / focus
|
|
95
|
+
* idioms shared across JS test runners; kept in COMMON since several languages
|
|
96
|
+
* reuse them). */
|
|
97
|
+
const COMMON_PATTERNS = [{
|
|
98
|
+
name: "skipped-test",
|
|
99
|
+
re: /(\.\s*skip\s*\(|\bxit\s*\(|\bxdescribe\s*\(|\.\s*only\s*\()/
|
|
100
|
+
}];
|
|
101
|
+
/** Language-specific weakening patterns, selected by the added line's file type. */
|
|
102
|
+
const LANG_PATTERNS = {
|
|
103
|
+
ts: [
|
|
104
|
+
{
|
|
105
|
+
name: "ts-suppression",
|
|
106
|
+
re: /@ts-(ignore|nocheck|expect-error)\b/
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
name: "any-cast",
|
|
110
|
+
re: /\bas\s+any\b|:\s*any\b/
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
name: "eslint-disable",
|
|
114
|
+
re: /eslint-disable\b/
|
|
115
|
+
}
|
|
116
|
+
],
|
|
117
|
+
py: [
|
|
118
|
+
{
|
|
119
|
+
name: "py-type-ignore",
|
|
120
|
+
re: /#\s*type:\s*ignore\b/
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
name: "py-noqa",
|
|
124
|
+
re: /#\s*noqa\b/
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
name: "py-skip",
|
|
128
|
+
re: /@(pytest\.mark\.skip|unittest\.skip)\b/
|
|
129
|
+
}
|
|
130
|
+
],
|
|
131
|
+
go: [{
|
|
132
|
+
name: "go-skip",
|
|
133
|
+
re: /\bt\.Skip\s*\(/
|
|
134
|
+
}, {
|
|
135
|
+
name: "go-nolint",
|
|
136
|
+
re: /\/\/\s*nolint\b/
|
|
137
|
+
}],
|
|
138
|
+
rust: [{
|
|
139
|
+
name: "rust-ignore",
|
|
140
|
+
re: /#\[\s*ignore\b/
|
|
141
|
+
}, {
|
|
142
|
+
name: "rust-allow",
|
|
143
|
+
re: /#\[\s*allow\s*\(/
|
|
144
|
+
}]
|
|
145
|
+
};
|
|
146
|
+
/** Map a file path to a language key for `LANG_PATTERNS` (null → COMMON only). */
|
|
147
|
+
function langForFile(file) {
|
|
148
|
+
if (!file) return null;
|
|
149
|
+
switch (file.slice(file.lastIndexOf(".")).toLowerCase()) {
|
|
150
|
+
case ".ts":
|
|
151
|
+
case ".tsx":
|
|
152
|
+
case ".mts":
|
|
153
|
+
case ".cts":
|
|
154
|
+
case ".js":
|
|
155
|
+
case ".jsx":
|
|
156
|
+
case ".mjs":
|
|
157
|
+
case ".cjs": return "ts";
|
|
158
|
+
case ".py":
|
|
159
|
+
case ".pyi": return "py";
|
|
160
|
+
case ".go": return "go";
|
|
161
|
+
case ".rs": return "rust";
|
|
162
|
+
default: return null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/** Union of COMMON + every language — the pre-header default (no file known yet),
|
|
166
|
+
* so a header-less diff is matched permissively, exactly as before this became
|
|
167
|
+
* language-scoped. Once a real file header appears, the set narrows to that
|
|
168
|
+
* file's language (or COMMON only for an unknown extension → fail open). */
|
|
169
|
+
const ALL_PATTERNS = [...COMMON_PATTERNS, ...Object.values(LANG_PATTERNS).flat()];
|
|
170
|
+
/** A `diff --git a/x b/x` or `+++ b/x` header → the current file path. */
|
|
171
|
+
function fileFromHeader(line) {
|
|
172
|
+
const git = /^diff --git a\/.+ b\/(.+)$/.exec(line);
|
|
173
|
+
if (git) return git[1];
|
|
174
|
+
const plus = /^\+\+\+ b\/(.+)$/.exec(line);
|
|
175
|
+
if (plus) return plus[1];
|
|
176
|
+
}
|
|
177
|
+
function detectGateWeakening(diff) {
|
|
178
|
+
const findings = [];
|
|
179
|
+
let file;
|
|
180
|
+
let patterns = ALL_PATTERNS;
|
|
181
|
+
for (const raw of diff.split("\n")) {
|
|
182
|
+
const headerFile = fileFromHeader(raw);
|
|
183
|
+
if (headerFile !== void 0) {
|
|
184
|
+
file = headerFile;
|
|
185
|
+
const lang = langForFile(file);
|
|
186
|
+
patterns = lang ? [...COMMON_PATTERNS, ...LANG_PATTERNS[lang]] : COMMON_PATTERNS;
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (!raw.startsWith("+") || raw.startsWith("+++")) continue;
|
|
190
|
+
const added = raw.slice(1);
|
|
191
|
+
for (const p of patterns) if (p.re.test(added)) findings.push(file === void 0 ? {
|
|
192
|
+
pattern: p.name,
|
|
193
|
+
line: added.trim()
|
|
194
|
+
} : {
|
|
195
|
+
pattern: p.name,
|
|
196
|
+
line: added.trim(),
|
|
197
|
+
file
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
weakened: findings.length > 0,
|
|
202
|
+
findings
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
//#endregion
|
|
206
|
+
//#region src/lib/orchestration/stop-gate.ts
|
|
207
|
+
/**
|
|
208
|
+
* The Phase-0 structural-gate decision — the cheap, CERTAIN floor win. It blocks
|
|
209
|
+
* marking work "done" when any canonical gate fails OR the diff weakens a gate.
|
|
210
|
+
* A non-model check that fires every run and never ships broken or gate-gamed
|
|
211
|
+
* code; per the floor analysis it is the only component that raises the
|
|
212
|
+
* worst-case floor BY CONSTRUCTION (the orchestration on top is a conditional
|
|
213
|
+
* bet).
|
|
214
|
+
*
|
|
215
|
+
* This is the logic behind the spawned-session Stop-hook AND the kernel runner's
|
|
216
|
+
* pre-accept check. The process exec and the diff are inputs, so the decision is
|
|
217
|
+
* pure + unit-testable; the hook script that runs `git diff` + invokes this is a
|
|
218
|
+
* thin wrapper.
|
|
219
|
+
*/
|
|
220
|
+
async function evaluateStopGate(input) {
|
|
221
|
+
const gate = await runGateChecks(input.checks, input.cwd, input.exec);
|
|
222
|
+
const weak = detectGateWeakening(input.diff);
|
|
223
|
+
const failedChecks = input.checks.map((c) => c.id).filter((id) => !gate.passed.has(id));
|
|
224
|
+
const block = failedChecks.length > 0 || weak.weakened;
|
|
225
|
+
const parts = [];
|
|
226
|
+
if (failedChecks.length > 0) parts.push(`failing gates: ${failedChecks.join(", ")}`);
|
|
227
|
+
if (weak.weakened) {
|
|
228
|
+
const pats = [...new Set(weak.findings.map((f) => f.pattern))].join(", ");
|
|
229
|
+
parts.push(`gate-weakening in the diff: ${pats}`);
|
|
230
|
+
}
|
|
231
|
+
return {
|
|
232
|
+
block,
|
|
233
|
+
reason: block ? parts.join("; ") : "all canonical gates pass; no gate-weakening in the diff",
|
|
234
|
+
failedChecks,
|
|
235
|
+
weakening: weak.findings
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
//#endregion
|
|
239
|
+
//#region src/lib/orchestration/stop-gate-hook.ts
|
|
240
|
+
/**
|
|
241
|
+
* The Phase-0 structural-gate Stop-hook glue. `runStopGateForLaunch` resolves a
|
|
242
|
+
* SEALED gate by id and evaluates it against the working-tree diff, returning the
|
|
243
|
+
* block decision a spawned-session Stop hook (or a manual check) acts on.
|
|
244
|
+
*
|
|
245
|
+
* Safety posture (deliberately different from the kernel): a Stop hook that
|
|
246
|
+
* blocks on a MISCONFIGURATION would wedge the user's session (they could not
|
|
247
|
+
* stop). So this fails OPEN on config problems (unknown gate id) and blocks ONLY
|
|
248
|
+
* on a genuine red gate or a gate-weakening diff. The kernel, by contrast, fails
|
|
249
|
+
* CLOSED to the baseline. Both are correct for their context.
|
|
250
|
+
*
|
|
251
|
+
* The sealed gate + diff + exec are inputs, so the decision is pure and
|
|
252
|
+
* unit-testable; the thin live wrapper (capture `git diff`, spawn the checks,
|
|
253
|
+
* map the result to an exit code) is the only part that needs a real process.
|
|
254
|
+
*/
|
|
255
|
+
async function runStopGateForLaunch(input) {
|
|
256
|
+
const gate = resolveSealedGate(input.gateId);
|
|
257
|
+
if (!gate) return {
|
|
258
|
+
block: false,
|
|
259
|
+
reason: `stop-gate: unknown gateId "${input.gateId}" (not blocking)`,
|
|
260
|
+
failedChecks: [],
|
|
261
|
+
weakening: []
|
|
262
|
+
};
|
|
263
|
+
return evaluateStopGate({
|
|
264
|
+
checks: gate.checks,
|
|
265
|
+
cwd: input.workspace,
|
|
266
|
+
exec: input.exec,
|
|
267
|
+
diff: input.diff
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* The launch-stable baseline key for the dynamic gate path: keyed by the
|
|
272
|
+
* workdir + the descriptor's check set + a per-launch token, NOT the session id
|
|
273
|
+
* and NOT HEAD. The LAUNCHER generates the token (passed to the child via env),
|
|
274
|
+
* computes this key, and seeds the baseline (the set of checks already failing at
|
|
275
|
+
* launch, BEFORE the agent mutates the tree); the runtime hook recomputes the
|
|
276
|
+
* same key (reading the token from its env) and reads that baseline, so an
|
|
277
|
+
* agent-introduced failure is a REGRESSION rather than being silently adopted as
|
|
278
|
+
* the baseline at first stop. The per-launch token isolates concurrent sessions
|
|
279
|
+
* on the same repo (they don't clobber each other's baseline). HEAD is
|
|
280
|
+
* deliberately NOT in the key: an agent that COMMITS its work mid-session must
|
|
281
|
+
* still be judged against the pre-session baseline (a HEAD-keyed baseline would
|
|
282
|
+
* vanish on the commit — the exact moment it matters). A changed check set (the
|
|
283
|
+
* agent edited the harness) yields a different key → the launch baseline no
|
|
284
|
+
* longer matches and the hook degrades to first-stop baselining.
|
|
285
|
+
*/
|
|
286
|
+
function launchBaselineKey(workdir, descriptorKey, token) {
|
|
287
|
+
return JSON.stringify([
|
|
288
|
+
"launch",
|
|
289
|
+
path.resolve(workdir),
|
|
290
|
+
descriptorKey,
|
|
291
|
+
token ?? ""
|
|
292
|
+
]);
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Run the resolved checks ONCE at launch (pre-mutation) and record the failing
|
|
296
|
+
* set as the baseline under `launchBaselineKey`. Best-effort: any failure is
|
|
297
|
+
* swallowed (the hook then degrades to first-stop baselining). The launcher MUST
|
|
298
|
+
* pass the SAME check set the hook will run (same `includeTests`) so a
|
|
299
|
+
* pre-existing test failure isn't later miscounted as a regression.
|
|
300
|
+
*
|
|
301
|
+
* NOTE: capture is best-effort pre-mutation — the launcher fires it before the
|
|
302
|
+
* child can edit, and in practice the agent does not mutate until well after a
|
|
303
|
+
* user prompt, so the checks see the clean tree. If the agent somehow mutates
|
|
304
|
+
* before this completes, the worst case is the legacy first-stop baseline (no
|
|
305
|
+
* regression, never a wedge).
|
|
306
|
+
*/
|
|
307
|
+
async function captureLaunchBaseline(input) {
|
|
308
|
+
if (input.checks.length === 0) return;
|
|
309
|
+
try {
|
|
310
|
+
const gate = await runGateChecks(input.checks, input.workdir, input.exec);
|
|
311
|
+
const failed = input.checks.map((c) => c.id).filter((id) => !gate.passed.has(id));
|
|
312
|
+
await input.baseline.set(launchBaselineKey(input.workdir, input.descriptorKey, input.token), failed);
|
|
313
|
+
} catch {}
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* The advisory background review (hook V2) is ON by default whenever the Stop
|
|
317
|
+
* gate runs; it is the cross-lab accountability layer. Opt out with
|
|
318
|
+
* `GH_ROUTER_DISABLE_STOP_REVIEW=1` to keep the deterministic gate but drop the
|
|
319
|
+
* LLM review. (Disabling the whole gate with `GH_ROUTER_DISABLE_STOP_GATE=1`
|
|
320
|
+
* also drops the review, since the review only ever fires from the gate's green
|
|
321
|
+
* path.)
|
|
322
|
+
*/
|
|
323
|
+
function stopReviewEnabled(env = process.env) {
|
|
324
|
+
return parseBoolEnv(env.GH_ROUTER_DISABLE_STOP_REVIEW) !== true;
|
|
325
|
+
}
|
|
326
|
+
/** The sealed gate the Stop hook runs, overridable via `GH_ROUTER_STOP_GATE_ID`
|
|
327
|
+
* (must be a registered sealed id; the live wrapper falls open on an unknown
|
|
328
|
+
* id). Defaults to `default-ci`. */
|
|
329
|
+
function stopGateId(env = process.env) {
|
|
330
|
+
const v = (env.GH_ROUTER_STOP_GATE_ID ?? "").trim();
|
|
331
|
+
return v.length > 0 ? v : "default-ci";
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* PLAN-MODE env fallback. When the session is in plan mode, plan/memory scratch
|
|
335
|
+
* edits should not trip the gate-WEAKENING scan (a plan/memory markdown file may
|
|
336
|
+
* legitimately quote `it.skip` / `as any` while describing work). The launcher
|
|
337
|
+
* sets `GH_ROUTER_STOP_GATE_PLAN_MODE` for a plan-mode session; the payload may
|
|
338
|
+
* ALSO carry a forward-compat `plan_mode` field (see `decideStopHook`). Either
|
|
339
|
+
* signal being present activates the scoping — the executable gate STILL runs
|
|
340
|
+
* and non-plan hunks are STILL scanned.
|
|
341
|
+
*/
|
|
342
|
+
function stopGatePlanMode(env = process.env) {
|
|
343
|
+
return parseBoolEnv(env.GH_ROUTER_STOP_GATE_PLAN_MODE) === true;
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Is a unified-diff file path confined to a `plans/` or `memory/` directory?
|
|
347
|
+
* Windows-safe (normalizes `\` → `/`). Matches only when a DIRECTORY segment of
|
|
348
|
+
* the path is exactly `plans` or `memory`, so `plans/x.md`, `memory/y.md`, and
|
|
349
|
+
* `a/plans/z` match while the filenames `docs/plans.md` / `myplans/x` do not.
|
|
350
|
+
*/
|
|
351
|
+
function isPlanMemoryDiffPath(p) {
|
|
352
|
+
const segs = p.replace(/\\/g, "/").split("/");
|
|
353
|
+
for (let i = 0; i < segs.length - 1; i++) if (segs[i] === "plans" || segs[i] === "memory") return true;
|
|
354
|
+
return false;
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Drop the file sections of a unified `git diff` whose NEW path (the `b/…` side)
|
|
358
|
+
* is confined to `plans/` or `memory/` — used for the gate-WEAKENING scan when
|
|
359
|
+
* plan mode is active. Sections for other files (e.g. `src/…`) are preserved
|
|
360
|
+
* verbatim, so a MIXED diff keeps its non-plan hunks and `src/` weakening is
|
|
361
|
+
* still caught. The executable gate does not read the diff, so filtering it here
|
|
362
|
+
* never affects which checks run. A diff with no `diff --git` sections (or an
|
|
363
|
+
* empty one) is returned unchanged.
|
|
364
|
+
*/
|
|
365
|
+
function stripPlanMemoryDiffHunks(diff) {
|
|
366
|
+
if (diff.length === 0) return diff;
|
|
367
|
+
const out = [];
|
|
368
|
+
let dropping = false;
|
|
369
|
+
for (const line of diff.split("\n")) {
|
|
370
|
+
if (line.startsWith("diff --git ")) {
|
|
371
|
+
const bPath = /^diff --git a\/.+ b\/(.+)$/.exec(line)?.[1];
|
|
372
|
+
dropping = bPath !== void 0 && isPlanMemoryDiffPath(bPath);
|
|
373
|
+
if (!dropping) out.push(line);
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
if (!dropping) out.push(line);
|
|
377
|
+
}
|
|
378
|
+
return out.join("\n");
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* C3: whether the structural Stop-gate is disabled for THIS launch. True when the
|
|
382
|
+
* `GH_ROUTER_DISABLE_STOP_GATE` env is set OR the `--no-stop-gate` launcher flag was
|
|
383
|
+
* passed (`args["no-stop-gate"] === true`). A DRIVEN session sets the flag so a
|
|
384
|
+
* blocking Stop hook never hangs its turn-end (~10 min) waiting on the fleet driver.
|
|
385
|
+
* Pure so it is unit-testable without the live launch path.
|
|
386
|
+
*/
|
|
387
|
+
function stopGateDisabled(args, env = process.env) {
|
|
388
|
+
return parseBoolEnv(env.GH_ROUTER_DISABLE_STOP_GATE) === true || args["no-stop-gate"] === true;
|
|
389
|
+
}
|
|
390
|
+
/** True when a settings `Stop` entry already registers `command` (so the merge
|
|
391
|
+
* is idempotent across re-launches). */
|
|
392
|
+
function entryHasCommand(entry, command) {
|
|
393
|
+
if (!entry || typeof entry !== "object") return false;
|
|
394
|
+
const hooks = entry.hooks;
|
|
395
|
+
if (!Array.isArray(hooks)) return false;
|
|
396
|
+
return hooks.some((h) => h && typeof h === "object" && h.command === command);
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Idempotently merge a hook running `command` for `event` (default `Stop`) into
|
|
400
|
+
* an existing Claude Code settings object WITHOUT clobbering other hook events or
|
|
401
|
+
* other entries. Returns a new object (never mutates the input). Re-running the
|
|
402
|
+
* launcher with the same command+event does not duplicate the hook.
|
|
403
|
+
*/
|
|
404
|
+
function mergeStopHookIntoSettings(existing, command, event = "Stop", timeoutSec, matcher) {
|
|
405
|
+
const base = existing && typeof existing === "object" ? { ...existing } : {};
|
|
406
|
+
const hooks = base.hooks && typeof base.hooks === "object" ? { ...base.hooks } : {};
|
|
407
|
+
const arr = Array.isArray(hooks[event]) ? [...hooks[event]] : [];
|
|
408
|
+
if (!arr.some((e) => entryHasCommand(e, command))) {
|
|
409
|
+
const hook = {
|
|
410
|
+
type: "command",
|
|
411
|
+
command
|
|
412
|
+
};
|
|
413
|
+
if (typeof timeoutSec === "number" && Number.isFinite(timeoutSec) && timeoutSec > 0) hook.timeout = timeoutSec;
|
|
414
|
+
arr.push(matcher ? {
|
|
415
|
+
matcher,
|
|
416
|
+
hooks: [hook]
|
|
417
|
+
} : { hooks: [hook] });
|
|
418
|
+
}
|
|
419
|
+
hooks[event] = arr;
|
|
420
|
+
base.hooks = hooks;
|
|
421
|
+
return base;
|
|
422
|
+
}
|
|
423
|
+
async function decideStopHook(input) {
|
|
424
|
+
const maxBlocks = input.maxBlocks ?? 2;
|
|
425
|
+
let payload = {};
|
|
426
|
+
let parsed = false;
|
|
427
|
+
try {
|
|
428
|
+
const p = JSON.parse(input.stdin);
|
|
429
|
+
if (p && typeof p === "object") {
|
|
430
|
+
payload = p;
|
|
431
|
+
parsed = true;
|
|
432
|
+
}
|
|
433
|
+
} catch {}
|
|
434
|
+
if (!parsed) return { exitCode: 0 };
|
|
435
|
+
if (isSubagentContext(payload)) return { exitCode: 0 };
|
|
436
|
+
const sessionId = typeof payload.session_id === "string" && payload.session_id.length > 0 ? payload.session_id : "";
|
|
437
|
+
if (!sessionId) return { exitCode: 0 };
|
|
438
|
+
const cwdRaw = typeof payload.cwd === "string" && payload.cwd.length > 0 ? payload.cwd : input.fallbackCwd;
|
|
439
|
+
let cwd = cwdRaw;
|
|
440
|
+
try {
|
|
441
|
+
cwd = await promises.realpath(cwdRaw);
|
|
442
|
+
} catch {}
|
|
443
|
+
let enabled;
|
|
444
|
+
try {
|
|
445
|
+
enabled = await input.isEnabledForRepo(cwd);
|
|
446
|
+
} catch {
|
|
447
|
+
return { exitCode: 0 };
|
|
448
|
+
}
|
|
449
|
+
if (!enabled) return { exitCode: 0 };
|
|
450
|
+
let priorBlocks;
|
|
451
|
+
try {
|
|
452
|
+
priorBlocks = await input.budget.count(sessionId);
|
|
453
|
+
} catch {
|
|
454
|
+
return { exitCode: 0 };
|
|
455
|
+
}
|
|
456
|
+
if (priorBlocks >= maxBlocks) return {
|
|
457
|
+
exitCode: 0,
|
|
458
|
+
stderr: `structural gate: reached the ${maxBlocks}-block limit for this prompt; allowing the stop WITHOUT re-running the checks. If the failures from the last block are unfixed, they are shipping — run the checks manually to verify.`
|
|
459
|
+
};
|
|
460
|
+
let resolvedKey = input.gateId;
|
|
461
|
+
let dynamicBaselineKey;
|
|
462
|
+
const planMode = input.planMode === true || payload.plan_mode === true;
|
|
463
|
+
const scanDiff = (diff) => planMode ? stripPlanMemoryDiffHunks(diff) : diff;
|
|
464
|
+
const capturedDiff = async (workdir) => {
|
|
465
|
+
try {
|
|
466
|
+
return {
|
|
467
|
+
diff: await input.captureDiff(workdir),
|
|
468
|
+
captured: true
|
|
469
|
+
};
|
|
470
|
+
} catch {
|
|
471
|
+
return {
|
|
472
|
+
diff: "",
|
|
473
|
+
captured: false
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
const runGate = async () => {
|
|
478
|
+
if (input.resolveChecks) {
|
|
479
|
+
const resolved = await input.resolveChecks(cwd).catch(() => null);
|
|
480
|
+
if (!resolved || resolved.checks.length === 0) return null;
|
|
481
|
+
resolvedKey = resolved.descriptorKey;
|
|
482
|
+
dynamicBaselineKey = resolved.baselineKey;
|
|
483
|
+
const workdir = resolved.workdir || cwd;
|
|
484
|
+
const { diff, captured } = await capturedDiff(workdir);
|
|
485
|
+
if (captured && diff.length === 0) return {
|
|
486
|
+
kind: "no-diff",
|
|
487
|
+
diff
|
|
488
|
+
};
|
|
489
|
+
const result = await evaluateStopGate({
|
|
490
|
+
checks: resolved.checks,
|
|
491
|
+
cwd: workdir,
|
|
492
|
+
exec: input.exec,
|
|
493
|
+
diff: captured ? scanDiff(diff) : ""
|
|
494
|
+
});
|
|
495
|
+
return {
|
|
496
|
+
kind: "evaluated",
|
|
497
|
+
failedChecks: [...result.failedChecks],
|
|
498
|
+
weakeningPatterns: [...new Set(result.weakening.map((w) => w.pattern))],
|
|
499
|
+
diff
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
const { diff, captured } = await capturedDiff(cwd);
|
|
503
|
+
if (captured && diff.length === 0) return {
|
|
504
|
+
kind: "no-diff",
|
|
505
|
+
diff
|
|
506
|
+
};
|
|
507
|
+
const result = await runStopGateForLaunch({
|
|
508
|
+
workspace: cwd,
|
|
509
|
+
gateId: input.gateId,
|
|
510
|
+
exec: input.exec,
|
|
511
|
+
diff: captured ? scanDiff(diff) : ""
|
|
512
|
+
});
|
|
513
|
+
return {
|
|
514
|
+
kind: "evaluated",
|
|
515
|
+
failedChecks: [...result.failedChecks],
|
|
516
|
+
weakeningPatterns: [...new Set(result.weakening.map((w) => w.pattern))],
|
|
517
|
+
diff
|
|
518
|
+
};
|
|
519
|
+
};
|
|
520
|
+
const timeoutMs = input.timeoutMs ?? 3e5;
|
|
521
|
+
let timer;
|
|
522
|
+
const raced = await Promise.race([runGate(), new Promise((resolve) => {
|
|
523
|
+
timer = setTimeout(() => resolve("timeout"), timeoutMs);
|
|
524
|
+
})]);
|
|
525
|
+
if (timer) clearTimeout(timer);
|
|
526
|
+
if (raced === "timeout") return { exitCode: 0 };
|
|
527
|
+
if (raced === null) return { exitCode: 0 };
|
|
528
|
+
if (raced.kind === "no-diff") return { exitCode: 0 };
|
|
529
|
+
const baselineKey = dynamicBaselineKey ?? JSON.stringify([
|
|
530
|
+
sessionId,
|
|
531
|
+
cwd,
|
|
532
|
+
resolvedKey
|
|
533
|
+
]);
|
|
534
|
+
const recorded = await input.baseline.get(baselineKey).catch(() => null);
|
|
535
|
+
if (recorded === null) await input.baseline.set(baselineKey, raced.failedChecks).catch(() => {});
|
|
536
|
+
const regressed = regressions(raced.failedChecks, recorded);
|
|
537
|
+
const weakened = raced.weakeningPatterns.length > 0;
|
|
538
|
+
if (regressed.length === 0 && !weakened) {
|
|
539
|
+
await maybeSpawnReview(input, sessionId, cwd, raced.diff);
|
|
540
|
+
return { exitCode: 0 };
|
|
541
|
+
}
|
|
542
|
+
try {
|
|
543
|
+
await input.budget.record(sessionId);
|
|
544
|
+
} catch {
|
|
545
|
+
return { exitCode: 0 };
|
|
546
|
+
}
|
|
547
|
+
const parts = [];
|
|
548
|
+
if (regressed.length > 0) parts.push(`regressed gates: ${regressed.join(", ")}`);
|
|
549
|
+
if (weakened) parts.push(`gate-weakening in the diff: ${raced.weakeningPatterns.join(", ")}`);
|
|
550
|
+
return {
|
|
551
|
+
exitCode: 2,
|
|
552
|
+
stderr: `structural gate failed (block ${priorBlocks + 1}/${maxBlocks}): ${parts.join("; ")}. Fix the failing checks and revert any gate-weakening (no new .skip / as any / lint-disable) before finishing.`
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* The advisory-review side-effect on a GREEN stop: debounce by diff hash, then
|
|
557
|
+
* fire the detached background reviewer. ADVISORY-ONLY — it returns void, never
|
|
558
|
+
* throws (every step is swallowed), and the caller does not await its result for
|
|
559
|
+
* the exit decision. A no-op when the review layer isn't wired (no debounce /
|
|
560
|
+
* spawn injected, e.g. GH_ROUTER_DISABLE_STOP_REVIEW) or the diff is empty.
|
|
561
|
+
*
|
|
562
|
+
* `markReviewed` runs BEFORE the spawn so a crashing spawn still records the
|
|
563
|
+
* debounce (an identical tree won't re-trigger on the next stop). The review is
|
|
564
|
+
* gated on the diff CHANGING since the last review — without it, every stop of
|
|
565
|
+
* an unchanged tree would re-spend a background gpt-5.6-sol review.
|
|
566
|
+
*
|
|
567
|
+
* The whole body is bounded by a short timeout (the stores are local temp files
|
|
568
|
+
* that complete in well under a millisecond in practice, so the timeout never
|
|
569
|
+
* fires normally — but if the debounce read/write ever stalled, the stop must
|
|
570
|
+
* still proceed promptly; the advisory layer never delays a clean stop).
|
|
571
|
+
*/
|
|
572
|
+
const REVIEW_SIDE_EFFECT_BUDGET_MS = 2e3;
|
|
573
|
+
async function maybeSpawnReview(input, sessionId, cwd, diff) {
|
|
574
|
+
if (!input.reviewDebounce || !input.spawnReview) return;
|
|
575
|
+
if (diff.trim().length === 0) return;
|
|
576
|
+
let timer;
|
|
577
|
+
try {
|
|
578
|
+
const work = (async () => {
|
|
579
|
+
const diffHash = createHash("sha256").update(diff).digest("hex");
|
|
580
|
+
if (!await input.reviewDebounce.shouldReview(sessionId, diffHash)) return;
|
|
581
|
+
await input.reviewDebounce.markReviewed(sessionId, diffHash);
|
|
582
|
+
input.spawnReview({
|
|
583
|
+
sessionId,
|
|
584
|
+
cwd,
|
|
585
|
+
diff,
|
|
586
|
+
diffHash
|
|
587
|
+
});
|
|
588
|
+
})();
|
|
589
|
+
await Promise.race([work, new Promise((resolve) => {
|
|
590
|
+
timer = setTimeout(resolve, REVIEW_SIDE_EFFECT_BUDGET_MS);
|
|
591
|
+
})]);
|
|
592
|
+
} catch {} finally {
|
|
593
|
+
if (timer) clearTimeout(timer);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* A file-backed `BlockBudget` under `stateDir`, keyed by a hash of the session id
|
|
598
|
+
* (so a session id is never written verbatim to a predictable path). Best-effort:
|
|
599
|
+
* a read miss counts as 0; `record` increments. A write/read error propagates so
|
|
600
|
+
* `decideStopHook` stands down (it can't guarantee termination without the
|
|
601
|
+
* budget).
|
|
602
|
+
*/
|
|
603
|
+
function fileBlockBudget(stateDir) {
|
|
604
|
+
const fileFor = (sid) => path.join(stateDir, `block-${createHash("sha256").update(sid).digest("hex").slice(0, 32)}`);
|
|
605
|
+
const readCount = async (sid) => {
|
|
606
|
+
try {
|
|
607
|
+
const raw = await promises.readFile(fileFor(sid), "utf8");
|
|
608
|
+
const n = Number.parseInt(raw.trim(), 10);
|
|
609
|
+
return Number.isFinite(n) && n > 0 ? n : 0;
|
|
610
|
+
} catch {
|
|
611
|
+
return 0;
|
|
612
|
+
}
|
|
613
|
+
};
|
|
614
|
+
return {
|
|
615
|
+
count: readCount,
|
|
616
|
+
async record(sid) {
|
|
617
|
+
const next = await readCount(sid) + 1;
|
|
618
|
+
await promises.mkdir(stateDir, { recursive: true });
|
|
619
|
+
await promises.writeFile(fileFor(sid), String(next), { mode: 384 });
|
|
620
|
+
},
|
|
621
|
+
async reset(sid) {
|
|
622
|
+
await promises.unlink(fileFor(sid)).catch(() => {});
|
|
623
|
+
}
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
627
|
+
* Build the shell command string Claude Code runs for the Stop hook. Invokes the
|
|
628
|
+
* running github-router via its node/bun binary so it works regardless of PATH.
|
|
629
|
+
* Pure (takes the binary + script paths) so the quoting is unit-testable; the
|
|
630
|
+
* cross-platform firing is verified by the gated E2E.
|
|
631
|
+
*/
|
|
632
|
+
function buildStopHookCommand(execPath, scriptPath) {
|
|
633
|
+
const q = (s) => `"${s}"`;
|
|
634
|
+
if (scriptPath && scriptPath !== execPath) return `${q(execPath)} ${q(scriptPath)} internal-stop-hook`;
|
|
635
|
+
return `${q(execPath)} internal-stop-hook`;
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Build the shell command Claude Code runs for the SessionStart/SessionEnd hooks
|
|
639
|
+
* (registered only when github-router runs inside an ai-or-die Terminal tab). The
|
|
640
|
+
* sidecar path is baked in as a literal `--out` arg — NOT passed via env — so it
|
|
641
|
+
* survives `AIORDIE_CLAUDE_BIND` being stripped from the child's environment and a
|
|
642
|
+
* nested `github-router claude` can't inherit it. Pure (binary + script + out
|
|
643
|
+
* paths) for unit-testable quoting; the live firing is verified end-to-end.
|
|
644
|
+
*/
|
|
645
|
+
function buildSessionBindHookCommand(execPath, scriptPath, outPath) {
|
|
646
|
+
const q = (s) => `"${s}"`;
|
|
647
|
+
return `${scriptPath && scriptPath !== execPath ? `${q(execPath)} ${q(scriptPath)}` : q(execPath)} internal-session-bind --out ${q(outPath)}`;
|
|
648
|
+
}
|
|
649
|
+
/** Command for the `internal-artifact-open` hook (no args — token comes from the
|
|
650
|
+
* mirror creds file, plan from the plans dir; nothing secret in argv). */
|
|
651
|
+
function buildArtifactOpenHookCommand(execPath, scriptPath) {
|
|
652
|
+
const q = (s) => `"${s}"`;
|
|
653
|
+
return `${scriptPath && scriptPath !== execPath ? `${q(execPath)} ${q(scriptPath)}` : q(execPath)} internal-artifact-open`;
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Read-merge-atomic-write the Stop hook into a Claude Code `settings.json` file
|
|
657
|
+
* (the mirrored one). A MISSING file (ENOENT) starts from `{}`; any OTHER read or
|
|
658
|
+
* parse error THROWS (the caller's try/catch warns and continues) rather than
|
|
659
|
+
* overwriting a file we couldn't understand with our defaults. Preserves every
|
|
660
|
+
* other setting, is idempotent, and uses temp+rename so Claude Code's mtime
|
|
661
|
+
* watcher never sees a half-written file. Returns the merged object.
|
|
662
|
+
*/
|
|
663
|
+
async function injectStopHookIntoSettingsFile(settingsPath, command, event = "Stop", timeoutSec, matcher) {
|
|
664
|
+
let existing = {};
|
|
665
|
+
let raw;
|
|
666
|
+
try {
|
|
667
|
+
raw = await promises.readFile(settingsPath, "utf8");
|
|
668
|
+
} catch (err) {
|
|
669
|
+
if (err.code !== "ENOENT") throw err;
|
|
670
|
+
raw = void 0;
|
|
671
|
+
}
|
|
672
|
+
if (raw !== void 0) {
|
|
673
|
+
const parsed = JSON.parse(raw);
|
|
674
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) existing = parsed;
|
|
675
|
+
else throw new Error(`settings.json at ${settingsPath} is not a JSON object; refusing to overwrite`);
|
|
676
|
+
}
|
|
677
|
+
const merged = mergeStopHookIntoSettings(existing, command, event, timeoutSec, matcher);
|
|
678
|
+
const tmp = `${settingsPath}.${process.pid}.tmp`;
|
|
679
|
+
await promises.writeFile(tmp, `${JSON.stringify(merged, null, 2)}\n`, { mode: 384 });
|
|
680
|
+
await promises.rename(tmp, settingsPath);
|
|
681
|
+
return merged;
|
|
682
|
+
}
|
|
683
|
+
//#endregion
|
|
684
|
+
export { decideStopHook as a, launchBaselineKey as c, stopGatePlanMode as d, stopReviewEnabled as f, runGateChecks as h, captureLaunchBaseline as i, stopGateDisabled as l, sealedGateIds as m, buildSessionBindHookCommand as n, fileBlockBudget as o, resolveSealedGate as p, buildStopHookCommand as r, injectStopHookIntoSettingsFile as s, buildArtifactOpenHookCommand as t, stopGateId as u };
|
|
685
|
+
|
|
686
|
+
//# sourceMappingURL=stop-gate-hook-DQc2KV7B.js.map
|