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.
Files changed (89) hide show
  1. package/README.md +84 -71
  2. package/action/action.yml +134 -10
  3. package/action/approval-review.mjs +13 -3
  4. package/action/bootstrap.mjs +4 -0
  5. package/action/emit-review.mjs +16 -0
  6. package/action/runtime.mjs +32 -0
  7. package/action/sticky-comment.cjs +1260 -479
  8. package/dist/cli.d.ts +103 -7
  9. package/dist/cli.js +1208 -186
  10. package/dist/config.d.ts +96 -11
  11. package/dist/config.js +102 -4
  12. package/dist/detect.d.ts +29 -2
  13. package/dist/detect.js +98 -7
  14. package/dist/driver/browser.d.ts +32 -0
  15. package/dist/driver/browser.js +56 -1
  16. package/dist/driver/target.d.ts +4 -1
  17. package/dist/driver/target.js +27 -6
  18. package/dist/engine/actions.d.ts +5 -0
  19. package/dist/engine/actions.js +8 -0
  20. package/dist/engine/explore.d.ts +78 -0
  21. package/dist/engine/explore.js +373 -0
  22. package/dist/engine/loop.d.ts +2 -2
  23. package/dist/engine/loop.js +8 -8
  24. package/dist/engine/prompts.d.ts +28 -1
  25. package/dist/engine/prompts.js +88 -0
  26. package/dist/evidence/ci.d.ts +13 -1
  27. package/dist/evidence/ci.js +38 -3
  28. package/dist/evidence/gate.d.ts +8 -0
  29. package/dist/evidence/gate.js +1 -1
  30. package/dist/evidence/link.js +1 -1
  31. package/dist/executor/a0.d.ts +114 -1
  32. package/dist/executor/a0.js +216 -4
  33. package/dist/fsutil.d.ts +3 -2
  34. package/dist/fsutil.js +7 -4
  35. package/dist/journal/schema.d.ts +1 -1
  36. package/dist/log.d.ts +2 -1
  37. package/dist/log.js +10 -2
  38. package/dist/mention.d.ts +45 -0
  39. package/dist/mention.js +107 -0
  40. package/dist/pipeline/app.d.ts +126 -0
  41. package/dist/pipeline/app.js +250 -0
  42. package/dist/pipeline/budget.d.ts +1 -0
  43. package/dist/pipeline/budget.js +1 -1
  44. package/dist/pipeline/verify.d.ts +20 -3
  45. package/dist/pipeline/verify.js +189 -35
  46. package/dist/probe/persist.d.ts +68 -0
  47. package/dist/probe/persist.js +184 -0
  48. package/dist/probe/queue.d.ts +12 -0
  49. package/dist/probe/queue.js +10 -2
  50. package/dist/report/brand-assets.generated.d.ts +9 -0
  51. package/dist/report/brand-assets.generated.js +8 -0
  52. package/dist/report/comment.d.ts +99 -6
  53. package/dist/report/comment.js +292 -103
  54. package/dist/report/html.d.ts +50 -0
  55. package/dist/report/html.js +879 -0
  56. package/dist/report/manifest.d.ts +29 -0
  57. package/dist/report/manifest.js +37 -0
  58. package/dist/report/run.d.ts +54 -1
  59. package/dist/report/run.js +34 -9
  60. package/dist/report/viewmodel.d.ts +91 -0
  61. package/dist/report/viewmodel.js +241 -0
  62. package/dist/review/adjudicate.d.ts +6 -6
  63. package/dist/review/adjudicate.js +2 -2
  64. package/dist/review/inline.d.ts +44 -0
  65. package/dist/review/inline.js +95 -0
  66. package/dist/review/packs.d.ts +21 -0
  67. package/dist/review/packs.js +47 -0
  68. package/dist/review/scope.d.ts +16 -0
  69. package/dist/review/scope.js +74 -0
  70. package/dist/review/secrets.d.ts +10 -10
  71. package/dist/review/secrets.js +7 -7
  72. package/dist/review/testfiles.d.ts +18 -0
  73. package/dist/review/testfiles.js +26 -0
  74. package/dist/review/triage.d.ts +1 -1
  75. package/dist/review/triage.js +10 -10
  76. package/dist/review/validate.d.ts +41 -0
  77. package/dist/review/validate.js +76 -0
  78. package/dist/ui/errors.d.ts +54 -0
  79. package/dist/ui/errors.js +236 -0
  80. package/dist/ui/style.d.ts +34 -0
  81. package/dist/ui/style.js +48 -0
  82. package/dist/ui/summary.d.ts +38 -0
  83. package/dist/ui/summary.js +101 -0
  84. package/dist/vision/cost.d.ts +1 -1
  85. package/dist/vision/decisions.d.ts +9 -3
  86. package/dist/vision/decisions.js +31 -21
  87. package/dist/vision/openrouter.d.ts +4 -0
  88. package/dist/vision/openrouter.js +30 -4
  89. 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;
@@ -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
+ }
@@ -1,4 +1,4 @@
1
- export type CallKind = 'ground' | 'heal' | 'assert' | 'code' | 'decide';
1
+ export type CallKind = 'ground' | 'heal' | 'assert' | 'code' | 'decide' | 'explore';
2
2
  export interface CallCost {
3
3
  model: string;
4
4
  provider: string;
@@ -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 Jev slug — the alias `~typesafe/jev-latest` drifts silently and
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
- constructor(kind: DecisionErrorKind, message: string, retryable: boolean);
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 Jev lanes (secrets, findings, triage). */
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;
@@ -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 Jev slug — the alias `~typesafe/jev-latest` drifts silently and
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
- constructor(kind, message, retryable) {
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 Jev lanes (secrets, findings, triage). */
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
- function classifyStatus(status) {
102
- if (status === 401 || status === 403) {
103
- return new DecisionError('auth', `decide: HTTP ${status} — check OPENROUTER_API_KEY`, false);
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 new DecisionError('validation', `decide: HTTP ${status} — request rejected`, false);
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
- return new DecisionError('unexpected', `decide: HTTP ${status}`, false);
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} — retrying in ${delay}ms`);
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 — retrying once');
228
+ debug('decisions', 'timeout, retrying once');
219
229
  continue;
220
230
  }
221
231
  break;