@delegance/claude-autopilot 5.0.6 → 5.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/adapters/council/claude.js +11 -1
- package/dist/src/adapters/council/openai.js +18 -2
- package/dist/src/adapters/council/types.d.ts +10 -1
- package/dist/src/adapters/review-engine/parse-output.js +26 -8
- package/dist/src/cli/council.js +15 -2
- package/dist/src/cli/fix.js +21 -3
- package/dist/src/cli/pr-desc.d.ts +6 -0
- package/dist/src/cli/pr-desc.js +14 -1
- package/dist/src/cli/scan.js +8 -8
- package/dist/src/core/council/runner.d.ts +9 -1
- package/dist/src/core/council/runner.js +36 -5
- package/dist/src/core/persist/cost-log.js +14 -3
- package/package.json +1 -1
|
@@ -3,6 +3,9 @@ import { GuardrailError } from "../../core/errors.js";
|
|
|
3
3
|
import { classifyError } from "../review-engine/prompt-builder.js";
|
|
4
4
|
const SYSTEM_PROMPT = `You are a technical advisor reviewing a software design decision. Evaluate the provided context and question critically. Be direct and specific. Surface tradeoffs, risks, and your recommendation.`;
|
|
5
5
|
const MAX_OUTPUT_TOKENS = 2048;
|
|
6
|
+
// Default Opus 4.7 rates — env override for other models.
|
|
7
|
+
const COST_PER_M_INPUT = Number(process.env.CLAUDE_COST_INPUT_PER_M ?? 15.0);
|
|
8
|
+
const COST_PER_M_OUTPUT = Number(process.env.CLAUDE_COST_OUTPUT_PER_M ?? 75.0);
|
|
6
9
|
export function makeClaudeCouncilAdapter(model, label) {
|
|
7
10
|
return {
|
|
8
11
|
label,
|
|
@@ -30,10 +33,17 @@ export function makeClaudeCouncilAdapter(model, label) {
|
|
|
30
33
|
retryable: code === 'rate_limit',
|
|
31
34
|
});
|
|
32
35
|
}
|
|
33
|
-
|
|
36
|
+
const text = response.content
|
|
34
37
|
.filter(b => b.type === 'text')
|
|
35
38
|
.map(b => b.text)
|
|
36
39
|
.join('');
|
|
40
|
+
const usage = response.usage ? {
|
|
41
|
+
input: response.usage.input_tokens,
|
|
42
|
+
output: response.usage.output_tokens,
|
|
43
|
+
costUSD: (response.usage.input_tokens / 1_000_000) * COST_PER_M_INPUT +
|
|
44
|
+
(response.usage.output_tokens / 1_000_000) * COST_PER_M_OUTPUT,
|
|
45
|
+
} : undefined;
|
|
46
|
+
return { text, usage };
|
|
37
47
|
},
|
|
38
48
|
};
|
|
39
49
|
}
|
|
@@ -13,6 +13,10 @@ const MAX_OUTPUT_TOKENS = 2048;
|
|
|
13
13
|
function isResponsesOnlyModel(model) {
|
|
14
14
|
return /codex|^o[1-9]|^gpt-5\.3-/i.test(model);
|
|
15
15
|
}
|
|
16
|
+
// Per-million-token rates for gpt-5.3-codex (override via env for other models).
|
|
17
|
+
// Mirrors the review-engine codex adapter's pricing.
|
|
18
|
+
const COST_PER_M_INPUT = Number(process.env.CODEX_COST_INPUT_PER_M ?? 1.25);
|
|
19
|
+
const COST_PER_M_OUTPUT = Number(process.env.CODEX_COST_OUTPUT_PER_M ?? 10.0);
|
|
16
20
|
export function makeOpenAICouncilAdapter(model, label) {
|
|
17
21
|
return {
|
|
18
22
|
label,
|
|
@@ -31,7 +35,13 @@ export function makeOpenAICouncilAdapter(model, label) {
|
|
|
31
35
|
input: userInput,
|
|
32
36
|
max_output_tokens: MAX_OUTPUT_TOKENS,
|
|
33
37
|
});
|
|
34
|
-
|
|
38
|
+
const usage = response.usage ? {
|
|
39
|
+
input: response.usage.input_tokens,
|
|
40
|
+
output: response.usage.output_tokens,
|
|
41
|
+
costUSD: (response.usage.input_tokens / 1_000_000) * COST_PER_M_INPUT +
|
|
42
|
+
(response.usage.output_tokens / 1_000_000) * COST_PER_M_OUTPUT,
|
|
43
|
+
} : undefined;
|
|
44
|
+
return { text: response.output_text ?? '', usage };
|
|
35
45
|
}
|
|
36
46
|
const response = await client.chat.completions.create({
|
|
37
47
|
model,
|
|
@@ -41,7 +51,13 @@ export function makeOpenAICouncilAdapter(model, label) {
|
|
|
41
51
|
{ role: 'user', content: userInput },
|
|
42
52
|
],
|
|
43
53
|
});
|
|
44
|
-
|
|
54
|
+
const usage = response.usage ? {
|
|
55
|
+
input: response.usage.prompt_tokens,
|
|
56
|
+
output: response.usage.completion_tokens,
|
|
57
|
+
costUSD: (response.usage.prompt_tokens / 1_000_000) * COST_PER_M_INPUT +
|
|
58
|
+
(response.usage.completion_tokens / 1_000_000) * COST_PER_M_OUTPUT,
|
|
59
|
+
} : undefined;
|
|
60
|
+
return { text: response.choices[0]?.message?.content ?? '', usage };
|
|
45
61
|
}
|
|
46
62
|
catch (err) {
|
|
47
63
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -1,5 +1,14 @@
|
|
|
1
|
+
export interface CouncilUsage {
|
|
2
|
+
input: number;
|
|
3
|
+
output: number;
|
|
4
|
+
costUSD?: number;
|
|
5
|
+
}
|
|
6
|
+
export interface CouncilConsultResult {
|
|
7
|
+
text: string;
|
|
8
|
+
usage?: CouncilUsage;
|
|
9
|
+
}
|
|
1
10
|
export interface CouncilAdapter {
|
|
2
11
|
readonly label: string;
|
|
3
|
-
consult(prompt: string, context: string): Promise<
|
|
12
|
+
consult(prompt: string, context: string): Promise<CouncilConsultResult>;
|
|
4
13
|
}
|
|
5
14
|
//# sourceMappingURL=types.d.ts.map
|
|
@@ -22,9 +22,11 @@ const CODE_EXT = String.raw `(?:` +
|
|
|
22
22
|
// 3
|
|
23
23
|
String.raw `asm|cjs|clj|cpp|css|edn|elm|env|erl|exs|fsi|fsx|gql|hcl|hpp|htm|ini|jsx|lua|mdx|mjs|mli|nim|php|sol|sql|tsx|vue|xml|yml|zig|zsh|` +
|
|
24
24
|
// 2
|
|
25
|
-
String.raw `cc|cs|ex|fs|go|hs|jl|js|kt|md|mk|ml|mm|pl|pm|py|rb|rs|sc|sh|tf|ts
|
|
26
|
-
//
|
|
27
|
-
|
|
25
|
+
String.raw `cc|cs|ex|fs|go|hs|jl|js|kt|md|mk|ml|mm|pl|pm|py|rb|rs|sc|sh|tf|ts` +
|
|
26
|
+
// (single-letter code extensions like c/d/h/m/r/s are intentionally NOT in
|
|
27
|
+
// the bare-reference alternation: prose like "fn.r" or "lib.h" matches as
|
|
28
|
+
// a "file" too easily and breaks the `fix` command. They still match when
|
|
29
|
+
// explicitly backtick-wrapped — the LLM has to signal intent.)
|
|
28
30
|
String.raw `)`;
|
|
29
31
|
// Matches "path/to/file.ts:42" (bare with known ext), "`path/to/file.ts`" (any
|
|
30
32
|
// ext when explicitly backtick-wrapped). Backtick-wrapped accepts any extension
|
|
@@ -33,10 +35,20 @@ const FILE_REF = new RegExp(String.raw `(?:` +
|
|
|
33
35
|
String.raw `\x60([^\x60]+\.[a-z]{1,6})\x60` +
|
|
34
36
|
String.raw `|(\b[\w./\-]+\.` + CODE_EXT + String.raw `)(?::(\d+))?` +
|
|
35
37
|
String.raw `)`, 'i');
|
|
38
|
+
// Matches "line 42", "on line 42", "at line 42" — used as a fallback when the
|
|
39
|
+
// LLM mentions a line number separately from the file ref. Critical for `fix`:
|
|
40
|
+
// without a line, the fixer can't extract a code snippet, so findings without
|
|
41
|
+
// `line` got silently dropped from `fix --dry-run` (the path-only finding case
|
|
42
|
+
// was the most-cited demo torpedo from the 5.0.7 stress test).
|
|
43
|
+
const LINE_REF = /\b(?:on |at )?line\s+(\d+)\b/i;
|
|
36
44
|
function extractFileRef(text) {
|
|
37
45
|
const m = text.match(FILE_REF);
|
|
38
|
-
if (!m)
|
|
39
|
-
|
|
46
|
+
if (!m) {
|
|
47
|
+
// No file ref at all — but maybe the body still has "line N" prose we can
|
|
48
|
+
// surface. Caller treats file `<unspecified>` as a sentinel either way.
|
|
49
|
+
const lm = text.match(LINE_REF);
|
|
50
|
+
return lm ? { file: '<unspecified>', line: parseInt(lm[1], 10) } : { file: '<unspecified>' };
|
|
51
|
+
}
|
|
40
52
|
const raw = (m[1] ?? m[2]);
|
|
41
53
|
// Skip version strings (v1.2.3), bare dotfile extensions with no path
|
|
42
54
|
// separator, and known prose abbreviations that slipped through the regex
|
|
@@ -45,10 +57,16 @@ function extractFileRef(text) {
|
|
|
45
57
|
if (/^v?\d/.test(raw) ||
|
|
46
58
|
(!raw.includes('/') && raw.startsWith('.') && raw.split('.').length === 2) ||
|
|
47
59
|
/^(?:e\.g|i\.e|etc|vs|cf|al|U\.S|U\.K)$/i.test(raw)) {
|
|
48
|
-
|
|
60
|
+
const lm = text.match(LINE_REF);
|
|
61
|
+
return lm ? { file: '<unspecified>', line: parseInt(lm[1], 10) } : { file: '<unspecified>' };
|
|
49
62
|
}
|
|
50
|
-
|
|
51
|
-
|
|
63
|
+
// Prefer the colon-line from the file ref (`foo.ts:42`); fall back to a
|
|
64
|
+
// separately-mentioned line ("line 42") only when the file ref didn't carry one.
|
|
65
|
+
const colonLine = m[3] ? parseInt(m[3], 10) : undefined;
|
|
66
|
+
if (colonLine !== undefined)
|
|
67
|
+
return { file: raw, line: colonLine };
|
|
68
|
+
const lm = text.match(LINE_REF);
|
|
69
|
+
return lm ? { file: raw, line: parseInt(lm[1], 10) } : { file: raw };
|
|
52
70
|
}
|
|
53
71
|
// Accepts any of: `### [CRITICAL] title`, `### CRITICAL title`, `### **CRITICAL** title`,
|
|
54
72
|
// `### **[CRITICAL]** title`. Severity capture works across variants.
|
package/dist/src/cli/council.js
CHANGED
|
@@ -7,6 +7,7 @@ import { runCouncil } from "../core/council/runner.js";
|
|
|
7
7
|
import { makeClaudeCouncilAdapter } from "../adapters/council/claude.js";
|
|
8
8
|
import { makeOpenAICouncilAdapter } from "../adapters/council/openai.js";
|
|
9
9
|
import { GuardrailError } from "../core/errors.js";
|
|
10
|
+
import { appendCostLog } from "../core/persist/cost-log.js";
|
|
10
11
|
function makeAdapter(entry) {
|
|
11
12
|
switch (entry.adapter) {
|
|
12
13
|
case 'claude': return makeClaudeCouncilAdapter(entry.model, entry.label);
|
|
@@ -58,14 +59,26 @@ export async function runCouncilCmd(opts) {
|
|
|
58
59
|
}
|
|
59
60
|
const adapters = councilConfig.models.map(makeAdapter);
|
|
60
61
|
const synthesizer = opts.noSynthesize
|
|
61
|
-
? { label: 'none', consult: async () => '' }
|
|
62
|
+
? { label: 'none', consult: async () => ({ text: '' }) }
|
|
62
63
|
: makeAdapter(councilConfig.synthesizer);
|
|
63
|
-
const
|
|
64
|
+
const start = Date.now();
|
|
65
|
+
const { result, usage } = await runCouncil(councilConfig, adapters, synthesizer, opts.prompt, contextDoc);
|
|
64
66
|
// When no-synthesize, clear the empty synthesis object
|
|
65
67
|
if (opts.noSynthesize && result.synthesis?.text === '') {
|
|
66
68
|
delete result['synthesis'];
|
|
67
69
|
}
|
|
68
70
|
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
71
|
+
// Persist to cost log so `claude-autopilot costs` reflects council runs
|
|
72
|
+
// (previously dropped — only scan + run pipeline were tracked, leading to
|
|
73
|
+
// misleadingly low lifetime totals after a council-heavy session).
|
|
74
|
+
appendCostLog(cwd, {
|
|
75
|
+
timestamp: new Date().toISOString(),
|
|
76
|
+
files: 0,
|
|
77
|
+
inputTokens: usage.inputTokens,
|
|
78
|
+
outputTokens: usage.outputTokens,
|
|
79
|
+
costUSD: usage.costUSD,
|
|
80
|
+
durationMs: Date.now() - start,
|
|
81
|
+
});
|
|
69
82
|
if (result.status === 'failed')
|
|
70
83
|
return 2;
|
|
71
84
|
if (result.status === 'partial')
|
package/dist/src/cli/fix.js
CHANGED
|
@@ -38,8 +38,13 @@ export async function runFix(options = {}) {
|
|
|
38
38
|
console.log(fmt('yellow', '[fix] No cached findings — run `guardrail scan <path>` or `guardrail run` first.'));
|
|
39
39
|
return 0;
|
|
40
40
|
}
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
// Two gates:
|
|
42
|
+
// - "actionable": has a real file path. Surfaced in dry-run so the user sees
|
|
43
|
+
// findings even when the LLM didn't pin a line number.
|
|
44
|
+
// - "fixable": also has a line. The LLM-fix loop needs both to extract a
|
|
45
|
+
// code snippet around the finding location.
|
|
46
|
+
const actionable = findings.filter(f => {
|
|
47
|
+
if (!f.file || f.file === '<unspecified>' || f.file === '<pipeline>')
|
|
43
48
|
return false;
|
|
44
49
|
if (severityFilter === 'all')
|
|
45
50
|
return true;
|
|
@@ -47,8 +52,21 @@ export async function runFix(options = {}) {
|
|
|
47
52
|
return f.severity === 'critical';
|
|
48
53
|
return f.severity === 'critical' || f.severity === 'warning';
|
|
49
54
|
});
|
|
55
|
+
const fixable = actionable.filter(f => f.line && f.line > 0);
|
|
56
|
+
if (actionable.length === 0) {
|
|
57
|
+
console.log(fmt('yellow', `[fix] No actionable findings (severity=${severityFilter}, need file path).`));
|
|
58
|
+
return 0;
|
|
59
|
+
}
|
|
50
60
|
if (fixable.length === 0) {
|
|
51
|
-
|
|
61
|
+
const verb = actionable.length === 1 ? 'has' : 'have';
|
|
62
|
+
const noun = actionable.length === 1 ? 'finding' : 'findings';
|
|
63
|
+
console.log(fmt('yellow', `[fix] ${actionable.length} ${noun} ${verb} file but no line — model output was line-less. Re-run scan with --ask "include line numbers" or run \`claude-autopilot run\` for richer extraction.`));
|
|
64
|
+
for (const f of actionable) {
|
|
65
|
+
const sev = f.severity === 'critical' ? fmt('red', 'CRITICAL')
|
|
66
|
+
: f.severity === 'warning' ? fmt('yellow', 'WARNING ')
|
|
67
|
+
: fmt('dim', 'NOTE ');
|
|
68
|
+
console.log(` [${sev}] ${fmt('dim', f.file)} ${f.message}`);
|
|
69
|
+
}
|
|
52
70
|
return 0;
|
|
53
71
|
}
|
|
54
72
|
const modeNote = options.dryRun ? ' (dry run)' : options.yes ? '' : ' (interactive — use --yes to skip prompts)';
|
|
@@ -13,8 +13,14 @@ export interface PrDescOptions {
|
|
|
13
13
|
kind: string;
|
|
14
14
|
}): Promise<{
|
|
15
15
|
rawOutput: string;
|
|
16
|
+
usage?: {
|
|
17
|
+
input: number;
|
|
18
|
+
output: number;
|
|
19
|
+
costUSD?: number;
|
|
20
|
+
};
|
|
16
21
|
}>;
|
|
17
22
|
};
|
|
23
|
+
_cwd?: string;
|
|
18
24
|
}
|
|
19
25
|
export interface PrDescResult {
|
|
20
26
|
title: string;
|
package/dist/src/cli/pr-desc.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
2
|
import { execSync, spawnSync } from 'node:child_process';
|
|
3
|
+
import { appendCostLog } from "../core/persist/cost-log.js";
|
|
3
4
|
export function truncateDiff(diff, charLimit = 6000) {
|
|
4
5
|
if (diff.length <= charLimit)
|
|
5
6
|
return diff;
|
|
@@ -87,7 +88,8 @@ export async function runPrDesc(options) {
|
|
|
87
88
|
const findings = options._cachedFindings ?? loadCachedFindings();
|
|
88
89
|
const prompt = buildPrompt(branchName, truncateDiff(diff), summarizeFindings(findings));
|
|
89
90
|
const engine = options._reviewEngine ?? await resolveEngine();
|
|
90
|
-
const
|
|
91
|
+
const start = Date.now();
|
|
92
|
+
const { rawOutput, usage } = await engine.review({ content: prompt, kind: 'pr-diff' });
|
|
91
93
|
// Extract first non-empty bullet from the model's Summary section as a
|
|
92
94
|
// last-resort title fallback when the model didn't emit `Title: ...`.
|
|
93
95
|
const firstSummaryLine = rawOutput.split('\n')
|
|
@@ -101,6 +103,17 @@ export async function runPrDesc(options) {
|
|
|
101
103
|
else {
|
|
102
104
|
process.stdout.write(formatted + '\n');
|
|
103
105
|
}
|
|
106
|
+
// Persist to cost log AFTER the output is emitted. The function itself
|
|
107
|
+
// swallows write errors (see core/persist/cost-log.ts) so a read-only FS
|
|
108
|
+
// or full disk doesn't kill commands that already succeeded.
|
|
109
|
+
appendCostLog(options._cwd ?? process.cwd(), {
|
|
110
|
+
timestamp: new Date().toISOString(),
|
|
111
|
+
files: 1,
|
|
112
|
+
inputTokens: usage?.input ?? 0,
|
|
113
|
+
outputTokens: usage?.output ?? 0,
|
|
114
|
+
costUSD: usage?.costUSD ?? 0,
|
|
115
|
+
durationMs: Date.now() - start,
|
|
116
|
+
});
|
|
104
117
|
if (options.post) {
|
|
105
118
|
return createPr(title, body, options.yes ?? false);
|
|
106
119
|
}
|
package/dist/src/cli/scan.js
CHANGED
|
@@ -133,17 +133,17 @@ export async function runScan(options = {}) {
|
|
|
133
133
|
cwd,
|
|
134
134
|
gitSummary: focusHint,
|
|
135
135
|
});
|
|
136
|
-
// Single-file scan
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
//
|
|
141
|
-
// `<unspecified>` and
|
|
136
|
+
// Single-file scan: every finding is about that file (or its imports).
|
|
137
|
+
// The LLM sometimes emits prose tokens like "n.r" or "fn.c" that the parser
|
|
138
|
+
// greedily matches as a file ref, producing junk paths that break `fix`.
|
|
139
|
+
// For single-file scan we KNOW the file — overwrite unconditionally rather
|
|
140
|
+
// than only filling `<unspecified>`. The 5.0.6 fallback was conditional on
|
|
141
|
+
// `<unspecified>` and missed the prose-noise case, leaving findings with
|
|
142
|
+
// bogus `n.r` paths that broke `fix --severity all` ("no fixable findings").
|
|
142
143
|
if (relFiles.length === 1) {
|
|
143
144
|
const onlyFile = relFiles[0];
|
|
144
145
|
for (const f of result.findings) {
|
|
145
|
-
|
|
146
|
-
f.file = onlyFile;
|
|
146
|
+
f.file = onlyFile;
|
|
147
147
|
}
|
|
148
148
|
}
|
|
149
149
|
// Apply ignore rules
|
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
import type { CouncilConfig, CouncilResult } from './types.ts';
|
|
2
2
|
import type { CouncilAdapter } from '../../adapters/council/types.ts';
|
|
3
|
-
export
|
|
3
|
+
export interface CouncilRunOutput {
|
|
4
|
+
result: CouncilResult;
|
|
5
|
+
usage: {
|
|
6
|
+
inputTokens: number;
|
|
7
|
+
outputTokens: number;
|
|
8
|
+
costUSD: number;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export declare function runCouncil(config: CouncilConfig, adapters: CouncilAdapter[], synthesizer: CouncilAdapter, prompt: string, contextDoc: string): Promise<CouncilRunOutput>;
|
|
4
12
|
//# sourceMappingURL=runner.d.ts.map
|
|
@@ -4,13 +4,19 @@ async function consultWithTimeout(adapter, prompt, context, timeoutMs) {
|
|
|
4
4
|
const start = Date.now();
|
|
5
5
|
let timer;
|
|
6
6
|
try {
|
|
7
|
-
const
|
|
7
|
+
const consultResult = await Promise.race([
|
|
8
8
|
adapter.consult(prompt, context),
|
|
9
9
|
new Promise((_, reject) => {
|
|
10
10
|
timer = setTimeout(() => reject(new Error('timeout')), timeoutMs);
|
|
11
11
|
}),
|
|
12
12
|
]);
|
|
13
|
-
return {
|
|
13
|
+
return {
|
|
14
|
+
label: adapter.label,
|
|
15
|
+
status: 'ok',
|
|
16
|
+
text: consultResult.text,
|
|
17
|
+
latencyMs: Date.now() - start,
|
|
18
|
+
usage: consultResult.usage,
|
|
19
|
+
};
|
|
14
20
|
}
|
|
15
21
|
catch (err) {
|
|
16
22
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -30,9 +36,27 @@ export async function runCouncil(config, adapters, synthesizer, prompt, contextD
|
|
|
30
36
|
const run_id = crypto.randomUUID();
|
|
31
37
|
const context = windowContext(contextDoc, config.parallelInputMaxTokens);
|
|
32
38
|
const responses = await Promise.all(adapters.map(a => consultWithTimeout(a, prompt, context, config.timeoutMs)));
|
|
39
|
+
const aggregateUsage = (entries) => {
|
|
40
|
+
let inputTokens = 0, outputTokens = 0, costUSD = 0;
|
|
41
|
+
for (const e of entries) {
|
|
42
|
+
if (e.usage) {
|
|
43
|
+
inputTokens += e.usage.input;
|
|
44
|
+
outputTokens += e.usage.output;
|
|
45
|
+
costUSD += e.usage.costUSD ?? 0;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return { inputTokens, outputTokens, costUSD };
|
|
49
|
+
};
|
|
50
|
+
// Strip internal `usage` field before serializing to the public CouncilResult
|
|
51
|
+
// schema — usage is summed and surfaced separately so the CLI can log it to
|
|
52
|
+
// the cost ledger without leaking it into the JSON wire format.
|
|
53
|
+
const publicResponses = responses.map(({ usage: _u, ...rest }) => rest);
|
|
33
54
|
const successful = responses.filter(r => r.status === 'ok');
|
|
34
55
|
if (successful.length < config.minSuccessfulResponses) {
|
|
35
|
-
return {
|
|
56
|
+
return {
|
|
57
|
+
result: { schema_version: 1, run_id, status: 'failed', prompt, responses: publicResponses },
|
|
58
|
+
usage: aggregateUsage(responses),
|
|
59
|
+
};
|
|
36
60
|
}
|
|
37
61
|
const responseSections = successful
|
|
38
62
|
.map(r => `### ${r.label}\n${r.text}`)
|
|
@@ -47,13 +71,20 @@ export async function runCouncil(config, adapters, synthesizer, prompt, contextD
|
|
|
47
71
|
// Synthesizer shares the same per-call timeout as model calls so a hung
|
|
48
72
|
// synthesizer API doesn't block the whole command indefinitely.
|
|
49
73
|
const synthResponse = await consultWithTimeout(synthesizer, synthesisPrompt, synthesisCtx, config.timeoutMs);
|
|
74
|
+
const totalUsage = aggregateUsage([...responses, synthResponse]);
|
|
50
75
|
// status:'ok' means the synthesizer call itself completed without error.
|
|
51
76
|
// Empty text is valid (e.g. the --no-synthesize stub that intentionally
|
|
52
77
|
// returns ''); only treat actual failures/timeouts as partial.
|
|
53
78
|
if (synthResponse.status === 'ok') {
|
|
54
79
|
const synthesis = { label: synthesizer.label, text: synthResponse.text ?? '', latencyMs: synthResponse.latencyMs };
|
|
55
|
-
return {
|
|
80
|
+
return {
|
|
81
|
+
result: { schema_version: 1, run_id, status: 'success', prompt, responses: publicResponses, synthesis },
|
|
82
|
+
usage: totalUsage,
|
|
83
|
+
};
|
|
56
84
|
}
|
|
57
|
-
return {
|
|
85
|
+
return {
|
|
86
|
+
result: { schema_version: 1, run_id, status: 'partial', prompt, responses: publicResponses },
|
|
87
|
+
usage: totalUsage,
|
|
88
|
+
};
|
|
58
89
|
}
|
|
59
90
|
//# sourceMappingURL=runner.js.map
|
|
@@ -3,9 +3,20 @@ import * as path from 'node:path';
|
|
|
3
3
|
const CACHE_DIR = '.guardrail-cache';
|
|
4
4
|
const LOG_FILE = 'costs.jsonl';
|
|
5
5
|
export function appendCostLog(cwd, entry) {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
// Cost log is observability, not a contract. A failed write (read-only FS,
|
|
7
|
+
// full disk, permission error) must NEVER block the caller — every callsite
|
|
8
|
+
// calls this *after* its primary output is emitted, and a throw here would
|
|
9
|
+
// cause unhandled-rejection crashes after work has already succeeded.
|
|
10
|
+
// Bugbot HIGH on PR #51 surfaced this for pr-desc/council; consolidating
|
|
11
|
+
// the swallow here so the same defense applies to scan/run automatically.
|
|
12
|
+
try {
|
|
13
|
+
const dir = path.join(cwd, CACHE_DIR);
|
|
14
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
15
|
+
fs.appendFileSync(path.join(dir, LOG_FILE), JSON.stringify(entry) + '\n', 'utf8');
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
// Intentionally empty — observability failures should not surface to users.
|
|
19
|
+
}
|
|
9
20
|
}
|
|
10
21
|
export function readCostLog(cwd) {
|
|
11
22
|
const p = path.join(cwd, CACHE_DIR, LOG_FILE);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@delegance/claude-autopilot",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.8",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Autonomous development pipeline for Claude Code: brainstorm → spec → plan → implement → migrate → validate → PR → review → merge. Multi-model, local-first, every phase a skill you can intervene in.",
|
|
6
6
|
"keywords": [
|