github-router 0.3.250 → 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/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 -12904
- 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-BvommiSI.js → peer-mcp-personas-BDet588n.js} +163 -2392
- 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-dOYoMQis.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-BvommiSI.js.map +0 -1
|
@@ -0,0 +1,635 @@
|
|
|
1
|
+
import { t as PATHS } from "./paths-C1cLZ7Ga.js";
|
|
2
|
+
import { a as resolveExecutable } from "./exec-DTO2oin0.js";
|
|
3
|
+
import { p as resolveSealedGate } from "./stop-gate-hook-DQc2KV7B.js";
|
|
4
|
+
import { c as repoRoot, s as repoFingerprint } from "./stop-gate-policy-CHwWbuF_.js";
|
|
5
|
+
import { existsSync, promises } from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
8
|
+
//#region src/lib/orchestration/harness-parse.ts
|
|
9
|
+
/**
|
|
10
|
+
* Deterministic, language-agnostic harness PARSER for the structural Stop-gate.
|
|
11
|
+
*
|
|
12
|
+
* The gate runs a repo's own checks (typecheck/lint/test/build) when the agent
|
|
13
|
+
* tries to finish. Historically it only auto-enabled for Bun/TS repos via a
|
|
14
|
+
* hard-coded sealed gate. This module generalizes detection by PARSING the
|
|
15
|
+
* project's own authoritative config — `package.json` scripts, CI `run:` steps,
|
|
16
|
+
* Make/just/task targets, and language manifests — and emitting the canonical
|
|
17
|
+
* check commands it finds.
|
|
18
|
+
*
|
|
19
|
+
* Design properties (a cross-lab panel chose a parser over a model for these):
|
|
20
|
+
* - EVIDENCE-PINNED BY CONSTRUCTION: every emitted command is either lifted
|
|
21
|
+
* verbatim from a parsed source file (a package.json script, a CI `run:`
|
|
22
|
+
* line, a Make target invocation) or is a fixed canonical manifest command
|
|
23
|
+
* for a detected ecosystem (`cargo check`, `go vet ./...`). The parser never
|
|
24
|
+
* invents a command, so it has no hallucination / prompt-injection surface.
|
|
25
|
+
* - PURE + DETERMINISTIC: a function of the repo's files + the live PATH. The
|
|
26
|
+
* runtime Stop hook re-derives it at each stop (no cache), so it always
|
|
27
|
+
* reflects the current tree and fails open for free when a tool vanished.
|
|
28
|
+
* - SHELL-SAFE: `liveExec` runs `command.trim().split(/\s+/)` (naive argv
|
|
29
|
+
* split, no shell), so every emitted command is validated to be plain
|
|
30
|
+
* space-separated tokens with no shell operators and no `%` (cmd.exe quoting
|
|
31
|
+
* throws on `%`). See `isSafeCommand`.
|
|
32
|
+
*
|
|
33
|
+
* This is consumed ONLY by the local Stop hook. The sealed-gate kernel
|
|
34
|
+
* (`run_workflow`) is never fed a parsed command — `GateDescriptor.kind` is
|
|
35
|
+
* `"parsed"`, never a sealed id, and `sealedGateIds()` is unchanged.
|
|
36
|
+
*/
|
|
37
|
+
/** The fast static checks that are always-on; `test` is opt-in (it runs project
|
|
38
|
+
* code and can be slow on every stop). */
|
|
39
|
+
const STATIC_IDS = /* @__PURE__ */ new Set(["typecheck", "lint"]);
|
|
40
|
+
/** Reject anything that is not a plain, shell-free, `%`-free argv line. This is
|
|
41
|
+
* the load-bearing guard for `liveExec`'s naive whitespace split. */
|
|
42
|
+
function isSafeCommand(command) {
|
|
43
|
+
const c = command.trim();
|
|
44
|
+
if (c.length === 0 || c.length > 200) return false;
|
|
45
|
+
if (/[\r\n]/.test(c)) return false;
|
|
46
|
+
const tokens = c.split(/\s+/).filter(Boolean);
|
|
47
|
+
if (tokens.length === 0) return false;
|
|
48
|
+
return tokens.every((t) => /^[A-Za-z0-9._/:@=+-]+$/.test(t));
|
|
49
|
+
}
|
|
50
|
+
/** First token of a command (the executable), for a PATH-presence probe. */
|
|
51
|
+
function firstToken(command) {
|
|
52
|
+
return command.trim().split(/\s+/)[0] ?? "";
|
|
53
|
+
}
|
|
54
|
+
/** True when a command would MUTATE the tree and so must never be a gate: a
|
|
55
|
+
* `--fix`/`--write` flag or a `:fix`/`:write` script-name suffix (e.g.
|
|
56
|
+
* `npm run lint:fix`, `eslint --fix`). Whitespace is normalized first so a
|
|
57
|
+
* tab/double-space can't slip a flag past the scan. Exported + shared with the
|
|
58
|
+
* discovered-command sanitizer. */
|
|
59
|
+
function isMutatingCommand(command) {
|
|
60
|
+
const c = command.trim().replace(/\s+/g, " ");
|
|
61
|
+
if (/(^| )(--fix|--write|-w|--watch|--serve|--interactive|-i)( |=|$)/i.test(c)) return true;
|
|
62
|
+
if (/(^| |:|-)(fix|write)\b/i.test(c)) return true;
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
/** True when the command is safe AND its executable resolves on PATH (a missing
|
|
66
|
+
* tool → drop the check rather than guarantee a false-red). */
|
|
67
|
+
function commandRunnable(command) {
|
|
68
|
+
if (!isSafeCommand(command)) return false;
|
|
69
|
+
return resolveExecutable(firstToken(command), { env: process.env }) !== null;
|
|
70
|
+
}
|
|
71
|
+
async function readJsonFile(file) {
|
|
72
|
+
try {
|
|
73
|
+
return JSON.parse(await promises.readFile(file, "utf8"));
|
|
74
|
+
} catch {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async function readTextFile(file) {
|
|
79
|
+
try {
|
|
80
|
+
return await promises.readFile(file, "utf8");
|
|
81
|
+
} catch {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/** package.json `scripts` as a plain string→string map (best-effort). */
|
|
86
|
+
async function readScripts(root) {
|
|
87
|
+
const pkg = await readJsonFile(path.join(root, "package.json"));
|
|
88
|
+
const scripts = pkg && typeof pkg === "object" ? pkg.scripts : void 0;
|
|
89
|
+
const out = {};
|
|
90
|
+
if (scripts && typeof scripts === "object") {
|
|
91
|
+
for (const [k, v] of Object.entries(scripts)) if (typeof v === "string") out[k] = v;
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
/** Pick the JS package runner from the lockfile present at the root. */
|
|
96
|
+
function nodeRunner(root) {
|
|
97
|
+
if (existsSync(path.join(root, "bun.lockb")) || existsSync(path.join(root, "bun.lock"))) return "bun";
|
|
98
|
+
if (existsSync(path.join(root, "pnpm-lock.yaml"))) return "pnpm";
|
|
99
|
+
if (existsSync(path.join(root, "yarn.lock"))) return "yarn";
|
|
100
|
+
return "npm";
|
|
101
|
+
}
|
|
102
|
+
/** Map a package.json script NAME to a canonical id (read-only checks only — a
|
|
103
|
+
* `:fix`/`:write` variant mutates the tree and is never a gate). */
|
|
104
|
+
function scriptNameToId(name) {
|
|
105
|
+
if (/:(fix|write)$/i.test(name)) return null;
|
|
106
|
+
if (/^(typecheck|type-check|check-types|tsc|types)$/i.test(name)) return "typecheck";
|
|
107
|
+
if (/^(lint|lint:check|eslint)$/i.test(name)) return "lint";
|
|
108
|
+
if (/^test$/i.test(name)) return "test";
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
async function collectNodeChecks(root, includeTests) {
|
|
112
|
+
if (!existsSync(path.join(root, "package.json"))) return [];
|
|
113
|
+
const runner = nodeRunner(root);
|
|
114
|
+
if (resolveExecutable(runner, { env: process.env }) === null) return [];
|
|
115
|
+
const scripts = await readScripts(root);
|
|
116
|
+
const out = [];
|
|
117
|
+
for (const [name] of Object.entries(scripts)) {
|
|
118
|
+
const id = scriptNameToId(name);
|
|
119
|
+
if (!id) continue;
|
|
120
|
+
if (id === "test" && !includeTests) continue;
|
|
121
|
+
const command = `${runner} run ${name}`;
|
|
122
|
+
if (isSafeCommand(command)) out.push({
|
|
123
|
+
id,
|
|
124
|
+
command,
|
|
125
|
+
source: "package.json"
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Best-effort CI gap-filler: scan GitHub Actions / GitLab CI YAML for single-line
|
|
132
|
+
* `run:` static checks (typecheck/lint/build) the project actually runs. We do
|
|
133
|
+
* NOT lift `test` from CI — CI test jobs frequently need services/secrets and
|
|
134
|
+
* would false-red on every stop. Multi-line / scripted `run:` blocks are skipped
|
|
135
|
+
* (they can't be a single argv). Any parse failure yields nothing.
|
|
136
|
+
*/
|
|
137
|
+
async function collectCiChecks(root) {
|
|
138
|
+
const files = [];
|
|
139
|
+
const wfDir = path.join(root, ".github", "workflows");
|
|
140
|
+
try {
|
|
141
|
+
for (const name of await promises.readdir(wfDir)) if (/\.ya?ml$/i.test(name)) files.push(path.join(wfDir, name));
|
|
142
|
+
} catch {}
|
|
143
|
+
const gitlab = path.join(root, ".gitlab-ci.yml");
|
|
144
|
+
if (existsSync(gitlab)) files.push(gitlab);
|
|
145
|
+
if (files.length === 0) return [];
|
|
146
|
+
let parseYaml;
|
|
147
|
+
try {
|
|
148
|
+
({parse: parseYaml} = await import("yaml"));
|
|
149
|
+
} catch {
|
|
150
|
+
return [];
|
|
151
|
+
}
|
|
152
|
+
const out = [];
|
|
153
|
+
const seen = /* @__PURE__ */ new Set();
|
|
154
|
+
const classify = (cmd) => {
|
|
155
|
+
if (/\b(typecheck|type-check|tsc|mypy|pyright)\b/i.test(cmd)) return "typecheck";
|
|
156
|
+
if (/\b(lint|eslint|ruff|clippy|golangci|vet)\b/i.test(cmd)) return "lint";
|
|
157
|
+
return null;
|
|
158
|
+
};
|
|
159
|
+
const consider = (run, source) => {
|
|
160
|
+
if (typeof run !== "string") return;
|
|
161
|
+
const cmd = run.trim();
|
|
162
|
+
if (cmd.includes("\n")) return;
|
|
163
|
+
if (!isSafeCommand(cmd) || isMutatingCommand(cmd) || !commandRunnable(cmd)) return;
|
|
164
|
+
const id = classify(cmd);
|
|
165
|
+
if (!id || seen.has(id)) return;
|
|
166
|
+
seen.add(id);
|
|
167
|
+
out.push({
|
|
168
|
+
id,
|
|
169
|
+
command: cmd,
|
|
170
|
+
source
|
|
171
|
+
});
|
|
172
|
+
};
|
|
173
|
+
const walk = (node, source) => {
|
|
174
|
+
if (!node || typeof node !== "object") return;
|
|
175
|
+
if (Array.isArray(node)) {
|
|
176
|
+
for (const v of node) walk(v, source);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
for (const [k, v] of Object.entries(node)) if (k === "run" && typeof v === "string") consider(v, source);
|
|
180
|
+
else if (k === "script") {
|
|
181
|
+
if (typeof v === "string") consider(v, source);
|
|
182
|
+
else if (Array.isArray(v)) for (const s of v) consider(s, source);
|
|
183
|
+
} else walk(v, source);
|
|
184
|
+
};
|
|
185
|
+
for (const f of files) {
|
|
186
|
+
const text = await readTextFile(f);
|
|
187
|
+
if (text === void 0) continue;
|
|
188
|
+
try {
|
|
189
|
+
walk(parseYaml(text), path.relative(root, f) || path.basename(f));
|
|
190
|
+
} catch {}
|
|
191
|
+
}
|
|
192
|
+
return out;
|
|
193
|
+
}
|
|
194
|
+
/** Make / just / Taskfile targets matching canonical ids → `<tool> <target>`. */
|
|
195
|
+
async function collectTaskChecks(root, includeTests) {
|
|
196
|
+
const out = [];
|
|
197
|
+
const wantId = (target) => scriptNameToId(target);
|
|
198
|
+
for (const [file, tool] of [
|
|
199
|
+
["Makefile", "make"],
|
|
200
|
+
["makefile", "make"],
|
|
201
|
+
["justfile", "just"],
|
|
202
|
+
[".justfile", "just"]
|
|
203
|
+
]) {
|
|
204
|
+
const text = await readTextFile(path.join(root, file));
|
|
205
|
+
if (text === void 0) continue;
|
|
206
|
+
if (resolveExecutable(tool, { env: process.env }) === null) continue;
|
|
207
|
+
for (const line of text.split(/\r?\n/)) {
|
|
208
|
+
const m = /^([A-Za-z0-9._-]+)\s*:/.exec(line);
|
|
209
|
+
if (!m) continue;
|
|
210
|
+
const id = wantId(m[1]);
|
|
211
|
+
if (!id || id === "test" && !includeTests) continue;
|
|
212
|
+
const command = `${tool} ${m[1]}`;
|
|
213
|
+
if (isSafeCommand(command)) out.push({
|
|
214
|
+
id,
|
|
215
|
+
command,
|
|
216
|
+
source: file
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
const taskfile = ["Taskfile.yml", "Taskfile.yaml"].map((n) => path.join(root, n)).find((p) => existsSync(p));
|
|
221
|
+
if (taskfile && resolveExecutable("task", { env: process.env }) !== null) {
|
|
222
|
+
const text = await readTextFile(taskfile);
|
|
223
|
+
if (text !== void 0) try {
|
|
224
|
+
const { parse } = await import("yaml");
|
|
225
|
+
const doc = parse(text);
|
|
226
|
+
for (const name of Object.keys(doc?.tasks ?? {})) {
|
|
227
|
+
const id = wantId(name);
|
|
228
|
+
if (!id || id === "test" && !includeTests) continue;
|
|
229
|
+
const command = `task ${name}`;
|
|
230
|
+
if (isSafeCommand(command)) out.push({
|
|
231
|
+
id,
|
|
232
|
+
command,
|
|
233
|
+
source: path.basename(taskfile)
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
} catch {}
|
|
237
|
+
}
|
|
238
|
+
return out;
|
|
239
|
+
}
|
|
240
|
+
async function collectRustChecks(root, includeTests) {
|
|
241
|
+
if (!existsSync(path.join(root, "Cargo.toml"))) return [];
|
|
242
|
+
if (resolveExecutable("cargo", { env: process.env }) === null) return [];
|
|
243
|
+
const out = [{
|
|
244
|
+
id: "typecheck",
|
|
245
|
+
command: "cargo check",
|
|
246
|
+
source: "Cargo.toml"
|
|
247
|
+
}];
|
|
248
|
+
if (resolveExecutable("cargo-clippy", { env: process.env }) !== null) out.push({
|
|
249
|
+
id: "lint",
|
|
250
|
+
command: "cargo clippy",
|
|
251
|
+
source: "Cargo.toml"
|
|
252
|
+
});
|
|
253
|
+
if (includeTests) out.push({
|
|
254
|
+
id: "test",
|
|
255
|
+
command: "cargo test",
|
|
256
|
+
source: "Cargo.toml"
|
|
257
|
+
});
|
|
258
|
+
return out;
|
|
259
|
+
}
|
|
260
|
+
async function collectGoChecks(root, includeTests) {
|
|
261
|
+
if (!existsSync(path.join(root, "go.mod"))) return [];
|
|
262
|
+
if (resolveExecutable("go", { env: process.env }) === null) return [];
|
|
263
|
+
const out = [{
|
|
264
|
+
id: "typecheck",
|
|
265
|
+
command: "go vet ./...",
|
|
266
|
+
source: "go.mod"
|
|
267
|
+
}];
|
|
268
|
+
if (includeTests) out.push({
|
|
269
|
+
id: "test",
|
|
270
|
+
command: "go test ./...",
|
|
271
|
+
source: "go.mod"
|
|
272
|
+
});
|
|
273
|
+
return out;
|
|
274
|
+
}
|
|
275
|
+
async function collectPythonChecks(root, includeTests) {
|
|
276
|
+
const present = [
|
|
277
|
+
"pyproject.toml",
|
|
278
|
+
"setup.cfg",
|
|
279
|
+
"pytest.ini",
|
|
280
|
+
"tox.ini",
|
|
281
|
+
"ruff.toml",
|
|
282
|
+
"mypy.ini"
|
|
283
|
+
].filter((f) => existsSync(path.join(root, f)));
|
|
284
|
+
if (present.length === 0) return [];
|
|
285
|
+
const evidence = present[0];
|
|
286
|
+
const out = [];
|
|
287
|
+
const configText = await readTextFile(path.join(root, present[0])) ?? "";
|
|
288
|
+
const mentions = (tool) => present.some((f) => f.startsWith(tool)) || configText.includes(tool);
|
|
289
|
+
if (mentions("mypy") && resolveExecutable("mypy", { env: process.env }) !== null) out.push({
|
|
290
|
+
id: "typecheck",
|
|
291
|
+
command: "mypy .",
|
|
292
|
+
source: evidence
|
|
293
|
+
});
|
|
294
|
+
if (mentions("ruff") && resolveExecutable("ruff", { env: process.env }) !== null) out.push({
|
|
295
|
+
id: "lint",
|
|
296
|
+
command: "ruff check .",
|
|
297
|
+
source: evidence
|
|
298
|
+
});
|
|
299
|
+
if (includeTests && (mentions("pytest") || existsSync(path.join(root, "pytest.ini")))) {
|
|
300
|
+
if (resolveExecutable("pytest", { env: process.env }) !== null) out.push({
|
|
301
|
+
id: "test",
|
|
302
|
+
command: "pytest -q",
|
|
303
|
+
source: evidence
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
return out;
|
|
307
|
+
}
|
|
308
|
+
/** Pick the first runnable candidate per id, in source-priority order. */
|
|
309
|
+
function pickByPriority(candidates) {
|
|
310
|
+
const byId = /* @__PURE__ */ new Map();
|
|
311
|
+
for (const c of candidates) if (!byId.has(c.id) && commandRunnable(c.command)) byId.set(c.id, c);
|
|
312
|
+
const order = [
|
|
313
|
+
"typecheck",
|
|
314
|
+
"lint",
|
|
315
|
+
"test"
|
|
316
|
+
];
|
|
317
|
+
const checks = [];
|
|
318
|
+
const evidence = /* @__PURE__ */ new Set();
|
|
319
|
+
for (const id of order) {
|
|
320
|
+
const c = byId.get(id);
|
|
321
|
+
if (c) {
|
|
322
|
+
checks.push({
|
|
323
|
+
id: c.id,
|
|
324
|
+
command: c.command
|
|
325
|
+
});
|
|
326
|
+
evidence.add(c.source);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return {
|
|
330
|
+
checks,
|
|
331
|
+
evidence: [...evidence]
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Resolve the gate descriptor for an already-resolved repo `root`.
|
|
336
|
+
* 1. bun/TS sealed fast-path — byte-identical to the legacy `detectHarnessGateId`
|
|
337
|
+
* (bun on PATH + a `typecheck` script → sealed `default-ci`/`typecheck-test`).
|
|
338
|
+
* 2. else the deterministic parser: collect candidates from package.json
|
|
339
|
+
* scripts (primary, self-contained), Make/just/task, language manifests, and
|
|
340
|
+
* a CI gap-filler for static checks; pick one per id by priority. A `parsed`
|
|
341
|
+
* descriptor is returned only when at least one STATIC check survives (a
|
|
342
|
+
* test-only set would either be off-by-default or risk false-reds).
|
|
343
|
+
* 3. else null (the launcher prints why and the gate stays off).
|
|
344
|
+
*/
|
|
345
|
+
async function parseGateDescriptor(root, opts) {
|
|
346
|
+
if (resolveExecutable("bun", { env: process.env }) !== null) {
|
|
347
|
+
const scripts = await readScripts(root);
|
|
348
|
+
if (typeof scripts.typecheck === "string") return {
|
|
349
|
+
kind: "sealed",
|
|
350
|
+
gateId: typeof scripts.lint === "string" ? "default-ci" : "typecheck-test",
|
|
351
|
+
workdir: root
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
const candidates = (await Promise.all([
|
|
355
|
+
collectNodeChecks(root, opts.includeTests),
|
|
356
|
+
collectTaskChecks(root, opts.includeTests),
|
|
357
|
+
collectRustChecks(root, opts.includeTests),
|
|
358
|
+
collectGoChecks(root, opts.includeTests),
|
|
359
|
+
collectPythonChecks(root, opts.includeTests),
|
|
360
|
+
collectCiChecks(root)
|
|
361
|
+
])).flat();
|
|
362
|
+
if (candidates.length === 0) return null;
|
|
363
|
+
const ecosystem = candidates.find((c) => c.source === "package.json") ? "node" : candidates.find((c) => c.source === "Cargo.toml") ? "rust" : candidates.find((c) => c.source === "go.mod") ? "go" : candidates.find((c) => /^(pyproject|setup|pytest|tox|ruff|mypy)/.test(c.source)) ? "python" : candidates.find((c) => /^(Makefile|makefile|justfile|\.justfile|Taskfile)/.test(c.source)) ? "make" : "ci";
|
|
364
|
+
const { checks, evidence } = pickByPriority(candidates);
|
|
365
|
+
if (checks.length === 0) return null;
|
|
366
|
+
if (!checks.some((c) => STATIC_IDS.has(c.id)) && !opts.includeTests) return null;
|
|
367
|
+
return {
|
|
368
|
+
kind: "parsed",
|
|
369
|
+
checks,
|
|
370
|
+
ecosystem,
|
|
371
|
+
workdir: root,
|
|
372
|
+
evidence
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
/** The checks a descriptor runs: a sealed descriptor resolves its sealed command
|
|
376
|
+
* set from the registry; parsed/discovered carry their own. Fresh array. */
|
|
377
|
+
function checksForDescriptor(d) {
|
|
378
|
+
if (d.kind === "sealed") {
|
|
379
|
+
const sealed = resolveSealedGate(d.gateId);
|
|
380
|
+
return sealed ? sealed.checks.map((c) => ({
|
|
381
|
+
id: c.id,
|
|
382
|
+
command: c.command
|
|
383
|
+
})) : [];
|
|
384
|
+
}
|
|
385
|
+
return d.checks.map((c) => ({
|
|
386
|
+
id: c.id,
|
|
387
|
+
command: c.command
|
|
388
|
+
}));
|
|
389
|
+
}
|
|
390
|
+
/** A stable key over a descriptor's effective check set, for baseline isolation.
|
|
391
|
+
* Sealed descriptors key on their gate id (preserving legacy baseline keys);
|
|
392
|
+
* parsed/discovered key on the canonicalized (id,command) set, so a changed
|
|
393
|
+
* command set yields a fresh baseline instead of masking/inventing regressions. */
|
|
394
|
+
function descriptorHash(d) {
|
|
395
|
+
if (d.kind === "sealed") return `sealed:${d.gateId}`;
|
|
396
|
+
const canon = [...d.checks].map((c) => `${c.id}${c.command.trim().replace(/\s+/g, " ")}`).sort().join("");
|
|
397
|
+
return `${d.kind}:${createHash("sha256").update(canon).digest("hex").slice(0, 32)}`;
|
|
398
|
+
}
|
|
399
|
+
//#endregion
|
|
400
|
+
//#region src/lib/orchestration/gate-discovery.ts
|
|
401
|
+
/**
|
|
402
|
+
* Evidence-pinned model FALLBACK for the structural Stop-gate — the last resort
|
|
403
|
+
* when the deterministic parser (`harness-parse`) finds no runnable checks.
|
|
404
|
+
*
|
|
405
|
+
* A read-only worker reads the repo's own config/docs and proposes the canonical
|
|
406
|
+
* check commands. Two guards keep this safe despite being model-authored:
|
|
407
|
+
* 1. SANITIZE — `sanitizeDiscoveredCheck` rejects shell metacharacters (so a
|
|
408
|
+
* command can never chain/redirect/expand), destructive/stateful verbs,
|
|
409
|
+
* interactive/watch shapes, and an executable that isn't on PATH. What
|
|
410
|
+
* survives is a single plain argv line, safe for `liveExec`'s naive split.
|
|
411
|
+
* 2. EVIDENCE-PIN — a surviving command must appear (whitespace-normalized)
|
|
412
|
+
* VERBATIM in one of the collected source files. The model cannot invent a
|
|
413
|
+
* command or be prompt-injected into emitting one that isn't already a real
|
|
414
|
+
* command in the repo (and a real command in a user-trusted repo is the
|
|
415
|
+
* same authority the existing gate already runs).
|
|
416
|
+
*
|
|
417
|
+
* Discovery runs ONCE at launch and the result is cached per (repoFingerprint,
|
|
418
|
+
* sourcesHash) in a human-readable record. The runtime Stop hook only READS the
|
|
419
|
+
* cached record (no model call at stop). This is consumed ONLY by the local Stop
|
|
420
|
+
* hook; the sealed-gate kernel never sees a discovered command.
|
|
421
|
+
*/
|
|
422
|
+
/** The allowlist of files the discovery worker is steered to read — config +
|
|
423
|
+
* docs that legitimately describe how to check a project. Secret files are
|
|
424
|
+
* additionally blocked at the worker IO layer (`.env*`/`*.pem`/`id_*`/…). */
|
|
425
|
+
const SIGNAL_FILES = [
|
|
426
|
+
"package.json",
|
|
427
|
+
"Makefile",
|
|
428
|
+
"makefile",
|
|
429
|
+
"justfile",
|
|
430
|
+
".justfile",
|
|
431
|
+
"Taskfile.yml",
|
|
432
|
+
"Taskfile.yaml",
|
|
433
|
+
"Cargo.toml",
|
|
434
|
+
"go.mod",
|
|
435
|
+
"pyproject.toml",
|
|
436
|
+
"setup.cfg",
|
|
437
|
+
"tox.ini",
|
|
438
|
+
"pytest.ini",
|
|
439
|
+
"CONTRIBUTING.md",
|
|
440
|
+
"CONTRIBUTING",
|
|
441
|
+
"README.md",
|
|
442
|
+
"README",
|
|
443
|
+
"DEVELOPING.md",
|
|
444
|
+
"mix.exs",
|
|
445
|
+
"build.gradle",
|
|
446
|
+
"pom.xml",
|
|
447
|
+
"composer.json"
|
|
448
|
+
];
|
|
449
|
+
const SIGNAL_DIRS = [".github/workflows"];
|
|
450
|
+
const MAX_SIGNAL_BYTES = 65536;
|
|
451
|
+
/** Global caps so a repo with many workflow files can't inflate discovery /
|
|
452
|
+
* hashing cost: at most this many files and this much total text. */
|
|
453
|
+
const MAX_SIGNAL_FILES = 40;
|
|
454
|
+
const MAX_TOTAL_SIGNAL_BYTES = 524288;
|
|
455
|
+
/** Words that mark a command as destructive, stateful, or non-terminating — a
|
|
456
|
+
* "check" must never do any of these. (Shell operators are already rejected by
|
|
457
|
+
* `isSafeCommand`, so chaining can't smuggle them past this word scan.) */
|
|
458
|
+
const DENY_WORD = /\b(rm|rmdir|mv|dd|mkfs|sudo|chmod|chown|publish|push|deploy|migrate|kubectl|terraform|docker|curl|wget|ssh|scp|rsync|nc|eval|npm i|npm install|yarn add|pip install|apt|brew|watch|serve|repl|dev|start)\b/i;
|
|
459
|
+
/**
|
|
460
|
+
* True when `command` is safe to auto-run as a check: a plain argv line (no
|
|
461
|
+
* shell metacharacters), no destructive/stateful verb, no mutating/interactive
|
|
462
|
+
* shape, and its executable resolves on PATH. The deny-word scan runs on a
|
|
463
|
+
* whitespace-NORMALIZED copy so a tab / double-space can't split `npm install`
|
|
464
|
+
* past the literal-space patterns.
|
|
465
|
+
*/
|
|
466
|
+
function sanitizeDiscoveredCheck(command) {
|
|
467
|
+
if (!isSafeCommand(command)) return false;
|
|
468
|
+
const norm = normalizeWs(command);
|
|
469
|
+
if (DENY_WORD.test(norm)) return false;
|
|
470
|
+
if (isMutatingCommand(norm)) return false;
|
|
471
|
+
const first = norm.split(" ")[0] ?? "";
|
|
472
|
+
return resolveExecutable(first, { env: process.env }) !== null;
|
|
473
|
+
}
|
|
474
|
+
function normalizeWs(s) {
|
|
475
|
+
return s.replace(/\s+/g, " ").trim();
|
|
476
|
+
}
|
|
477
|
+
/** Collect the text of the allowlisted signal files (capped), for evidence-pin +
|
|
478
|
+
* the sources hash. Returns the concatenated text and the relative file list. */
|
|
479
|
+
async function collectSignals(root) {
|
|
480
|
+
const parts = [];
|
|
481
|
+
const files = [];
|
|
482
|
+
let total = 0;
|
|
483
|
+
const readCapped = async (abs, rel) => {
|
|
484
|
+
if (files.length >= MAX_SIGNAL_FILES || total >= MAX_TOTAL_SIGNAL_BYTES) return;
|
|
485
|
+
try {
|
|
486
|
+
const raw = await promises.readFile(abs, "utf8");
|
|
487
|
+
const slice = raw.length > MAX_SIGNAL_BYTES ? raw.slice(0, MAX_SIGNAL_BYTES) : raw;
|
|
488
|
+
parts.push(slice);
|
|
489
|
+
files.push(rel);
|
|
490
|
+
total += slice.length;
|
|
491
|
+
} catch {}
|
|
492
|
+
};
|
|
493
|
+
for (const f of SIGNAL_FILES) {
|
|
494
|
+
const abs = path.join(root, f);
|
|
495
|
+
if (existsSync(abs)) await readCapped(abs, f);
|
|
496
|
+
}
|
|
497
|
+
for (const d of SIGNAL_DIRS) {
|
|
498
|
+
const dir = path.join(root, d);
|
|
499
|
+
try {
|
|
500
|
+
for (const name of await promises.readdir(dir)) if (/\.(ya?ml)$/i.test(name)) await readCapped(path.join(dir, name), path.join(d, name));
|
|
501
|
+
} catch {}
|
|
502
|
+
}
|
|
503
|
+
return {
|
|
504
|
+
text: parts.join("\n"),
|
|
505
|
+
files
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
/** A freshness hash over the signal files' contents + relative paths. A change
|
|
509
|
+
* to how the project checks itself flips this → re-discovery on next launch. */
|
|
510
|
+
async function sourcesHash(root) {
|
|
511
|
+
const { text, files } = await collectSignals(root);
|
|
512
|
+
return createHash("sha256").update(files.sort().join("\n")).update("\0").update(text).digest("hex");
|
|
513
|
+
}
|
|
514
|
+
function discoveredDir() {
|
|
515
|
+
return path.join(PATHS.APP_DIR, "stop-gate", "discovered");
|
|
516
|
+
}
|
|
517
|
+
function recordPathFor(root) {
|
|
518
|
+
return path.join(discoveredDir(), createHash("sha256").update(path.resolve(root)).digest("hex").slice(0, 32));
|
|
519
|
+
}
|
|
520
|
+
/** Read the cached discovered record for `root`, verifying it still matches the
|
|
521
|
+
* live repo identity AND the live sources hash. Any mismatch / unreadable /
|
|
522
|
+
* empty-checks record → null (re-discover or stay off; never run a stale set). */
|
|
523
|
+
async function readDiscoveredGate(root) {
|
|
524
|
+
let rec;
|
|
525
|
+
try {
|
|
526
|
+
rec = JSON.parse(await promises.readFile(recordPathFor(root), "utf8"));
|
|
527
|
+
} catch {
|
|
528
|
+
return null;
|
|
529
|
+
}
|
|
530
|
+
if (!rec || !Array.isArray(rec.checks) || rec.checks.length === 0) return null;
|
|
531
|
+
const fp = await repoFingerprint(root).catch(() => "");
|
|
532
|
+
if (fp.length === 0 || fp !== rec.fingerprint) return null;
|
|
533
|
+
const sh = await sourcesHash(root).catch(() => "");
|
|
534
|
+
if (sh.length === 0 || sh !== rec.sourcesHash) return null;
|
|
535
|
+
const checks = rec.checks.filter((c) => c && typeof c.id === "string" && VALID_IDS.has(c.id) && typeof c.command === "string" && sanitizeDiscoveredCheck(c.command));
|
|
536
|
+
if (checks.length === 0) return null;
|
|
537
|
+
return {
|
|
538
|
+
...rec,
|
|
539
|
+
checks
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
async function writeDiscoveredGate(rec) {
|
|
543
|
+
await promises.mkdir(discoveredDir(), { recursive: true });
|
|
544
|
+
const tmp = `${recordPathFor(rec.root)}.${process.pid}.tmp`;
|
|
545
|
+
await promises.writeFile(tmp, `${JSON.stringify(rec, null, 2)}\n`, { mode: 384 });
|
|
546
|
+
await promises.rename(tmp, recordPathFor(rec.root));
|
|
547
|
+
}
|
|
548
|
+
const DISCOVERY_PROMPT = (files) => `You are configuring an automated pre-finish CHECK gate for this repository. Read ONLY these already-present config/doc files to learn how the project checks itself: ${files.join(", ")}. Identify the canonical FAST static check command (typecheck and/or lint) and, if obvious, the test command. Rules: (1) return a command ONLY if it appears VERBATIM in one of those files — never invent one; (2) commands must be non-interactive, self-terminating, read-only verification (NO install / publish / push / deploy / migrate / format-in-place / watch / serve / dev-server / delete); (3) at most 3 commands; (4) if unsure, return an empty list. Respond with ONLY a fenced \`\`\`json block of the shape {"ecosystem":"<label>","checks":[{"id":"typecheck|lint|test","command":"<single-line command>"}],"confidence":"high|low"} and NOTHING else.`;
|
|
549
|
+
/** Extract the first fenced/bare JSON object from the worker's text. */
|
|
550
|
+
function extractJson(text) {
|
|
551
|
+
const fenced = /```(?:json)?\s*([\s\S]*?)```/i.exec(text);
|
|
552
|
+
const body = fenced ? fenced[1] : text;
|
|
553
|
+
const start = body.indexOf("{");
|
|
554
|
+
const end = body.lastIndexOf("}");
|
|
555
|
+
if (start < 0 || end <= start) return void 0;
|
|
556
|
+
try {
|
|
557
|
+
return JSON.parse(body.slice(start, end + 1));
|
|
558
|
+
} catch {
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
const VALID_IDS = /* @__PURE__ */ new Set([
|
|
563
|
+
"typecheck",
|
|
564
|
+
"lint",
|
|
565
|
+
"test"
|
|
566
|
+
]);
|
|
567
|
+
/**
|
|
568
|
+
* Sanitize + EVIDENCE-PIN the model's raw checks against the collected source
|
|
569
|
+
* text. Pure (no IO) so it is unit-testable without the worker. A check survives
|
|
570
|
+
* only if: its id is canonical, it isn't a `test` while tests are off, its id
|
|
571
|
+
* hasn't already been taken, it passes `sanitizeDiscoveredCheck`, AND its command
|
|
572
|
+
* appears (whitespace-normalized) VERBATIM in `evidenceText`. The evidence-pin is
|
|
573
|
+
* the load-bearing guard: the model cannot invent a command or be prompt-injected
|
|
574
|
+
* into one that isn't already a real command in the (user-trusted) repo.
|
|
575
|
+
*/
|
|
576
|
+
function filterDiscoveredChecks(rawChecks, evidenceText, includeTests) {
|
|
577
|
+
const evidenceNorm = normalizeWs(evidenceText);
|
|
578
|
+
const seen = /* @__PURE__ */ new Set();
|
|
579
|
+
const checks = [];
|
|
580
|
+
for (const c of Array.isArray(rawChecks) ? rawChecks : []) {
|
|
581
|
+
if (!c || typeof c !== "object") continue;
|
|
582
|
+
const id = c.id;
|
|
583
|
+
const command = c.command;
|
|
584
|
+
if (typeof id !== "string" || !VALID_IDS.has(id)) continue;
|
|
585
|
+
if (typeof command !== "string") continue;
|
|
586
|
+
if (id === "test" && !includeTests) continue;
|
|
587
|
+
if (seen.has(id)) continue;
|
|
588
|
+
if (!sanitizeDiscoveredCheck(command)) continue;
|
|
589
|
+
if (!evidenceNorm.includes(normalizeWs(command))) continue;
|
|
590
|
+
seen.add(id);
|
|
591
|
+
checks.push({
|
|
592
|
+
id,
|
|
593
|
+
command: command.trim()
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
return checks;
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* Run the read-only worker to discover check commands for `cwd`. Returns the
|
|
600
|
+
* sanitized + evidence-pinned result, or null (worker unavailable / errored /
|
|
601
|
+
* nothing survived). NEVER throws.
|
|
602
|
+
*/
|
|
603
|
+
async function discoverGateCommands(cwd, opts) {
|
|
604
|
+
const root = await repoRoot(cwd).catch(() => cwd);
|
|
605
|
+
const { text: evidenceText, files } = await collectSignals(root);
|
|
606
|
+
if (files.length === 0) return null;
|
|
607
|
+
let result;
|
|
608
|
+
try {
|
|
609
|
+
const { runWorkerAgent } = await import("./engine-DNCX0Hfd.js");
|
|
610
|
+
result = await runWorkerAgent({
|
|
611
|
+
mode: "explore",
|
|
612
|
+
workspace: root,
|
|
613
|
+
prompt: DISCOVERY_PROMPT(files),
|
|
614
|
+
signal: opts.signal
|
|
615
|
+
});
|
|
616
|
+
} catch {
|
|
617
|
+
return null;
|
|
618
|
+
}
|
|
619
|
+
if (result.isError) return null;
|
|
620
|
+
const parsed = extractJson(result.text);
|
|
621
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
622
|
+
const obj = parsed;
|
|
623
|
+
const checks = filterDiscoveredChecks(obj.checks, evidenceText, opts.includeTests);
|
|
624
|
+
if (checks.length === 0) return null;
|
|
625
|
+
return {
|
|
626
|
+
ecosystem: typeof obj.ecosystem === "string" && obj.ecosystem.length > 0 ? obj.ecosystem : "discovered",
|
|
627
|
+
checks,
|
|
628
|
+
confidence: typeof obj.confidence === "string" ? obj.confidence : "low",
|
|
629
|
+
evidence: files
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
//#endregion
|
|
633
|
+
export { checksForDescriptor as a, writeDiscoveredGate as i, readDiscoveredGate as n, descriptorHash as o, sourcesHash as r, parseGateDescriptor as s, discoverGateCommands as t };
|
|
634
|
+
|
|
635
|
+
//# sourceMappingURL=gate-discovery-CepmbZnc.js.map
|