argus-reviewer-e2e 0.3.0 → 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 +134 -10
- package/action/approval-review.mjs +13 -3
- package/action/bootstrap.mjs +4 -0
- package/action/emit-review.mjs +16 -0
- package/action/runtime.mjs +32 -0
- package/action/sticky-comment.cjs +1260 -479
- package/dist/cli.d.ts +103 -7
- package/dist/cli.js +1208 -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
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Styler } from './style.js';
|
|
2
|
+
/**
|
|
3
|
+
* CLI error grammar (R14, R15). Every error renders as three lines: the
|
|
4
|
+
* failed glyph with a one-line summary, the cause, and the next action on a
|
|
5
|
+
* line of its own so it can be copied. `--json` prints the same facts as one
|
|
6
|
+
* JSON object with a stable `code`.
|
|
7
|
+
*
|
|
8
|
+
* The codes are a closed set and part of the public CLI contract
|
|
9
|
+
* (docs/quickstart.md). Numeric exit codes are separate and unchanged: the
|
|
10
|
+
* call site that reports an error still decides the exit code.
|
|
11
|
+
*/
|
|
12
|
+
export declare const ERROR_CODES: readonly ["OPENROUTER_KEY_MISSING", "OPENROUTER_KEY_REJECTED", "OPENROUTER_OUT_OF_CREDIT", "OPENROUTER_RATE_LIMITED", "PROVIDER_UNAVAILABLE", "CONFIG_INVALID", "MANIFEST_UNREADABLE", "A0_UNREACHABLE", "USAGE", "COMMAND_FAILED", "INTERNAL"];
|
|
13
|
+
export type ErrorCode = (typeof ERROR_CODES)[number];
|
|
14
|
+
export declare const ISSUE_URL = "https://github.com/duketopceo/Argus/issues/new";
|
|
15
|
+
export interface CliErrorOptions {
|
|
16
|
+
fix?: string;
|
|
17
|
+
retryAfterSeconds?: number;
|
|
18
|
+
httpStatus?: number;
|
|
19
|
+
cause?: unknown;
|
|
20
|
+
}
|
|
21
|
+
export declare class CliError extends Error {
|
|
22
|
+
readonly code: ErrorCode;
|
|
23
|
+
readonly fix: string | undefined;
|
|
24
|
+
readonly retryAfterSeconds: number | undefined;
|
|
25
|
+
/** Diagnostics only: never printed in the human three-line form (R5). */
|
|
26
|
+
readonly httpStatus: number | undefined;
|
|
27
|
+
constructor(code: ErrorCode, message: string, opts?: CliErrorOptions);
|
|
28
|
+
}
|
|
29
|
+
/** Seconds until the provider's rate limit resets, from the response headers. */
|
|
30
|
+
export declare function retryAfterSeconds(headers: Headers | undefined, now?: number): number | undefined;
|
|
31
|
+
/**
|
|
32
|
+
* Map a failed provider response to its class. Statuses that are not a
|
|
33
|
+
* provider or account condition (400, 404, 422) stay unclassified: they
|
|
34
|
+
* are request bugs and surface through the caller's own handling.
|
|
35
|
+
*/
|
|
36
|
+
export declare function classifyHttpStatus(status: number, headers?: Headers, now?: number, provider?: string): CliError | undefined;
|
|
37
|
+
/** The most actionable classified error, or undefined unless every error is classified. */
|
|
38
|
+
export declare function pickProviderError(errors: readonly unknown[]): CliError | undefined;
|
|
39
|
+
/** Normalize anything thrown into a CliError; unknown errors become `fallback`. */
|
|
40
|
+
export declare function toCliError(e: unknown, fallback: ErrorCode): CliError;
|
|
41
|
+
export interface RenderOptions {
|
|
42
|
+
/** Command name for the summary line ("run", "code-review"). */
|
|
43
|
+
context?: string | undefined;
|
|
44
|
+
/** The command to re-run, substituted for `{cmd}` in a fix. */
|
|
45
|
+
rerun?: string | undefined;
|
|
46
|
+
/** Include the stack (INTERNAL) and diagnostics. */
|
|
47
|
+
debug?: boolean | undefined;
|
|
48
|
+
/** Terminal width: the cause line wraps to it (default 80). */
|
|
49
|
+
width?: number | undefined;
|
|
50
|
+
}
|
|
51
|
+
/** The human form: glyph + summary, cause, fix on its own line. */
|
|
52
|
+
export declare function renderError(err: CliError, style: Styler, opts?: RenderOptions): string[];
|
|
53
|
+
/** The `--json` form: one line, one object, stable `code`. */
|
|
54
|
+
export declare function errorJson(err: CliError, opts?: RenderOptions): string;
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { maskSecrets, STATUS_GLYPH } from '../report/viewmodel.js';
|
|
2
|
+
/**
|
|
3
|
+
* CLI error grammar (R14, R15). Every error renders as three lines: the
|
|
4
|
+
* failed glyph with a one-line summary, the cause, and the next action on a
|
|
5
|
+
* line of its own so it can be copied. `--json` prints the same facts as one
|
|
6
|
+
* JSON object with a stable `code`.
|
|
7
|
+
*
|
|
8
|
+
* The codes are a closed set and part of the public CLI contract
|
|
9
|
+
* (docs/quickstart.md). Numeric exit codes are separate and unchanged: the
|
|
10
|
+
* call site that reports an error still decides the exit code.
|
|
11
|
+
*/
|
|
12
|
+
export const ERROR_CODES = [
|
|
13
|
+
'OPENROUTER_KEY_MISSING',
|
|
14
|
+
'OPENROUTER_KEY_REJECTED',
|
|
15
|
+
'OPENROUTER_OUT_OF_CREDIT',
|
|
16
|
+
'OPENROUTER_RATE_LIMITED',
|
|
17
|
+
'PROVIDER_UNAVAILABLE',
|
|
18
|
+
'CONFIG_INVALID',
|
|
19
|
+
'MANIFEST_UNREADABLE',
|
|
20
|
+
'A0_UNREACHABLE',
|
|
21
|
+
'USAGE',
|
|
22
|
+
'COMMAND_FAILED',
|
|
23
|
+
'INTERNAL',
|
|
24
|
+
];
|
|
25
|
+
export const ISSUE_URL = 'https://github.com/duketopceo/Argus/issues/new';
|
|
26
|
+
/** One-line summary per code. USAGE uses the error message itself. */
|
|
27
|
+
const TITLE = {
|
|
28
|
+
OPENROUTER_KEY_MISSING: 'OpenRouter key missing',
|
|
29
|
+
OPENROUTER_KEY_REJECTED: 'OpenRouter rejected the key',
|
|
30
|
+
OPENROUTER_OUT_OF_CREDIT: 'OpenRouter key is out of credit',
|
|
31
|
+
OPENROUTER_RATE_LIMITED: 'OpenRouter rate limit reached',
|
|
32
|
+
PROVIDER_UNAVAILABLE: 'model provider unavailable',
|
|
33
|
+
CONFIG_INVALID: 'config could not be loaded',
|
|
34
|
+
MANIFEST_UNREADABLE: 'run manifest unreadable',
|
|
35
|
+
A0_UNREACHABLE: 'Agent Zero unreachable',
|
|
36
|
+
USAGE: 'usage error',
|
|
37
|
+
COMMAND_FAILED: 'failed',
|
|
38
|
+
INTERNAL: 'unexpected error (an Argus bug)',
|
|
39
|
+
};
|
|
40
|
+
const KEY_FIX = 'export OPENROUTER_API_KEY=sk-or-...';
|
|
41
|
+
/** Next action per code when the thrower does not supply one. `{cmd}` is the re-run command. */
|
|
42
|
+
const DEFAULT_FIX = {
|
|
43
|
+
OPENROUTER_KEY_MISSING: KEY_FIX,
|
|
44
|
+
OPENROUTER_KEY_REJECTED: KEY_FIX,
|
|
45
|
+
OPENROUTER_OUT_OF_CREDIT: 'https://openrouter.ai/settings/credits',
|
|
46
|
+
OPENROUTER_RATE_LIMITED: '{cmd}',
|
|
47
|
+
PROVIDER_UNAVAILABLE: '{cmd}',
|
|
48
|
+
CONFIG_INVALID: 'edit the config file named above; docs/quickstart.md lists every key',
|
|
49
|
+
MANIFEST_UNREADABLE: 'argus-reviewer verify',
|
|
50
|
+
A0_UNREACHABLE: 'argus-reviewer init',
|
|
51
|
+
USAGE: 'argus-reviewer --help',
|
|
52
|
+
COMMAND_FAILED: '{cmd} --debug',
|
|
53
|
+
INTERNAL: ISSUE_URL,
|
|
54
|
+
};
|
|
55
|
+
export class CliError extends Error {
|
|
56
|
+
code;
|
|
57
|
+
fix;
|
|
58
|
+
retryAfterSeconds;
|
|
59
|
+
/** Diagnostics only: never printed in the human three-line form (R5). */
|
|
60
|
+
httpStatus;
|
|
61
|
+
constructor(code, message, opts = {}) {
|
|
62
|
+
super(message, opts.cause !== undefined ? { cause: opts.cause } : undefined);
|
|
63
|
+
this.name = 'CliError';
|
|
64
|
+
this.code = code;
|
|
65
|
+
this.fix = opts.fix;
|
|
66
|
+
this.retryAfterSeconds = opts.retryAfterSeconds;
|
|
67
|
+
this.httpStatus = opts.httpStatus;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/** Seconds until the provider's rate limit resets, from the response headers. */
|
|
71
|
+
export function retryAfterSeconds(headers, now = Date.now()) {
|
|
72
|
+
if (headers === undefined)
|
|
73
|
+
return undefined;
|
|
74
|
+
const ra = headers.get('retry-after');
|
|
75
|
+
if (ra !== null && ra.trim() !== '') {
|
|
76
|
+
const secs = Number(ra);
|
|
77
|
+
if (Number.isFinite(secs) && secs >= 0)
|
|
78
|
+
return Math.ceil(secs);
|
|
79
|
+
const at = Date.parse(ra);
|
|
80
|
+
if (Number.isFinite(at))
|
|
81
|
+
return Math.max(0, Math.ceil((at - now) / 1000));
|
|
82
|
+
}
|
|
83
|
+
// OpenRouter's own header: epoch milliseconds of the reset.
|
|
84
|
+
const reset = headers.get('x-ratelimit-reset');
|
|
85
|
+
if (reset !== null && reset.trim() !== '') {
|
|
86
|
+
const at = Number(reset);
|
|
87
|
+
if (Number.isFinite(at) && at > 0)
|
|
88
|
+
return Math.max(0, Math.ceil((at - now) / 1000));
|
|
89
|
+
}
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Map a failed provider response to its class. Statuses that are not a
|
|
94
|
+
* provider or account condition (400, 404, 422) stay unclassified: they
|
|
95
|
+
* are request bugs and surface through the caller's own handling.
|
|
96
|
+
*/
|
|
97
|
+
export function classifyHttpStatus(status, headers, now = Date.now(), provider = 'OpenRouter') {
|
|
98
|
+
if (status === 401 || status === 403) {
|
|
99
|
+
return new CliError('OPENROUTER_KEY_REJECTED', `${provider} did not accept OPENROUTER_API_KEY`, {
|
|
100
|
+
httpStatus: status,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
if (status === 402) {
|
|
104
|
+
return new CliError('OPENROUTER_OUT_OF_CREDIT', `${provider} reports no credit left on this key`, {
|
|
105
|
+
httpStatus: status,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
if (status === 429) {
|
|
109
|
+
const secs = retryAfterSeconds(headers, now);
|
|
110
|
+
const when = secs !== undefined ? `resets in ${secs}s` : 'reset time not given; wait a minute';
|
|
111
|
+
return new CliError('OPENROUTER_RATE_LIMITED', `${provider} is rate limiting this key; ${when}`, {
|
|
112
|
+
httpStatus: status,
|
|
113
|
+
...(secs !== undefined ? { retryAfterSeconds: secs } : {}),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
if (status >= 500 && status <= 599) {
|
|
117
|
+
return new CliError('PROVIDER_UNAVAILABLE', `${provider} or its upstream provider is down; see status.openrouter.ai`, {
|
|
118
|
+
httpStatus: status,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
/** Order for collapsing several candidate failures into one: account problems first. */
|
|
124
|
+
const PROVIDER_PRIORITY = [
|
|
125
|
+
'OPENROUTER_KEY_REJECTED',
|
|
126
|
+
'OPENROUTER_OUT_OF_CREDIT',
|
|
127
|
+
'OPENROUTER_RATE_LIMITED',
|
|
128
|
+
'PROVIDER_UNAVAILABLE',
|
|
129
|
+
];
|
|
130
|
+
/** The most actionable classified error, or undefined unless every error is classified. */
|
|
131
|
+
export function pickProviderError(errors) {
|
|
132
|
+
if (errors.length === 0 || !errors.every((e) => e instanceof CliError))
|
|
133
|
+
return undefined;
|
|
134
|
+
const classified = errors;
|
|
135
|
+
for (const code of PROVIDER_PRIORITY) {
|
|
136
|
+
const hit = classified.find((e) => e.code === code);
|
|
137
|
+
if (hit !== undefined)
|
|
138
|
+
return hit;
|
|
139
|
+
}
|
|
140
|
+
return classified[0];
|
|
141
|
+
}
|
|
142
|
+
/** Node's parseArgs rejections: usage errors, not bugs. */
|
|
143
|
+
function isParseArgsError(e) {
|
|
144
|
+
const code = e?.code;
|
|
145
|
+
return typeof code === 'string' && code.startsWith('ERR_PARSE_ARGS_');
|
|
146
|
+
}
|
|
147
|
+
/** Normalize anything thrown into a CliError; unknown errors become `fallback`. */
|
|
148
|
+
export function toCliError(e, fallback) {
|
|
149
|
+
if (e instanceof CliError)
|
|
150
|
+
return e;
|
|
151
|
+
const code = e?.code;
|
|
152
|
+
if (typeof code === 'string' && ERROR_CODES.includes(code)) {
|
|
153
|
+
// A typed error from another layer (DecisionError) that carries a code.
|
|
154
|
+
const err = e;
|
|
155
|
+
return new CliError(code, err.message, {
|
|
156
|
+
cause: e,
|
|
157
|
+
...(err.retryAfterSeconds !== undefined ? { retryAfterSeconds: err.retryAfterSeconds } : {}),
|
|
158
|
+
...(err.httpStatus !== undefined ? { httpStatus: err.httpStatus } : {}),
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
if (isParseArgsError(e))
|
|
162
|
+
return new CliError('USAGE', e.message, { cause: e });
|
|
163
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
164
|
+
return new CliError(fallback, message, { cause: e });
|
|
165
|
+
}
|
|
166
|
+
/** Word-wrap `text` to `width` columns, each line prefixed with `indent`. */
|
|
167
|
+
function wrap(text, width, indent) {
|
|
168
|
+
const room = Math.max(20, width - indent.length);
|
|
169
|
+
const lines = [];
|
|
170
|
+
let line = '';
|
|
171
|
+
for (const word of text.split(/\s+/).filter((w) => w !== '')) {
|
|
172
|
+
if (line !== '' && line.length + 1 + word.length > room) {
|
|
173
|
+
lines.push(indent + line);
|
|
174
|
+
line = word;
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
line = line === '' ? word : `${line} ${word}`;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (line !== '')
|
|
181
|
+
lines.push(indent + line);
|
|
182
|
+
return lines;
|
|
183
|
+
}
|
|
184
|
+
function facts(err, opts) {
|
|
185
|
+
const cause = maskSecrets(err.message);
|
|
186
|
+
const rerun = opts.rerun ?? 'argus-reviewer';
|
|
187
|
+
let fix = (err.fix ?? DEFAULT_FIX[err.code]).replaceAll('{cmd}', rerun);
|
|
188
|
+
if (err.code === 'OPENROUTER_RATE_LIMITED' && err.fix === undefined && err.retryAfterSeconds !== undefined) {
|
|
189
|
+
fix = `sleep ${err.retryAfterSeconds} && ${rerun}`;
|
|
190
|
+
}
|
|
191
|
+
if (err.code === 'USAGE') {
|
|
192
|
+
const help = opts.context !== undefined ? `argus-reviewer ${opts.context} --help` : 'argus-reviewer --help';
|
|
193
|
+
return { code: err.code, summary: cause, cause: undefined, fix: err.fix ?? help };
|
|
194
|
+
}
|
|
195
|
+
const title = err.code === 'COMMAND_FAILED' ? `${opts.context ?? 'command'} failed` : TITLE[err.code];
|
|
196
|
+
const summary = opts.context !== undefined && err.code !== 'COMMAND_FAILED' ? `${opts.context}: ${title}` : title;
|
|
197
|
+
return { code: err.code, summary, cause, fix };
|
|
198
|
+
}
|
|
199
|
+
/** The human form: glyph + summary, cause, fix on its own line. */
|
|
200
|
+
export function renderError(err, style, opts = {}) {
|
|
201
|
+
const f = facts(err, opts);
|
|
202
|
+
const lines = [`${style.status('failed', STATUS_GLYPH.failed)} ${style.bold(f.summary)}`];
|
|
203
|
+
if (f.cause !== undefined && f.cause !== '')
|
|
204
|
+
lines.push(...wrap(f.cause, opts.width ?? 80, ' '));
|
|
205
|
+
lines.push(` ${style.role('accent', f.fix)}`);
|
|
206
|
+
if (opts.debug === true) {
|
|
207
|
+
if (err.httpStatus !== undefined)
|
|
208
|
+
lines.push(style.dim(` diagnostics: code ${err.code}, http ${err.httpStatus}`));
|
|
209
|
+
const stack = (err.cause instanceof Error ? err.cause : err).stack;
|
|
210
|
+
if (stack !== undefined)
|
|
211
|
+
lines.push(style.dim(maskSecrets(stack)));
|
|
212
|
+
}
|
|
213
|
+
return lines;
|
|
214
|
+
}
|
|
215
|
+
/** The `--json` form: one line, one object, stable `code`. */
|
|
216
|
+
export function errorJson(err, opts = {}) {
|
|
217
|
+
const f = facts(err, opts);
|
|
218
|
+
const error = {
|
|
219
|
+
code: f.code,
|
|
220
|
+
summary: f.summary,
|
|
221
|
+
...(f.cause !== undefined ? { cause: f.cause } : {}),
|
|
222
|
+
fix: f.fix,
|
|
223
|
+
};
|
|
224
|
+
if (err.retryAfterSeconds !== undefined)
|
|
225
|
+
error.retryAfterSeconds = err.retryAfterSeconds;
|
|
226
|
+
if (err.httpStatus !== undefined)
|
|
227
|
+
error.httpStatus = err.httpStatus;
|
|
228
|
+
if (err.code === 'INTERNAL')
|
|
229
|
+
error.issue = ISSUE_URL;
|
|
230
|
+
if (opts.debug === true) {
|
|
231
|
+
const stack = (err.cause instanceof Error ? err.cause : err).stack;
|
|
232
|
+
if (stack !== undefined)
|
|
233
|
+
error.stack = maskSecrets(stack);
|
|
234
|
+
}
|
|
235
|
+
return JSON.stringify({ error });
|
|
236
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { LaneStatus } from '../report/manifest.js';
|
|
2
|
+
/**
|
|
3
|
+
* TTY-aware terminal styler (plan KTD5, DESIGN.md 7.7). No dependency: SGR
|
|
4
|
+
* codes come from the ANSI-16 map that scripts/build-tokens.mjs writes to
|
|
5
|
+
* assets/brand/ansi.json. That file is not shipped in the npm package, so
|
|
6
|
+
* the codes are copied here and a unit test pins them to it.
|
|
7
|
+
*/
|
|
8
|
+
export type Role = 'ink' | 'ink-2' | 'ink-3' | 'accent' | 'passed' | 'failed' | 'caution';
|
|
9
|
+
export declare const ROLE_SGR: Record<Role, string>;
|
|
10
|
+
export declare const STATUS_SGR: Record<LaneStatus, string>;
|
|
11
|
+
export interface ColorInputs {
|
|
12
|
+
env: Record<string, string | undefined>;
|
|
13
|
+
isTTY: boolean;
|
|
14
|
+
noColorFlag?: boolean;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Precedence: `--no-color` and `NO_COLOR` beat `FORCE_COLOR`, which beats
|
|
18
|
+
* TTY detection. GitHub Actions logs are not a TTY, so they get color only
|
|
19
|
+
* through `FORCE_COLOR`. An empty `NO_COLOR` does not count (no-color.org),
|
|
20
|
+
* and `FORCE_COLOR=0`/`false` forces nothing.
|
|
21
|
+
*/
|
|
22
|
+
export declare function colorEnabled({ env, isTTY, noColorFlag }: ColorInputs): boolean;
|
|
23
|
+
export interface Styler {
|
|
24
|
+
readonly enabled: boolean;
|
|
25
|
+
role: (role: Role, text: string) => string;
|
|
26
|
+
status: (status: LaneStatus, text: string) => string;
|
|
27
|
+
/** The status glyph, colored by status. */
|
|
28
|
+
glyph: (status: LaneStatus) => string;
|
|
29
|
+
bold: (text: string) => string;
|
|
30
|
+
dim: (text: string) => string;
|
|
31
|
+
}
|
|
32
|
+
export declare function createStyler(enabled: boolean): Styler;
|
|
33
|
+
/** The styler for anything that must stay plain (files, JSON, tests). */
|
|
34
|
+
export declare const PLAIN: Styler;
|
package/dist/ui/style.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { STATUS_GLYPH } from '../report/viewmodel.js';
|
|
2
|
+
export const ROLE_SGR = {
|
|
3
|
+
ink: '39',
|
|
4
|
+
'ink-2': '39',
|
|
5
|
+
'ink-3': '2',
|
|
6
|
+
accent: '34',
|
|
7
|
+
passed: '32',
|
|
8
|
+
failed: '31',
|
|
9
|
+
caution: '33',
|
|
10
|
+
};
|
|
11
|
+
export const STATUS_SGR = {
|
|
12
|
+
passed: '32',
|
|
13
|
+
failed: '31',
|
|
14
|
+
inconclusive: '33',
|
|
15
|
+
blocked: '1',
|
|
16
|
+
unavailable: '2',
|
|
17
|
+
skipped: '2',
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Precedence: `--no-color` and `NO_COLOR` beat `FORCE_COLOR`, which beats
|
|
21
|
+
* TTY detection. GitHub Actions logs are not a TTY, so they get color only
|
|
22
|
+
* through `FORCE_COLOR`. An empty `NO_COLOR` does not count (no-color.org),
|
|
23
|
+
* and `FORCE_COLOR=0`/`false` forces nothing.
|
|
24
|
+
*/
|
|
25
|
+
export function colorEnabled({ env, isTTY, noColorFlag }) {
|
|
26
|
+
if (noColorFlag === true)
|
|
27
|
+
return false;
|
|
28
|
+
if (env.NO_COLOR !== undefined && env.NO_COLOR !== '')
|
|
29
|
+
return false;
|
|
30
|
+
const force = env.FORCE_COLOR;
|
|
31
|
+
if (force !== undefined && force !== '' && force !== '0' && force !== 'false')
|
|
32
|
+
return true;
|
|
33
|
+
return isTTY;
|
|
34
|
+
}
|
|
35
|
+
export function createStyler(enabled) {
|
|
36
|
+
// '39' is the terminal default: wrapping it would only add noise.
|
|
37
|
+
const sgr = (code, text) => !enabled || code === '39' || text === '' ? text : `\x1b[${code}m${text}\x1b[0m`;
|
|
38
|
+
return {
|
|
39
|
+
enabled,
|
|
40
|
+
role: (role, text) => sgr(ROLE_SGR[role], text),
|
|
41
|
+
status: (status, text) => sgr(STATUS_SGR[status], text),
|
|
42
|
+
glyph: (status) => sgr(STATUS_SGR[status], STATUS_GLYPH[status]),
|
|
43
|
+
bold: (text) => sgr('1', text),
|
|
44
|
+
dim: (text) => sgr('2', text),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/** The styler for anything that must stay plain (files, JSON, tests). */
|
|
48
|
+
export const PLAIN = createStyler(false);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type LaneStatus, type RunManifest } from '../report/manifest.js';
|
|
2
|
+
import type { Styler } from './style.js';
|
|
3
|
+
/**
|
|
4
|
+
* End-of-run summary block for `run` and `verify` (R13, DESIGN.md 7.7), in
|
|
5
|
+
* the same grammar as the PR comment: verdict line, one row per lane, then
|
|
6
|
+
* total spend against budget and the report path.
|
|
7
|
+
*
|
|
8
|
+
* ⊘ failed head abc1234 38.1s
|
|
9
|
+
* ⊘ review 3 findings, 2 reproduced $0.003100
|
|
10
|
+
* ● flow 4/4 journeys, 1 healed $0.000000
|
|
11
|
+
* total $0.004210 of $1.00 budget · report argus-reviewer-report/run-manifest.json
|
|
12
|
+
*/
|
|
13
|
+
export interface SummaryLane {
|
|
14
|
+
lane: string;
|
|
15
|
+
status: LaneStatus;
|
|
16
|
+
detail: string;
|
|
17
|
+
costUsd: number;
|
|
18
|
+
/** false for lanes whose spend Argus cannot see (a0): shows "unmetered". */
|
|
19
|
+
metered: boolean;
|
|
20
|
+
limitUsd?: number | undefined;
|
|
21
|
+
spentUsd?: number | undefined;
|
|
22
|
+
exceeded?: boolean | undefined;
|
|
23
|
+
}
|
|
24
|
+
export interface SummaryInput {
|
|
25
|
+
status: LaneStatus;
|
|
26
|
+
headSha?: string | undefined;
|
|
27
|
+
durationMs?: number | undefined;
|
|
28
|
+
lanes: SummaryLane[];
|
|
29
|
+
totalUsd: number;
|
|
30
|
+
/** Sum of the lane caps; omitted when no lane has a dollar cap. */
|
|
31
|
+
budgetUsd?: number | undefined;
|
|
32
|
+
reportPath?: string | undefined;
|
|
33
|
+
}
|
|
34
|
+
/** The config key that raises each lane's dollar cap (ARGUS_BUDGET_USD overrides all). */
|
|
35
|
+
export declare const BUDGET_KEY: Record<string, string>;
|
|
36
|
+
export declare function renderSummary(input: SummaryInput, style: Styler, width?: number): string[];
|
|
37
|
+
/** Project a verify manifest onto the summary block. */
|
|
38
|
+
export declare function verifySummary(manifest: RunManifest, reportPath: string): SummaryInput;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { LANE_IDS } from '../report/manifest.js';
|
|
2
|
+
import { formatDuration, formatUsd, LANE_STATUS_LABEL, maskSecrets, shortSha } from '../report/viewmodel.js';
|
|
3
|
+
/** The config key that raises each lane's dollar cap (ARGUS_BUDGET_USD overrides all). */
|
|
4
|
+
export const BUDGET_KEY = {
|
|
5
|
+
review: 'codeReviewBudgetUsd',
|
|
6
|
+
flow: 'budgetUsd',
|
|
7
|
+
app: 'app.budgetUsd',
|
|
8
|
+
};
|
|
9
|
+
const LANE_COL = 7;
|
|
10
|
+
const COST_COL = 9;
|
|
11
|
+
/**
|
|
12
|
+
* A lane row is 2 indent + glyph + space + lane + 2, the detail, then 2 +
|
|
13
|
+
* cost: 24 columns plus the detail. Six more columns of slack keep it clear
|
|
14
|
+
* of the right edge.
|
|
15
|
+
*/
|
|
16
|
+
const FIXED_COLS = 2 + 1 + 1 + LANE_COL + 2 + 2 + COST_COL + 6;
|
|
17
|
+
const DETAIL_MAX = 50;
|
|
18
|
+
const DETAIL_MIN = 16;
|
|
19
|
+
function fit(text, width) {
|
|
20
|
+
const flat = text.replace(/\s+/g, ' ').trim();
|
|
21
|
+
return flat.length <= width ? flat.padEnd(width) : `${flat.slice(0, width - 1)}…`;
|
|
22
|
+
}
|
|
23
|
+
const dollars = (n) => `$${n.toFixed(2)}`;
|
|
24
|
+
export function renderSummary(input, style, width = 80) {
|
|
25
|
+
const head = [
|
|
26
|
+
`${style.glyph(input.status)} ${style.bold(LANE_STATUS_LABEL[input.status])}`,
|
|
27
|
+
...(input.headSha !== undefined ? [`head ${input.headSha}`] : []),
|
|
28
|
+
...(input.durationMs !== undefined ? [formatDuration(input.durationMs)] : []),
|
|
29
|
+
].join(' ');
|
|
30
|
+
const lines = [head];
|
|
31
|
+
const detailWidth = Math.max(DETAIL_MIN, Math.min(DETAIL_MAX, width - FIXED_COLS));
|
|
32
|
+
for (const lane of input.lanes) {
|
|
33
|
+
const cost = lane.metered ? formatUsd(lane.costUsd) : 'unmetered';
|
|
34
|
+
lines.push(` ${style.glyph(lane.status)} ${lane.lane.padEnd(LANE_COL)} ` +
|
|
35
|
+
`${fit(maskSecrets(lane.detail), detailWidth)} ${style.dim(cost.padStart(COST_COL))}`);
|
|
36
|
+
}
|
|
37
|
+
for (const lane of input.lanes) {
|
|
38
|
+
if (lane.exceeded !== true || lane.limitUsd === undefined)
|
|
39
|
+
continue;
|
|
40
|
+
lines.push(` ${style.glyph('failed')} budget exceeded: spent ${dollars(lane.spentUsd ?? lane.costUsd)} ` +
|
|
41
|
+
`of ${dollars(lane.limitUsd)} (${lane.lane})`);
|
|
42
|
+
const key = BUDGET_KEY[lane.lane];
|
|
43
|
+
if (key !== undefined) {
|
|
44
|
+
lines.push(` raise ${key} in the config, or set ARGUS_BUDGET_USD`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const total = `total ${formatUsd(input.totalUsd)}` +
|
|
48
|
+
(input.budgetUsd !== undefined ? ` of ${dollars(input.budgetUsd)} budget` : '');
|
|
49
|
+
if (input.reportPath === undefined) {
|
|
50
|
+
lines.push(` ${total}`);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
const oneLine = ` ${total} · report ${input.reportPath}`;
|
|
54
|
+
// Too wide for one line: the report path gets its own, easy to copy.
|
|
55
|
+
if (oneLine.length <= width)
|
|
56
|
+
lines.push(oneLine);
|
|
57
|
+
else
|
|
58
|
+
lines.push(` ${total}`, ` report ${input.reportPath}`);
|
|
59
|
+
}
|
|
60
|
+
return lines;
|
|
61
|
+
}
|
|
62
|
+
function laneDetail(status, summary, reason) {
|
|
63
|
+
// A lane that did not pass leads with why; a passing lane with what it found.
|
|
64
|
+
const text = status === 'passed' ? (summary ?? reason) : (reason ?? summary);
|
|
65
|
+
return text ?? LANE_STATUS_LABEL[status];
|
|
66
|
+
}
|
|
67
|
+
function durationOf(startedAt, finishedAt) {
|
|
68
|
+
const ms = Date.parse(finishedAt) - Date.parse(startedAt);
|
|
69
|
+
return Number.isFinite(ms) && ms >= 0 ? ms : undefined;
|
|
70
|
+
}
|
|
71
|
+
/** Project a verify manifest onto the summary block. */
|
|
72
|
+
export function verifySummary(manifest, reportPath) {
|
|
73
|
+
const lanes = [];
|
|
74
|
+
let budget;
|
|
75
|
+
for (const id of LANE_IDS) {
|
|
76
|
+
const lane = manifest.lanes[id];
|
|
77
|
+
if (!lane.selected)
|
|
78
|
+
continue;
|
|
79
|
+
lanes.push({
|
|
80
|
+
lane: id,
|
|
81
|
+
status: lane.status,
|
|
82
|
+
detail: laneDetail(lane.status, lane.summary, lane.reason),
|
|
83
|
+
costUsd: lane.usage.costUsd,
|
|
84
|
+
metered: lane.usage.metered,
|
|
85
|
+
limitUsd: lane.budget.limitUsd,
|
|
86
|
+
spentUsd: lane.budget.spentUsd,
|
|
87
|
+
exceeded: lane.budget.exceeded,
|
|
88
|
+
});
|
|
89
|
+
if (lane.budget.limitUsd !== undefined)
|
|
90
|
+
budget = (budget ?? 0) + lane.budget.limitUsd;
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
status: manifest.aggregate.status,
|
|
94
|
+
headSha: shortSha(manifest.identity.intendedHeadSha ?? manifest.identity.checkoutSha),
|
|
95
|
+
durationMs: durationOf(manifest.startedAt, manifest.finishedAt),
|
|
96
|
+
lanes,
|
|
97
|
+
totalUsd: manifest.aggregate.costUsd,
|
|
98
|
+
budgetUsd: budget,
|
|
99
|
+
reportPath,
|
|
100
|
+
};
|
|
101
|
+
}
|
package/dist/vision/cost.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { type ErrorCode } from '../ui/errors.js';
|
|
1
2
|
import type { CallCost, ProviderValue } from './cost.js';
|
|
2
3
|
/**
|
|
3
|
-
* Pinned
|
|
4
|
+
* Pinned confidence-model slug — the alias `~typesafe/jev-latest` drifts silently and
|
|
4
5
|
* adjudication thresholds are calibrated to a version. The alias stays
|
|
5
6
|
* usable via `config.decisionModel` for experimentation.
|
|
6
7
|
*/
|
|
@@ -9,7 +10,12 @@ export type DecisionErrorKind = 'auth' | 'validation' | 'rate_limited' | 'overlo
|
|
|
9
10
|
export declare class DecisionError extends Error {
|
|
10
11
|
readonly kind: DecisionErrorKind;
|
|
11
12
|
readonly retryable: boolean;
|
|
12
|
-
|
|
13
|
+
/** CLI error class (src/ui/errors.ts) when the failure has one: key, credit, rate, provider. */
|
|
14
|
+
readonly code?: ErrorCode | undefined;
|
|
15
|
+
readonly retryAfterSeconds?: number | undefined;
|
|
16
|
+
constructor(kind: DecisionErrorKind, message: string, retryable: boolean,
|
|
17
|
+
/** CLI error class (src/ui/errors.ts) when the failure has one: key, credit, rate, provider. */
|
|
18
|
+
code?: ErrorCode | undefined, retryAfterSeconds?: number | undefined);
|
|
13
19
|
}
|
|
14
20
|
export interface NoulQuestion {
|
|
15
21
|
type: 'noul';
|
|
@@ -52,7 +58,7 @@ export declare const isChoiceAnswer: (a: DecisionAnswer) => a is ChoiceAnswer;
|
|
|
52
58
|
export declare const isScoreAnswer: (a: DecisionAnswer) => a is ScoreAnswer;
|
|
53
59
|
/** Short error label for lane debug lines: DecisionError kind, else message. */
|
|
54
60
|
export declare function describeDecisionError(e: unknown): string;
|
|
55
|
-
/** Shared per-call batch cap for the
|
|
61
|
+
/** Shared per-call batch cap for the confidence-model lanes (secrets, findings, triage). */
|
|
56
62
|
export declare const MAX_CANDIDATES = 50;
|
|
57
63
|
export interface DecisionClientOptions {
|
|
58
64
|
apiKey: string;
|
package/dist/vision/decisions.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { debug } from '../debug.js';
|
|
2
|
+
import { classifyHttpStatus } from '../ui/errors.js';
|
|
2
3
|
import { makeDecisionsCallCost } from './cost.js';
|
|
3
4
|
/**
|
|
4
|
-
* Pinned
|
|
5
|
+
* Pinned confidence-model slug — the alias `~typesafe/jev-latest` drifts silently and
|
|
5
6
|
* adjudication thresholds are calibrated to a version. The alias stays
|
|
6
7
|
* usable via `config.decisionModel` for experimentation.
|
|
7
8
|
*/
|
|
@@ -14,10 +15,16 @@ const MAX_SCORE_LEVELS = 10;
|
|
|
14
15
|
export class DecisionError extends Error {
|
|
15
16
|
kind;
|
|
16
17
|
retryable;
|
|
17
|
-
|
|
18
|
+
code;
|
|
19
|
+
retryAfterSeconds;
|
|
20
|
+
constructor(kind, message, retryable,
|
|
21
|
+
/** CLI error class (src/ui/errors.ts) when the failure has one: key, credit, rate, provider. */
|
|
22
|
+
code, retryAfterSeconds) {
|
|
18
23
|
super(message);
|
|
19
24
|
this.kind = kind;
|
|
20
25
|
this.retryable = retryable;
|
|
26
|
+
this.code = code;
|
|
27
|
+
this.retryAfterSeconds = retryAfterSeconds;
|
|
21
28
|
this.name = 'DecisionError';
|
|
22
29
|
}
|
|
23
30
|
}
|
|
@@ -29,7 +36,7 @@ export const isScoreAnswer = (a) => 'score' in a;
|
|
|
29
36
|
export function describeDecisionError(e) {
|
|
30
37
|
return e instanceof DecisionError ? e.kind : e.message;
|
|
31
38
|
}
|
|
32
|
-
/** Shared per-call batch cap for the
|
|
39
|
+
/** Shared per-call batch cap for the confidence-model lanes (secrets, findings, triage). */
|
|
33
40
|
export const MAX_CANDIDATES = 50;
|
|
34
41
|
function isRecord(v) {
|
|
35
42
|
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
@@ -98,23 +105,26 @@ function validateAnswers(questions, answers) {
|
|
|
98
105
|
}
|
|
99
106
|
return out;
|
|
100
107
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
108
|
+
/**
|
|
109
|
+
* HTTP status to a typed DecisionError. `kind` and `retryable` drive the
|
|
110
|
+
* retry loop and stay as they were; the CLI class (`code`) and its message
|
|
111
|
+
* come from the shared classifier in src/ui/errors.ts.
|
|
112
|
+
*/
|
|
113
|
+
function classifyStatus(status, headers) {
|
|
114
|
+
const shared = classifyHttpStatus(status, headers);
|
|
115
|
+
const make = (kind, fallback, retryable) => new DecisionError(kind, shared !== undefined ? `decide: ${shared.message}` : fallback, retryable, shared?.code, shared?.retryAfterSeconds);
|
|
116
|
+
if (status === 401 || status === 403)
|
|
117
|
+
return make('auth', `decide: HTTP ${status}, check OPENROUTER_API_KEY`, false);
|
|
105
118
|
if (status === 400 || status === 404 || status === 422) {
|
|
106
|
-
return
|
|
107
|
-
}
|
|
108
|
-
if (status === 429) {
|
|
109
|
-
return new DecisionError('rate_limited', 'decide: HTTP 429 rate limited', true);
|
|
110
|
-
}
|
|
111
|
-
if (status === 503) {
|
|
112
|
-
return new DecisionError('overloaded', 'decide: HTTP 503 provider overloaded', true);
|
|
113
|
-
}
|
|
114
|
-
if (status >= 500) {
|
|
115
|
-
return new DecisionError('server_error', `decide: HTTP ${status}`, true);
|
|
119
|
+
return make('validation', `decide: HTTP ${status}, request rejected`, false);
|
|
116
120
|
}
|
|
117
|
-
|
|
121
|
+
if (status === 429)
|
|
122
|
+
return make('rate_limited', 'decide: HTTP 429 rate limited', true);
|
|
123
|
+
if (status === 503)
|
|
124
|
+
return make('overloaded', 'decide: HTTP 503 provider overloaded', true);
|
|
125
|
+
if (status >= 500)
|
|
126
|
+
return make('server_error', `decide: HTTP ${status}`, true);
|
|
127
|
+
return make('unexpected', `decide: HTTP ${status}`, false);
|
|
118
128
|
}
|
|
119
129
|
function retryDelayMs(res) {
|
|
120
130
|
const retryAfterMs = res.headers.get('retry-after-ms');
|
|
@@ -178,10 +188,10 @@ export class DecisionClient {
|
|
|
178
188
|
body: JSON.stringify(body),
|
|
179
189
|
});
|
|
180
190
|
if (!res.ok) {
|
|
181
|
-
const err = classifyStatus(res.status);
|
|
191
|
+
const err = classifyStatus(res.status, res.headers);
|
|
182
192
|
if (err.retryable && attempt === 0) {
|
|
183
193
|
const delay = retryDelayMs(res);
|
|
184
|
-
debug('decisions', `${err.kind}
|
|
194
|
+
debug('decisions', `${err.kind}, retrying in ${delay}ms`);
|
|
185
195
|
await new Promise((r) => setTimeout(r, delay));
|
|
186
196
|
continue;
|
|
187
197
|
}
|
|
@@ -215,7 +225,7 @@ export class DecisionClient {
|
|
|
215
225
|
if (err.name === 'AbortError') {
|
|
216
226
|
lastErr = new DecisionError('timeout', `decide: timed out after ${this._timeoutMs}ms`, true);
|
|
217
227
|
if (attempt === 0) {
|
|
218
|
-
debug('decisions', 'timeout
|
|
228
|
+
debug('decisions', 'timeout, retrying once');
|
|
219
229
|
continue;
|
|
220
230
|
}
|
|
221
231
|
break;
|