argus-reviewer-e2e 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +84 -71
- package/action/action.yml +129 -11
- package/action/approval-review.mjs +13 -3
- package/action/bootstrap.mjs +2 -0
- package/action/emit-review.mjs +16 -0
- package/action/runtime.mjs +20 -0
- package/action/sticky-comment.cjs +1260 -479
- package/dist/cli.d.ts +103 -7
- package/dist/cli.js +1202 -186
- package/dist/config.d.ts +96 -11
- package/dist/config.js +102 -4
- package/dist/detect.d.ts +29 -2
- package/dist/detect.js +98 -7
- package/dist/driver/browser.d.ts +32 -0
- package/dist/driver/browser.js +56 -1
- package/dist/driver/target.d.ts +4 -1
- package/dist/driver/target.js +27 -6
- package/dist/engine/actions.d.ts +5 -0
- package/dist/engine/actions.js +8 -0
- package/dist/engine/explore.d.ts +78 -0
- package/dist/engine/explore.js +373 -0
- package/dist/engine/loop.d.ts +2 -2
- package/dist/engine/loop.js +8 -8
- package/dist/engine/prompts.d.ts +28 -1
- package/dist/engine/prompts.js +88 -0
- package/dist/evidence/ci.d.ts +13 -1
- package/dist/evidence/ci.js +38 -3
- package/dist/evidence/gate.d.ts +8 -0
- package/dist/evidence/gate.js +1 -1
- package/dist/evidence/link.js +1 -1
- package/dist/executor/a0.d.ts +114 -1
- package/dist/executor/a0.js +216 -4
- package/dist/fsutil.d.ts +3 -2
- package/dist/fsutil.js +7 -4
- package/dist/journal/schema.d.ts +1 -1
- package/dist/log.d.ts +2 -1
- package/dist/log.js +10 -2
- package/dist/mention.d.ts +45 -0
- package/dist/mention.js +107 -0
- package/dist/pipeline/app.d.ts +126 -0
- package/dist/pipeline/app.js +250 -0
- package/dist/pipeline/budget.d.ts +1 -0
- package/dist/pipeline/budget.js +1 -1
- package/dist/pipeline/verify.d.ts +20 -3
- package/dist/pipeline/verify.js +189 -35
- package/dist/probe/persist.d.ts +68 -0
- package/dist/probe/persist.js +184 -0
- package/dist/probe/queue.d.ts +12 -0
- package/dist/probe/queue.js +10 -2
- package/dist/report/brand-assets.generated.d.ts +9 -0
- package/dist/report/brand-assets.generated.js +8 -0
- package/dist/report/comment.d.ts +99 -6
- package/dist/report/comment.js +292 -103
- package/dist/report/html.d.ts +50 -0
- package/dist/report/html.js +879 -0
- package/dist/report/manifest.d.ts +29 -0
- package/dist/report/manifest.js +37 -0
- package/dist/report/run.d.ts +54 -1
- package/dist/report/run.js +34 -9
- package/dist/report/viewmodel.d.ts +91 -0
- package/dist/report/viewmodel.js +241 -0
- package/dist/review/adjudicate.d.ts +6 -6
- package/dist/review/adjudicate.js +2 -2
- package/dist/review/inline.d.ts +44 -0
- package/dist/review/inline.js +95 -0
- package/dist/review/packs.d.ts +21 -0
- package/dist/review/packs.js +47 -0
- package/dist/review/scope.d.ts +16 -0
- package/dist/review/scope.js +74 -0
- package/dist/review/secrets.d.ts +10 -10
- package/dist/review/secrets.js +7 -7
- package/dist/review/testfiles.d.ts +18 -0
- package/dist/review/testfiles.js +26 -0
- package/dist/review/triage.d.ts +1 -1
- package/dist/review/triage.js +10 -10
- package/dist/review/validate.d.ts +41 -0
- package/dist/review/validate.js +76 -0
- package/dist/ui/errors.d.ts +54 -0
- package/dist/ui/errors.js +236 -0
- package/dist/ui/style.d.ts +34 -0
- package/dist/ui/style.js +48 -0
- package/dist/ui/summary.d.ts +38 -0
- package/dist/ui/summary.js +101 -0
- package/dist/vision/cost.d.ts +1 -1
- package/dist/vision/decisions.d.ts +9 -3
- package/dist/vision/decisions.js +31 -21
- package/dist/vision/openrouter.d.ts +4 -0
- package/dist/vision/openrouter.js +30 -4
- package/package.json +11 -2
package/dist/executor/a0.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import { defaultExec } from '../detect.js';
|
|
1
|
+
import { buildA0ChildEnv, defaultExec, defaultProbe, resolveA0Host, } from '../detect.js';
|
|
2
|
+
// The allowlist lives in detect.ts beside the exec seam — re-exported here
|
|
3
|
+
// so `executor/a0` stays the single import site for delegation internals.
|
|
4
|
+
export { buildA0ChildEnv };
|
|
2
5
|
/**
|
|
3
6
|
* Agent Zero delegation — the thin seam that hands a natural-language task to
|
|
4
7
|
* an `a0` instance (`a0 headless -p`). The instance runs autonomously inside
|
|
@@ -8,8 +11,18 @@ import { defaultExec } from '../detect.js';
|
|
|
8
11
|
* non-deterministic agent run. Fingerprint replay stays local and ~free; A0 is
|
|
9
12
|
* for healing, second opinions on failures, and exploratory tasks that were
|
|
10
13
|
* never recorded.
|
|
14
|
+
*
|
|
15
|
+
* The `verify --a0` lane caps completed delegations at `inconclusive`: the
|
|
16
|
+
* host round-trip is proven (#53), but the agent's answer is self-reported
|
|
17
|
+
* evidence, never a `passed` verdict.
|
|
11
18
|
*/
|
|
12
19
|
export const A0_DEFAULT_TIMEOUT_MS = 600_000;
|
|
20
|
+
/** Delegations a `verify --a0` lane may run — config.a0.maxTasks overrides. */
|
|
21
|
+
export const A0_LANE_MAX_TASKS = 1;
|
|
22
|
+
/** Lane detail file the a0 runner writes and `runVerify` reads back. */
|
|
23
|
+
export const A0_LANE_REPORT = 'a0-lane.json';
|
|
24
|
+
/** Reported posture: the round-trip is verified (#53); the answer is not. */
|
|
25
|
+
export const A0_LIVE_LABEL = 'self-reported';
|
|
13
26
|
export function buildA0Args(prompt, host) {
|
|
14
27
|
const args = ['headless', '--new-chat', '--output', 'text'];
|
|
15
28
|
if (host !== undefined && host !== '')
|
|
@@ -19,16 +32,28 @@ export function buildA0Args(prompt, host) {
|
|
|
19
32
|
}
|
|
20
33
|
export async function runA0Task(prompt, opts = {}) {
|
|
21
34
|
const exec = opts.exec ?? defaultExec;
|
|
35
|
+
// The allowlisted child env is the default, not the opt-in — every
|
|
36
|
+
// delegation path (lane, heal, delegate) gets the R12 sanitization
|
|
37
|
+
// unless a caller deliberately passes a different env.
|
|
38
|
+
const baseEnv = opts.env ?? buildA0ChildEnv(process.env);
|
|
22
39
|
let res;
|
|
23
40
|
try {
|
|
24
|
-
res = await exec(opts.cli ?? 'a0', buildA0Args(prompt, opts.host), opts.timeoutMs ?? A0_DEFAULT_TIMEOUT_MS);
|
|
41
|
+
res = await exec(opts.cli ?? 'a0', buildA0Args(prompt, opts.host), opts.timeoutMs ?? A0_DEFAULT_TIMEOUT_MS, undefined, { baseEnv });
|
|
25
42
|
}
|
|
26
43
|
catch (e) {
|
|
27
44
|
// Spawn rejection (ENOENT when a0 is absent, hard timeout) must degrade
|
|
28
45
|
// to a failed delegation, not abort the calling command.
|
|
29
|
-
return { ok: false, output: e.message };
|
|
46
|
+
return { ok: false, output: e.message, spawnError: true };
|
|
30
47
|
}
|
|
31
|
-
return {
|
|
48
|
+
return {
|
|
49
|
+
ok: res.code === 0,
|
|
50
|
+
output: res.stdout.trim() || res.stderr.trim(),
|
|
51
|
+
timedOut: res.timedOut,
|
|
52
|
+
// ExecResult.spawnError is authoritative when the executor sets it —
|
|
53
|
+
// a completed run that merely prints "not found" to stderr must not
|
|
54
|
+
// misclassify. The sniff remains for injected execs without the field.
|
|
55
|
+
spawnError: res.spawnError ?? /ENOENT|not found|no such file/i.test(res.stderr),
|
|
56
|
+
};
|
|
32
57
|
}
|
|
33
58
|
/** Prompt wrapper: bind the task to an app URL when one is known. */
|
|
34
59
|
export function a0TaskPrompt(task, url) {
|
|
@@ -36,3 +61,190 @@ export function a0TaskPrompt(task, url) {
|
|
|
36
61
|
? task
|
|
37
62
|
: `Open ${url} in your browser, then do this task: ${task}`;
|
|
38
63
|
}
|
|
64
|
+
/** Render the typed payload into the delegated prompt — sanitized surface. */
|
|
65
|
+
export function buildA0LanePrompt(payload) {
|
|
66
|
+
const lines = [
|
|
67
|
+
'You are an escalation agent for the Argus PR reviewer.',
|
|
68
|
+
`Task: ${payload.task}`,
|
|
69
|
+
];
|
|
70
|
+
if (payload.targetUrl !== undefined && payload.targetUrl !== '') {
|
|
71
|
+
lines.push(`Target: open ${payload.targetUrl} and work against that application.`);
|
|
72
|
+
}
|
|
73
|
+
if (payload.intendedHeadSha !== undefined && payload.intendedHeadSha !== '') {
|
|
74
|
+
lines.push(`The evidence must describe the PR head ${payload.intendedHeadSha}.`);
|
|
75
|
+
}
|
|
76
|
+
if (payload.failureSummary !== undefined && payload.failureSummary !== '') {
|
|
77
|
+
lines.push(`Local failure being escalated: ${payload.failureSummary}`);
|
|
78
|
+
}
|
|
79
|
+
if (payload.evidenceRefs !== undefined && payload.evidenceRefs.length > 0) {
|
|
80
|
+
lines.push(`Local evidence you may consult: ${payload.evidenceRefs.join(', ')}`);
|
|
81
|
+
}
|
|
82
|
+
lines.push('Report concisely what you observed and whether the task goal is met.', 'Do not attempt to access provider keys, tokens, or repository secrets.');
|
|
83
|
+
return lines.join('\n');
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Whether `url` names a loopback target — the remote-a0/loopback-target
|
|
87
|
+
* refusal depends on this being complete: the whole 127.0.0.0/8 range,
|
|
88
|
+
* wildcard/zero hosts, `*.localhost`, and IPv4-mapped forms, not just the
|
|
89
|
+
* canonical `127.0.0.1`/`localhost` literals. `file:` URLs count too — a
|
|
90
|
+
* remote host's filesystem is not this machine's. DNS names that merely
|
|
91
|
+
* resolve to loopback are not caught (no lookup by design — fail-open on
|
|
92
|
+
* hostnames is deliberate here).
|
|
93
|
+
*/
|
|
94
|
+
export function isLoopback(url) {
|
|
95
|
+
try {
|
|
96
|
+
if (new URL(url).protocol === 'file:')
|
|
97
|
+
return true;
|
|
98
|
+
const host = new URL(url).hostname.replace(/\.$/, '').toLowerCase();
|
|
99
|
+
if (host === 'localhost' || host.endsWith('.localhost'))
|
|
100
|
+
return true;
|
|
101
|
+
// Node keeps IPv6 brackets in .hostname and normalizes mapped forms:
|
|
102
|
+
// [::1] stays, [::ffff:127.0.0.1] arrives as [::ffff:7f00:1], and
|
|
103
|
+
// short/integer IPv4 (127.1, 2130706433) normalizes to dotted-quad.
|
|
104
|
+
if (host === '[::1]' || host === '[::]')
|
|
105
|
+
return true;
|
|
106
|
+
const mapped = host.match(/^\[::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})\]$/i);
|
|
107
|
+
if (mapped !== null) {
|
|
108
|
+
const hi = parseInt(mapped[1] ?? 'x', 16);
|
|
109
|
+
const lo = parseInt(mapped[2] ?? 'x', 16);
|
|
110
|
+
const first = (hi >> 8) & 0xff;
|
|
111
|
+
if (first === 127 || (hi === 0 && lo === 0))
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
const v4 = host.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
|
115
|
+
if (v4 !== null) {
|
|
116
|
+
const octets = v4.slice(1).map((g) => Number(g ?? ''));
|
|
117
|
+
if (octets.every((n) => Number.isInteger(n) && n <= 255) && (octets[0] === 127 || octets[0] === 0)) {
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* `verify --a0` lane: explicit selection only, every preflight outcome
|
|
129
|
+
* recorded, zero spend before the host is proven reachable, and a hard
|
|
130
|
+
* `inconclusive` ceiling — a completed delegation is evidence, and the
|
|
131
|
+
* agent's report of what it saw is self-reported, not a verdict.
|
|
132
|
+
*/
|
|
133
|
+
export async function runA0Lane(input) {
|
|
134
|
+
const started = Date.now();
|
|
135
|
+
const deps = input.deps ?? {};
|
|
136
|
+
const note = deps.note ?? (() => undefined);
|
|
137
|
+
const done = (status, reason, extra) => ({
|
|
138
|
+
lane: 'a0',
|
|
139
|
+
status,
|
|
140
|
+
reason,
|
|
141
|
+
summary: status === 'inconclusive' ? `delegation returned — ${A0_LIVE_LABEL}` : reason,
|
|
142
|
+
host: undefined,
|
|
143
|
+
hostSource: undefined,
|
|
144
|
+
targetReachable: undefined,
|
|
145
|
+
task: input.task,
|
|
146
|
+
output: undefined,
|
|
147
|
+
tasks: 0,
|
|
148
|
+
durationMs: Date.now() - started,
|
|
149
|
+
metered: false,
|
|
150
|
+
...extra,
|
|
151
|
+
});
|
|
152
|
+
// Executable lanes are gated on the same trust resolution as config
|
|
153
|
+
// loading (SECURITY.md): an untrusted checkout never resolves a host or
|
|
154
|
+
// spawns the a0 CLI — blocked before any preflight.
|
|
155
|
+
if (!input.trusted) {
|
|
156
|
+
return done('blocked', 'verify --a0 is an executable lane — requires a trusted checkout');
|
|
157
|
+
}
|
|
158
|
+
if (input.task === undefined || input.task.trim() === '') {
|
|
159
|
+
return done('blocked', 'no delegated task configured — set a0 task via config app.task or the app lane');
|
|
160
|
+
}
|
|
161
|
+
// Preflight 1: host resolution — config wins, then the CLI's own chain
|
|
162
|
+
// (env, ~/.agent-zero/.env, localhost probe). Missing both ends = nothing
|
|
163
|
+
// to reach. A configured a0.url skips the probe chain entirely.
|
|
164
|
+
const resolveHost = deps.resolveHost ??
|
|
165
|
+
(async () => {
|
|
166
|
+
const r = await resolveA0Host(input.env, {
|
|
167
|
+
...(deps.probe !== undefined ? { probe: deps.probe } : {}),
|
|
168
|
+
});
|
|
169
|
+
return { host: r.host, source: r.source };
|
|
170
|
+
});
|
|
171
|
+
const cliVersion = deps.cliVersion ??
|
|
172
|
+
(async () => {
|
|
173
|
+
// The presence probe spawns the a0 binary too — it gets the same
|
|
174
|
+
// allowlisted env as the delegation or the whole contract is moot.
|
|
175
|
+
const res = await (deps.exec ?? defaultExec)(deps.cli ?? 'a0', ['--version'], 5_000, undefined, { baseEnv: buildA0ChildEnv(input.env) });
|
|
176
|
+
return res.code === 0 ? res.stdout.trim() : undefined;
|
|
177
|
+
});
|
|
178
|
+
const configured = input.a0?.url !== undefined && input.a0.url !== '';
|
|
179
|
+
const [resolved, version] = await Promise.all([
|
|
180
|
+
configured ? Promise.resolve({ host: undefined, source: undefined }) : resolveHost(),
|
|
181
|
+
cliVersion(),
|
|
182
|
+
]);
|
|
183
|
+
const host = input.a0?.url ?? resolved.host;
|
|
184
|
+
const hostSource = input.a0?.url !== undefined ? 'config' : resolved.source;
|
|
185
|
+
if (version === undefined && host === undefined) {
|
|
186
|
+
return done('unavailable', 'no Agent Zero found — install the a0 CLI or set a0.url / AGENT_ZERO_HOST');
|
|
187
|
+
}
|
|
188
|
+
if (host === undefined) {
|
|
189
|
+
// CLI present, host unresolved — the CLI may still self-resolve, but the
|
|
190
|
+
// lane must prove a host before spending: record the gap honestly.
|
|
191
|
+
return done('unavailable', 'a0 CLI is installed but no host resolved — set AGENT_ZERO_HOST or a0.url');
|
|
192
|
+
}
|
|
193
|
+
// Preflight 2: the host must answer as Agent Zero — a URL that serves
|
|
194
|
+
// something else is worse than no host.
|
|
195
|
+
const probe = deps.probe ?? defaultProbe;
|
|
196
|
+
if (!(await probe(host, 5_000))) {
|
|
197
|
+
return done('unavailable', `a0 host did not answer as Agent Zero: ${host}`, {
|
|
198
|
+
host,
|
|
199
|
+
hostSource,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
// Preflight 3: can the host reach the target? A remote a0 cannot drive a
|
|
203
|
+
// loopback app on this machine — that's a scope refusal, not a failure.
|
|
204
|
+
if (input.targetUrl !== undefined &&
|
|
205
|
+
isLoopback(input.targetUrl) &&
|
|
206
|
+
!isLoopback(host)) {
|
|
207
|
+
return done('blocked', `a0 host ${host} is remote but the target ${input.targetUrl} is loopback — the host cannot reach it`, { host, hostSource, targetReachable: 'refused' });
|
|
208
|
+
}
|
|
209
|
+
const maxTasks = input.a0?.maxTasks ?? A0_LANE_MAX_TASKS;
|
|
210
|
+
const timeoutMs = input.a0?.timeoutMs ?? A0_DEFAULT_TIMEOUT_MS;
|
|
211
|
+
note(`a0 lane: delegating 1 task to ${host} (max ${maxTasks}, ` +
|
|
212
|
+
`${Math.round(timeoutMs / 1000)}s wall-clock, usage unmetered, ${A0_LIVE_LABEL})`);
|
|
213
|
+
const payload = buildA0LanePrompt({
|
|
214
|
+
task: input.task,
|
|
215
|
+
targetUrl: input.targetUrl,
|
|
216
|
+
intendedHeadSha: input.intendedHeadSha,
|
|
217
|
+
failureSummary: input.failureSummary ?? undefined,
|
|
218
|
+
evidenceRefs: undefined,
|
|
219
|
+
});
|
|
220
|
+
const res = await runA0Task(payload, {
|
|
221
|
+
host,
|
|
222
|
+
timeoutMs,
|
|
223
|
+
env: buildA0ChildEnv(input.env),
|
|
224
|
+
...(deps.exec !== undefined ? { exec: deps.exec } : {}),
|
|
225
|
+
...(deps.cli !== undefined ? { cli: deps.cli } : {}),
|
|
226
|
+
});
|
|
227
|
+
if (res.spawnError === true) {
|
|
228
|
+
return done('unavailable', `a0 delegation could not start: ${res.output}`, {
|
|
229
|
+
host,
|
|
230
|
+
hostSource,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
if (res.timedOut === true) {
|
|
234
|
+
return done('inconclusive', `a0 delegation timed out after ${timeoutMs}ms`, {
|
|
235
|
+
host,
|
|
236
|
+
hostSource,
|
|
237
|
+
output: res.output !== '' ? res.output : undefined,
|
|
238
|
+
tasks: 1,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
// Every completed delegation — success or reported failure — is evidence
|
|
242
|
+
// whose truth rests on the agent's own report: inconclusive, never passed.
|
|
243
|
+
return done('inconclusive', res.ok ? undefined : `a0 delegation reported failure: ${res.output}`, {
|
|
244
|
+
host,
|
|
245
|
+
hostSource,
|
|
246
|
+
targetReachable: input.targetUrl !== undefined ? 'assumed' : 'unchecked',
|
|
247
|
+
output: res.output !== '' ? res.output : undefined,
|
|
248
|
+
tasks: 1,
|
|
249
|
+
});
|
|
250
|
+
}
|
package/dist/fsutil.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Atomic
|
|
3
|
-
* Callers: cache store, journal store, repo index.
|
|
2
|
+
* Atomic text write via tmp+rename — the one place the pattern lives.
|
|
3
|
+
* Callers: cache store, journal store, repo index, evidence report.
|
|
4
4
|
*/
|
|
5
|
+
export declare function writeAtomicText(path: string, text: string): Promise<void>;
|
|
5
6
|
export declare function writeAtomicJson(path: string, value: unknown, replacer?: (key: string, v: unknown) => unknown): Promise<void>;
|
package/dist/fsutil.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { mkdir, rename, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { dirname } from 'node:path';
|
|
3
3
|
/**
|
|
4
|
-
* Atomic
|
|
5
|
-
* Callers: cache store, journal store, repo index.
|
|
4
|
+
* Atomic text write via tmp+rename — the one place the pattern lives.
|
|
5
|
+
* Callers: cache store, journal store, repo index, evidence report.
|
|
6
6
|
*/
|
|
7
|
-
export async function
|
|
7
|
+
export async function writeAtomicText(path, text) {
|
|
8
8
|
await mkdir(dirname(path), { recursive: true });
|
|
9
9
|
const tmp = `${path}.${process.pid}.${Math.random().toString(36).slice(2, 8)}.tmp`;
|
|
10
|
-
await writeFile(tmp,
|
|
10
|
+
await writeFile(tmp, text, 'utf8');
|
|
11
11
|
await rename(tmp, path);
|
|
12
12
|
}
|
|
13
|
+
export async function writeAtomicJson(path, value, replacer) {
|
|
14
|
+
await writeAtomicText(path, `${JSON.stringify(value, replacer, 2)}\n`);
|
|
15
|
+
}
|
package/dist/journal/schema.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export declare const JOURNAL_SCHEMA_VERSION = 1;
|
|
7
7
|
export interface ErrorRecord {
|
|
8
|
-
/** Pipeline stage: boot | target | locate | assert | heal | report | index */
|
|
8
|
+
/** Pipeline stage: boot | target | locate | assert | heal | explore | report | index */
|
|
9
9
|
stage: string;
|
|
10
10
|
message: string;
|
|
11
11
|
/** Minimal context: step instruction, model, page URL — never secrets. */
|
package/dist/log.d.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* default 'warn'. Debug emits model call excerpts and recovery paths —
|
|
4
4
|
* never secret values.
|
|
5
5
|
*/
|
|
6
|
+
import { type Styler } from './ui/style.js';
|
|
6
7
|
export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
|
7
8
|
export interface Logger {
|
|
8
9
|
level: LogLevel;
|
|
@@ -13,5 +14,5 @@ export interface Logger {
|
|
|
13
14
|
}
|
|
14
15
|
export declare function createLogger(level: LogLevel, sink: {
|
|
15
16
|
err: (line: string) => void;
|
|
16
|
-
}, live?: (level: LogLevel, msg: string) => void): Logger;
|
|
17
|
+
}, live?: (level: LogLevel, msg: string) => void, style?: Styler): Logger;
|
|
17
18
|
export declare function resolveLogLevel(env: Record<string, string | undefined>, configured?: string): LogLevel;
|
package/dist/log.js
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tiny leveled logger. Level comes from ARGUS_DEBUG=1 or config.logLevel;
|
|
3
|
+
* default 'warn'. Debug emits model call excerpts and recovery paths —
|
|
4
|
+
* never secret values.
|
|
5
|
+
*/
|
|
6
|
+
import { PLAIN } from './ui/style.js';
|
|
1
7
|
const ORDER = { debug: 0, info: 1, warn: 2, error: 3 };
|
|
2
|
-
export function createLogger(level, sink, live) {
|
|
8
|
+
export function createLogger(level, sink, live, style = PLAIN) {
|
|
9
|
+
// DESIGN.md 7.7: a dim level word (bold for error) instead of `[level]`.
|
|
10
|
+
const prefix = (l) => (l === 'error' ? style.bold(l) : style.dim(l));
|
|
3
11
|
const emit = (l, msg) => {
|
|
4
12
|
if (ORDER[l] >= ORDER[level])
|
|
5
|
-
sink.err(
|
|
13
|
+
sink.err(`${prefix(l)}: ${msg}`);
|
|
6
14
|
live?.(l, msg);
|
|
7
15
|
};
|
|
8
16
|
// `live` receives every level regardless of `level` — the local dashboard
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { type PrMeta } from './evidence/ci.js';
|
|
2
|
+
/**
|
|
3
|
+
* `@argus` mention commands on PR comments (roadmap E3.U5). The mention
|
|
4
|
+
* lane runs on `issue_comment` events — strictly more privileged than
|
|
5
|
+
* `pull_request` (repo secrets + write-capable GITHUB_TOKEN are present),
|
|
6
|
+
* so it NEVER checks out the PR head. Review operates on a base-ref
|
|
7
|
+
* checkout with the PR diff fetched via the API — the same diff-only
|
|
8
|
+
* posture `code-review` already uses.
|
|
9
|
+
*/
|
|
10
|
+
export type MentionName = 'review' | 'record' | 'persist' | 'help';
|
|
11
|
+
export interface MentionCommand {
|
|
12
|
+
name: MentionName;
|
|
13
|
+
/** `record` flow description, quotes already stripped. */
|
|
14
|
+
arg?: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Parse a comment body into a whitelisted mention command. The mention must
|
|
18
|
+
* open the comment — a bare `@argus` in the middle of prose is not a command.
|
|
19
|
+
* `@argus` alone and `@argus help` both yield `help`; anything that isn't a
|
|
20
|
+
* whitelisted verb yields `unknown` so the caller can reply with the menu.
|
|
21
|
+
*/
|
|
22
|
+
export declare function parseMention(body: string): MentionCommand | 'unknown' | undefined;
|
|
23
|
+
export interface MentionGate {
|
|
24
|
+
allowed: boolean;
|
|
25
|
+
/** Reply text for denials the commenter should see; undefined → silent. */
|
|
26
|
+
reply?: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Two-part gate. `association` is the *commenter's* `author_association`
|
|
30
|
+
* (not the PR author's): only MEMBER/OWNER/COLLABORATOR may drive Argus.
|
|
31
|
+
* For fork-head PRs every command additionally needs the `argus-probe`
|
|
32
|
+
* label covering the current head SHA, and `record`/`persist` are disabled
|
|
33
|
+
* outright — they would execute or persist artifacts derived from code the
|
|
34
|
+
* label was meant to bound. `help` needs no label: it replies with a fixed
|
|
35
|
+
* menu and executes nothing.
|
|
36
|
+
*/
|
|
37
|
+
export declare function mayRunMention(cmd: MentionCommand, association: string | undefined, meta: PrMeta | undefined): MentionGate;
|
|
38
|
+
export declare const MENTION_HELP: string;
|
|
39
|
+
/**
|
|
40
|
+
* Post the mention reply as an issue comment. Best-effort — a failed reply
|
|
41
|
+
* logs and returns false rather than failing the dispatch.
|
|
42
|
+
*/
|
|
43
|
+
export declare function postIssueComment(repo: string, issue: string, body: string, token: string, ctx: {
|
|
44
|
+
err: (line: string) => void;
|
|
45
|
+
}): Promise<boolean>;
|
package/dist/mention.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { isTrustedAssociation, PROBE_LABEL } from './evidence/ci.js';
|
|
2
|
+
import { labelCoversHead } from './evidence/gate.js';
|
|
3
|
+
const NAMES = new Set(['review', 'record', 'persist', 'help']);
|
|
4
|
+
/**
|
|
5
|
+
* Parse a comment body into a whitelisted mention command. The mention must
|
|
6
|
+
* open the comment — a bare `@argus` in the middle of prose is not a command.
|
|
7
|
+
* `@argus` alone and `@argus help` both yield `help`; anything that isn't a
|
|
8
|
+
* whitelisted verb yields `unknown` so the caller can reply with the menu.
|
|
9
|
+
*/
|
|
10
|
+
export function parseMention(body) {
|
|
11
|
+
const first = body.trimStart().split('\n', 1)[0]?.trim() ?? '';
|
|
12
|
+
const m = /^@argus\b\s*(.*)$/i.exec(first);
|
|
13
|
+
if (m === null)
|
|
14
|
+
return undefined;
|
|
15
|
+
const rest = (m[1] ?? '').trim();
|
|
16
|
+
if (rest === '')
|
|
17
|
+
return { name: 'help' };
|
|
18
|
+
const verb = rest.split(/\s+/, 1)[0]?.toLowerCase() ?? '';
|
|
19
|
+
if (!NAMES.has(verb))
|
|
20
|
+
return 'unknown';
|
|
21
|
+
const name = verb;
|
|
22
|
+
if (name !== 'record')
|
|
23
|
+
return { name };
|
|
24
|
+
// `record "sign in with google"` — quotes optional; cap the flow text.
|
|
25
|
+
// Newlines/backticks are stripped: the arg is commenter-controlled text
|
|
26
|
+
// echoed into a public reply.
|
|
27
|
+
const arg = rest
|
|
28
|
+
.slice(verb.length)
|
|
29
|
+
.trim()
|
|
30
|
+
.replace(/^["']|["']$/g, '')
|
|
31
|
+
.replace(/[\r\n`]+/g, ' ')
|
|
32
|
+
.replace(/\s+/g, ' ')
|
|
33
|
+
.trim()
|
|
34
|
+
.slice(0, 200);
|
|
35
|
+
return { name, ...(arg !== '' ? { arg } : {}) };
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Two-part gate. `association` is the *commenter's* `author_association`
|
|
39
|
+
* (not the PR author's): only MEMBER/OWNER/COLLABORATOR may drive Argus.
|
|
40
|
+
* For fork-head PRs every command additionally needs the `argus-probe`
|
|
41
|
+
* label covering the current head SHA, and `record`/`persist` are disabled
|
|
42
|
+
* outright — they would execute or persist artifacts derived from code the
|
|
43
|
+
* label was meant to bound. `help` needs no label: it replies with a fixed
|
|
44
|
+
* menu and executes nothing.
|
|
45
|
+
*/
|
|
46
|
+
export function mayRunMention(cmd, association, meta) {
|
|
47
|
+
if (!isTrustedAssociation(association))
|
|
48
|
+
return { allowed: false };
|
|
49
|
+
if (cmd.name === 'help')
|
|
50
|
+
return { allowed: true };
|
|
51
|
+
if (meta === undefined) {
|
|
52
|
+
return { allowed: false, reply: "I can't see this PR's metadata — try again in a moment." };
|
|
53
|
+
}
|
|
54
|
+
if (!meta.isFork)
|
|
55
|
+
return { allowed: true };
|
|
56
|
+
if (cmd.name === 'record' || cmd.name === 'persist') {
|
|
57
|
+
return {
|
|
58
|
+
allowed: false,
|
|
59
|
+
reply: `\`@argus ${cmd.name}\` isn't available on fork PRs — record and persist run inside the repo's trust boundary.`,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
if (!meta.labels.includes(PROBE_LABEL) || !labelCoversHead(meta)) {
|
|
63
|
+
return {
|
|
64
|
+
allowed: false,
|
|
65
|
+
reply: `This PR comes from a fork — a maintainer can enable review by applying \`${PROBE_LABEL}\` to the current head.`,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
return { allowed: true };
|
|
69
|
+
}
|
|
70
|
+
export const MENTION_HELP = 'Commands: `@argus review` — re-run code review on the latest head · ' +
|
|
71
|
+
'`@argus record "<flow>"` — record a test flow against the app · ' +
|
|
72
|
+
'`@argus persist` — turn a reproduced probe into a regression-test PR · ' +
|
|
73
|
+
'`@argus help` — this menu.';
|
|
74
|
+
const GH_API = 'https://api.github.com';
|
|
75
|
+
/**
|
|
76
|
+
* Post the mention reply as an issue comment. Best-effort — a failed reply
|
|
77
|
+
* logs and returns false rather than failing the dispatch.
|
|
78
|
+
*/
|
|
79
|
+
export async function postIssueComment(repo, issue, body, token, ctx) {
|
|
80
|
+
const controller = new AbortController();
|
|
81
|
+
const timeout = setTimeout(() => controller.abort(), 30_000);
|
|
82
|
+
try {
|
|
83
|
+
const res = await fetch(`${GH_API}/repos/${repo}/issues/${issue}/comments`, {
|
|
84
|
+
method: 'POST',
|
|
85
|
+
signal: controller.signal,
|
|
86
|
+
headers: {
|
|
87
|
+
Authorization: `Bearer ${token}`,
|
|
88
|
+
Accept: 'application/vnd.github+json',
|
|
89
|
+
'X-GitHub-Api-Version': '2022-11-28',
|
|
90
|
+
'Content-Type': 'application/json',
|
|
91
|
+
},
|
|
92
|
+
body: JSON.stringify({ body }),
|
|
93
|
+
});
|
|
94
|
+
if (!res.ok) {
|
|
95
|
+
ctx.err(`mention: reply post failed — github ${res.status} ${res.statusText}`);
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
catch (e) {
|
|
101
|
+
ctx.err(`mention: reply post failed — ${e.message}`);
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
clearTimeout(timeout);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { AppExpectation, Config, Target } from '../config.js';
|
|
2
|
+
import type { BrowserDriver, PageCapture } from '../driver/browser.js';
|
|
3
|
+
import { TargetProcess } from '../driver/target.js';
|
|
4
|
+
import { Actions } from '../engine/actions.js';
|
|
5
|
+
import { type ExpectationContext, type ExploreResult } from '../engine/explore.js';
|
|
6
|
+
import type { VisionClient } from '../engine/loop.js';
|
|
7
|
+
import type { ErrorRecord } from '../journal/schema.js';
|
|
8
|
+
import type { Logger } from '../log.js';
|
|
9
|
+
import type { LaneStatus } from '../report/manifest.js';
|
|
10
|
+
import type { CallCost } from '../vision/cost.js';
|
|
11
|
+
import { Ledger } from '../vision/ledger.js';
|
|
12
|
+
/** Lane detail file the app runner writes and `runVerify` reads back. */
|
|
13
|
+
export declare const APP_LANE_REPORT = "app-lane.json";
|
|
14
|
+
/** Wall-clock bound when config.app.timeoutMs is unset. */
|
|
15
|
+
export declare const APP_LANE_DEFAULT_TIMEOUT_MS = 120000;
|
|
16
|
+
/**
|
|
17
|
+
* Lane detail record written to `app-lane.json`. `runVerify` merges `status`,
|
|
18
|
+
* `reason`, `summary`, `model`, and the call records into the manifest lane;
|
|
19
|
+
* the rest is evidence for the operator surfaces (TUI, dashboard, comment).
|
|
20
|
+
*/
|
|
21
|
+
export interface AppLaneReport {
|
|
22
|
+
lane: 'app';
|
|
23
|
+
status: LaneStatus;
|
|
24
|
+
reason: string | undefined;
|
|
25
|
+
summary: string | undefined;
|
|
26
|
+
task: string | undefined;
|
|
27
|
+
expected: AppExpectation | undefined;
|
|
28
|
+
targetUrl: string | undefined;
|
|
29
|
+
finalUrl: string | undefined;
|
|
30
|
+
expectedMet: boolean;
|
|
31
|
+
stopReason: string | undefined;
|
|
32
|
+
steps: {
|
|
33
|
+
action: string;
|
|
34
|
+
url: string;
|
|
35
|
+
note?: string;
|
|
36
|
+
}[];
|
|
37
|
+
visited: number;
|
|
38
|
+
anomalies: PageCapture[];
|
|
39
|
+
notes: ErrorRecord[];
|
|
40
|
+
artifacts: {
|
|
41
|
+
video: string | undefined;
|
|
42
|
+
};
|
|
43
|
+
calls: CallCost[];
|
|
44
|
+
visionCalls: number;
|
|
45
|
+
visionCostUsd: number;
|
|
46
|
+
durationMs: number;
|
|
47
|
+
model: string | undefined;
|
|
48
|
+
}
|
|
49
|
+
interface ExpectationCheck {
|
|
50
|
+
check: (ctx: ExpectationContext) => Promise<boolean>;
|
|
51
|
+
/** Human-readable form for the lane record, e.g. `text "Done"` + url /x/. */
|
|
52
|
+
describe: string;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Compile the configured expected state into one AND-ed predicate. An
|
|
56
|
+
* invalid url regex throws here — caught at preflight so a broken marker
|
|
57
|
+
* blocks the lane instead of failing mid-run.
|
|
58
|
+
*/
|
|
59
|
+
export declare function buildExpectationCheck(expected: AppExpectation): ExpectationCheck;
|
|
60
|
+
export interface AppTaskInput {
|
|
61
|
+
driver: BrowserDriver;
|
|
62
|
+
actions: Actions;
|
|
63
|
+
client: VisionClient;
|
|
64
|
+
ledger: Ledger;
|
|
65
|
+
config: Config;
|
|
66
|
+
targetUrl: string;
|
|
67
|
+
task: string;
|
|
68
|
+
expected: AppExpectation;
|
|
69
|
+
timeoutMs: number;
|
|
70
|
+
maxSteps?: number | undefined;
|
|
71
|
+
/**
|
|
72
|
+
* The lane's own spend bound, forwarded to the explore loop — without it
|
|
73
|
+
* `explore.budgetUsd` (a different lane's knob) throttles the app lane.
|
|
74
|
+
*/
|
|
75
|
+
budgetLimitUsd?: number | undefined;
|
|
76
|
+
logger?: Logger | undefined;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The bounded task loop: ExploreLoop substrate + task + expectation +
|
|
80
|
+
* wall-clock deadline. The lane — not the model's `done` — decides pass:
|
|
81
|
+
* the expected-state predicate is re-verified on a fresh observation after
|
|
82
|
+
* the loop stops, so a page that merely loads can never pass.
|
|
83
|
+
*/
|
|
84
|
+
export declare function runAppTask(input: AppTaskInput): Promise<{
|
|
85
|
+
result: ExploreResult;
|
|
86
|
+
expectedMet: boolean;
|
|
87
|
+
verifyError: string | undefined;
|
|
88
|
+
}>;
|
|
89
|
+
export interface AppLaneDeps {
|
|
90
|
+
/** Browser launch — default Chromium with error-capture taps on. */
|
|
91
|
+
launchDriver?: (config: Config) => Promise<BrowserDriver>;
|
|
92
|
+
/** Vision client factory — lazy key resolution stays with the caller. */
|
|
93
|
+
createClient?: (config: Config) => VisionClient;
|
|
94
|
+
/** URL readiness probe. */
|
|
95
|
+
waitForReady?: (url: string, timeoutMs: number) => Promise<void>;
|
|
96
|
+
/** Target-command boot. */
|
|
97
|
+
startTarget?: (spec: Target) => Promise<TargetProcess>;
|
|
98
|
+
/** Consumer page-setup hook (config.pageSetup module) — trusted only. */
|
|
99
|
+
applyPageSetup?: (driver: BrowserDriver) => Promise<void>;
|
|
100
|
+
logger?: Logger;
|
|
101
|
+
}
|
|
102
|
+
export interface AppLaneInput {
|
|
103
|
+
config: Config;
|
|
104
|
+
/** Resolved target URL — `--url` or `config.target.url`. */
|
|
105
|
+
url: string | undefined;
|
|
106
|
+
/** The checkout's trust lane — gates the whole executable lane. */
|
|
107
|
+
trusted: boolean;
|
|
108
|
+
/** `--task` override wins over `config.app.task`. */
|
|
109
|
+
task: string | undefined;
|
|
110
|
+
/** Flag-level expected-state markers win over `config.app.expected`. */
|
|
111
|
+
expected: AppExpectation | undefined;
|
|
112
|
+
/**
|
|
113
|
+
* Fully-resolved lane cap (`app.budgetUsd ?? ARGUS_BUDGET_USD ??
|
|
114
|
+
* budgetUsd`) — the caller resolves precedence so the enforced Ledger
|
|
115
|
+
* bound is exactly the one the manifest reports.
|
|
116
|
+
*/
|
|
117
|
+
budgetLimitUsd?: number;
|
|
118
|
+
deps?: AppLaneDeps;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Full `verify --app` lifecycle: contract preflight → trust/reachability →
|
|
122
|
+
* bounded task loop → lane record. Every status path produces a report —
|
|
123
|
+
* the lane can fail loudly, never silently.
|
|
124
|
+
*/
|
|
125
|
+
export declare function runAppLane(input: AppLaneInput): Promise<AppLaneReport>;
|
|
126
|
+
export {};
|