codehere 0.7.1 → 1.0.1
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 +22 -13
- package/dist/application/services/verdict-service.js +154 -0
- package/dist/application/use-cases/assess-cross-vendor.js +126 -0
- package/dist/application/use-cases/build-cross-vendor-verdict.js +118 -0
- package/dist/application/use-cases/export-aat.js +107 -0
- package/dist/application/use-cases/generate-acceptance-criteria.js +223 -0
- package/dist/application/use-cases/generate-final-report.js +140 -15
- package/dist/application/use-cases/generate-narration.js +5 -3
- package/dist/application/use-cases/generate-thread-title.js +5 -2
- package/dist/application/use-cases/narrator-prompt-template.js +1 -1
- package/dist/application/use-cases/plan-auto-review.js +57 -0
- package/dist/application/use-cases/reconcile-audit.js +100 -0
- package/dist/application/use-cases/rerun-tests.js +226 -0
- package/dist/application/use-cases/search-sessions.js +70 -0
- package/dist/application/use-cases/should-auto-review.js +71 -0
- package/dist/application/use-cases/thread-export.js +90 -0
- package/dist/application/use-cases/verify-claim.js +187 -5
- package/dist/domain/audit.js +11 -0
- package/dist/domain/services/codebase-detector.js +0 -6
- package/dist/domain/task.js +91 -0
- package/dist/error-handler.js +3 -115
- package/dist/index.js +279 -31
- package/dist/infrastructure/completion/bash-completion.js +7 -3
- package/dist/infrastructure/completion/fish-completion.js +7 -12
- package/dist/infrastructure/completion/zsh-completion.js +7 -12
- package/dist/infrastructure/config/config-manager.js +4 -3
- package/dist/infrastructure/config/product-context.js +1 -19
- package/dist/infrastructure/context/adapters/figma-source.js +37 -2
- package/dist/infrastructure/context/adapters/github-issue-source.js +3 -2
- package/dist/infrastructure/context/github-api.js +110 -0
- package/dist/infrastructure/context/github-device-flow.js +65 -0
- package/dist/infrastructure/context/oauth-store.js +5 -2
- package/dist/infrastructure/context/registry-bootstrap.js +0 -4
- package/dist/infrastructure/cost/budget-config.js +4 -3
- package/dist/infrastructure/cost/cost-router.js +16 -23
- package/dist/infrastructure/cost/spend-aggregator.js +91 -16
- package/dist/infrastructure/cost/team-ledger.js +355 -0
- package/dist/infrastructure/discovery/discovery-log.js +85 -0
- package/dist/infrastructure/hooks/audit-tip-signature.js +336 -0
- package/dist/infrastructure/hooks/claude-sdk-hook.js +139 -3
- package/dist/infrastructure/hooks/hook-mode.js +20 -4
- package/dist/infrastructure/hooks/mirror-tool-call-to-audit.js +8 -2
- package/dist/infrastructure/hooks/pre-prompt-scan.js +3 -3
- package/dist/infrastructure/observability/execution-tracer.js +1 -1
- package/dist/infrastructure/orchestration/agents-config-seed.js +23 -11
- package/dist/infrastructure/orchestration/aider-adapter.js +188 -24
- package/dist/infrastructure/orchestration/artifacts-extractor.js +60 -2
- package/dist/infrastructure/orchestration/cassette-recorder.js +1 -1
- package/dist/infrastructure/orchestration/claude-code-adapter.js +117 -3
- package/dist/infrastructure/orchestration/codex-adapter.js +52 -16
- package/dist/infrastructure/orchestration/conductor-adapter.js +46 -7
- package/dist/infrastructure/orchestration/context-handoff.js +32 -14
- package/dist/infrastructure/orchestration/detect-ollama.js +75 -0
- package/dist/infrastructure/orchestration/failover-config.js +1 -11
- package/dist/infrastructure/orchestration/gemini-adapter.js +92 -12
- package/dist/infrastructure/orchestration/generic-cli-adapter.js +134 -3
- package/dist/infrastructure/orchestration/graphify-context.js +14 -59
- package/dist/infrastructure/orchestration/memory-injector.js +4 -1
- package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +167 -9
- package/dist/infrastructure/orchestration/opencode-adapter.js +84 -14
- package/dist/infrastructure/orchestration/progress-aggregator.js +275 -23
- package/dist/infrastructure/orchestration/session-manager.js +593 -94
- package/dist/infrastructure/orchestration/session-persistence.js +177 -41
- package/dist/infrastructure/orchestration/thread-key-store.js +1 -1
- package/dist/infrastructure/orchestration/write-pass-store.js +0 -8
- package/dist/infrastructure/protocols/cloud-sync.js +2 -2
- package/dist/infrastructure/protocols/mcp-helpers.js +1 -1
- package/dist/infrastructure/protocols/mcp-server-impl.js +1498 -354
- package/dist/infrastructure/protocols/mcp-tools.js +401 -0
- package/dist/infrastructure/protocols/ui/app-script-artifacts.js +121 -0
- package/dist/infrastructure/protocols/ui/app-script-audit-footer.js +44 -0
- package/dist/infrastructure/protocols/ui/app-script-audit-verify.js +56 -0
- package/dist/infrastructure/protocols/ui/app-script-composer-cost.js +588 -0
- package/dist/infrastructure/protocols/ui/app-script-context-sources.js +168 -0
- package/dist/infrastructure/protocols/ui/app-script-conversation.js +67 -0
- package/dist/infrastructure/protocols/ui/app-script-drawer.js +115 -0
- package/dist/infrastructure/protocols/ui/app-script-failover.js +133 -0
- package/dist/infrastructure/protocols/ui/app-script-github.js +113 -0
- package/dist/infrastructure/protocols/ui/app-script-observer.js +104 -0
- package/dist/infrastructure/protocols/ui/app-script-onboarding.js +80 -0
- package/dist/infrastructure/protocols/ui/app-script-overlay.js +203 -0
- package/dist/infrastructure/protocols/ui/app-script-recovery.js +136 -0
- package/dist/infrastructure/protocols/ui/app-script-render.js +166 -0
- package/dist/infrastructure/protocols/ui/app-script-share.js +78 -0
- package/dist/infrastructure/protocols/ui/app-script-sidebar.js +194 -0
- package/dist/infrastructure/protocols/ui/app-script-tour.js +78 -0
- package/dist/infrastructure/protocols/ui/app-script-wpass.js +312 -0
- package/dist/infrastructure/protocols/ui/app-script.js +2998 -0
- package/dist/infrastructure/protocols/ui/font-faces.js +7 -0
- package/dist/infrastructure/protocols/ui/styles.js +2147 -0
- package/dist/infrastructure/protocols/ui/tokens.js +96 -0
- package/dist/infrastructure/protocols/ui-page-v2.js +116 -4126
- package/dist/infrastructure/protocols/verify-page.js +884 -148
- package/dist/infrastructure/security/fix-code.js +144 -0
- package/dist/infrastructure/security/fix-secret.js +89 -0
- package/dist/infrastructure/security/permissions-config.js +3 -2
- package/dist/infrastructure/security/scan-types.js +10 -0
- package/dist/infrastructure/security/ship-scan.js +195 -0
- package/dist/infrastructure/security/trust-scorer.js +70 -3
- package/dist/infrastructure/security/vibe-checks.js +99 -0
- package/dist/infrastructure/storage/atomic-write.js +86 -0
- package/dist/infrastructure/storage/project-thread-mirror.js +151 -4
- package/dist/infrastructure/storage/project-thread-reader.js +262 -25
- package/dist/infrastructure/storage/project-thread-title-backfill.js +1 -1
- package/dist/monitoring.js +2 -2
- package/dist/presentation/cli/commands/audit-command.js +79 -4
- package/dist/presentation/cli/commands/budget-command.js +2 -2
- package/dist/presentation/cli/commands/check-command.js +96 -0
- package/dist/presentation/cli/commands/context-source-command.js +20 -1
- package/dist/presentation/cli/commands/help-command.js +5 -5
- package/dist/presentation/cli/commands/hook-command.js +5 -5
- package/dist/presentation/cli/commands/init-command.js +89 -66
- package/dist/presentation/cli/commands/memory-command.js +28 -28
- package/dist/presentation/cli/commands/permission-command.js +1 -12
- package/dist/presentation/cli/commands/protocols-builder.js +2 -2
- package/dist/presentation/cli/commands/protocols-command.js +4 -4
- package/dist/presentation/cli/commands/repo-map-builder.js +1 -1
- package/dist/presentation/cli/commands/report-command.js +48 -32
- package/dist/presentation/cli/commands/scan-command.js +52 -0
- package/dist/presentation/cli/commands/scan-tool-command.js +24 -8
- package/dist/presentation/cli/commands/search-command.js +51 -0
- package/dist/presentation/cli/commands/setup-command.js +31 -8
- package/dist/presentation/cli/commands/status-command.js +39 -86
- package/dist/presentation/cli/commands/team-command.js +256 -0
- package/dist/presentation/cli/commands/thread-command.js +87 -5
- package/dist/presentation/cli/commands/trust-command.js +53 -31
- package/dist/presentation/cli/commands/verify-command.js +74 -9
- package/dist/presentation/cli/error-display.js +0 -8
- package/dist/presentation/cli/repl.js +15 -7
- package/dist/presentation/cli/task-deliverable.js +36 -10
- package/dist/testing/replay-adapter.js +0 -19
- package/dist/ui.js +51 -239
- package/dist/utils/version.js +0 -6
- package/package.json +30 -10
- package/dist/domain/entities/task.js +0 -61
- package/dist/infrastructure/protocols/ui-page.js +0 -2377
- package/dist/infrastructure/security/input-validator.js +0 -152
- package/dist/infrastructure/storage/embeddings-path.js +0 -37
- package/dist/infrastructure/storage/task-repository.js +0 -206
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Acceptance-criteria agent — REQUEST_METHOD.md stage 2 (the Contract), the
|
|
3
|
+
* keystone of the request method.
|
|
4
|
+
*
|
|
5
|
+
* Before an agent runs, derive 2-5 testable acceptance criteria from the
|
|
6
|
+
* request: the up-front, checkable definition of "done" that verify (stage 5)
|
|
7
|
+
* later grades AGAINST, instead of judging the result post-hoc. This is the one
|
|
8
|
+
* change that turns "route a prompt and audit it after" into "make the agent
|
|
9
|
+
* work to a contract and prove it met it."
|
|
10
|
+
*
|
|
11
|
+
* Mirrors generate-thread-title.ts: single-shot, no streaming, `claude --bare`,
|
|
12
|
+
* ~1 cheap call, degrades SILENTLY to [] when no provider responds (a request
|
|
13
|
+
* with no contract still runs — the method is additive, never a hard gate here).
|
|
14
|
+
*/
|
|
15
|
+
import { spawn } from 'child_process';
|
|
16
|
+
import { isMockMode } from '../../infrastructure/orchestration/mocks/resolve-agent-spawn.js';
|
|
17
|
+
const CRITERIA_TIMEOUT_MS = 15_000;
|
|
18
|
+
const MAX_CRITERIA = 5;
|
|
19
|
+
const PROMPT_TEMPLATE = (firstPrompt, cwd) => `Generate 2 to 5 acceptance criteria for this coding request. Each criterion is one short, testable statement of what "done" means — something a reviewer could check pass/fail.
|
|
20
|
+
|
|
21
|
+
Working directory: ${cwd}
|
|
22
|
+
Request: ${firstPrompt}
|
|
23
|
+
|
|
24
|
+
Rules:
|
|
25
|
+
- 2 to 5 criteria, ONE per line
|
|
26
|
+
- Each is a checkable OUTCOME (e.g. "greet() throws a TypeError when name is not a string"), not a step and not a vibe
|
|
27
|
+
- Ground each in the request only; do not invent scope
|
|
28
|
+
- No numbering, no bullets, no preamble, no trailing period
|
|
29
|
+
- Output ONLY the criteria, one per line
|
|
30
|
+
|
|
31
|
+
Criteria:`;
|
|
32
|
+
/**
|
|
33
|
+
* Parse model output into criteria. Rejects refusal/error-shaped lines (the
|
|
34
|
+
* `claude --bare` call can return rate-limit / auth text on stdout with exit 0,
|
|
35
|
+
* same failure mode the title agent guards against).
|
|
36
|
+
*/
|
|
37
|
+
export function sanitizeCriteria(raw) {
|
|
38
|
+
const bad = /\b(i cannot|i can'?t|i apologi[sz]e|i'?m sorry|as an ai|rate.?limit|unauthorized|request id|api key)\b/i;
|
|
39
|
+
return String(raw || '')
|
|
40
|
+
.split('\n')
|
|
41
|
+
.map((l) => l.replace(/^[\s\-*•–>#\d.)\]]+/, '').trim())
|
|
42
|
+
.filter((l) => l.length >= 3 && l.length <= 140 && !bad.test(l))
|
|
43
|
+
.slice(0, MAX_CRITERIA)
|
|
44
|
+
.map((text) => ({ text, met: null }));
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The scaling principle (REQUEST_METHOD §"no ceremony tax"): a trivial ask gets
|
|
48
|
+
* no contract. Trivial = very short, or a short read-only lookup. Everything
|
|
49
|
+
* else earns a contract. Keeps "just type and go" free for simple requests and
|
|
50
|
+
* skips the per-request LLM call where it would only be noise.
|
|
51
|
+
*/
|
|
52
|
+
export function isTrivialRequest(prompt) {
|
|
53
|
+
const p = String(prompt || '')
|
|
54
|
+
.trim()
|
|
55
|
+
.toLowerCase();
|
|
56
|
+
if (!p)
|
|
57
|
+
return true;
|
|
58
|
+
const words = p.split(/\s+/).filter(Boolean).length;
|
|
59
|
+
if (words <= 3)
|
|
60
|
+
return true;
|
|
61
|
+
if (words <= 6 && /^(list|ls|show|cat|print|echo|pwd|help|read|open|what is|what'?s)\b/.test(p))
|
|
62
|
+
return true;
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
/** Deterministic mock: grounded-enough criteria for demos + tests, no LLM. */
|
|
66
|
+
export function mockCriteria(firstPrompt) {
|
|
67
|
+
const p = String(firstPrompt || '').toLowerCase();
|
|
68
|
+
const out = [];
|
|
69
|
+
if (/rate.?limit|throttle/.test(p))
|
|
70
|
+
out.push('The rate limit is enforced under load');
|
|
71
|
+
if (/throw|error|validation|validate|guard|invalid/.test(p))
|
|
72
|
+
out.push('Invalid input is rejected with a clear error');
|
|
73
|
+
if (/test/.test(p))
|
|
74
|
+
out.push('Tests are added and pass');
|
|
75
|
+
if (!out.length)
|
|
76
|
+
out.push('The change does what the request asked', 'No existing behaviour regresses');
|
|
77
|
+
return out.slice(0, MAX_CRITERIA).map((text) => ({ text, met: null }));
|
|
78
|
+
}
|
|
79
|
+
function selectProviderBinary() {
|
|
80
|
+
const override = process.env.CODEHERE_CRITERIA_BINARY;
|
|
81
|
+
if (override)
|
|
82
|
+
return [override];
|
|
83
|
+
return ['claude'];
|
|
84
|
+
}
|
|
85
|
+
const GRADE_PROMPT = (criteria, ctx) => `You are grading whether a coding task met its acceptance criteria. For EACH criterion, answer strictly MET or UNMET based only on the evidence. If the evidence does not clearly show a criterion was met, answer UNMET (never guess "met").
|
|
86
|
+
|
|
87
|
+
Request: ${ctx.prompt}
|
|
88
|
+
Result summary: ${ctx.reportSummary || '(none)'}
|
|
89
|
+
Files changed: ${(ctx.files || []).join(', ') || '(none)'}
|
|
90
|
+
|
|
91
|
+
Criteria (one per line):
|
|
92
|
+
${criteria.map((c, i) => `${i + 1}. ${c.text}`).join('\n')}
|
|
93
|
+
|
|
94
|
+
Output exactly one line per criterion, in order, each "MET" or "UNMET" — nothing else.`;
|
|
95
|
+
/** Parse "MET/UNMET" lines back onto the criteria, in order. */
|
|
96
|
+
export function applyGrades(criteria, raw) {
|
|
97
|
+
const verdicts = String(raw || '')
|
|
98
|
+
.split('\n')
|
|
99
|
+
.map((l) => l.trim().toUpperCase())
|
|
100
|
+
.filter((l) => l === 'MET' || l === 'UNMET');
|
|
101
|
+
return criteria.map((c, i) => {
|
|
102
|
+
const v = verdicts[i];
|
|
103
|
+
return { text: c.text, met: v === 'MET' ? true : v === 'UNMET' ? false : (c.met ?? null) };
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Deterministic mock grade — honest by the same rule as the real one: a
|
|
108
|
+
* criterion is met ONLY if the run was verified AND the criterion's salient
|
|
109
|
+
* terms show up in the result; otherwise unmet. No fake green.
|
|
110
|
+
*/
|
|
111
|
+
export function mockGrade(criteria, ctx) {
|
|
112
|
+
const hay = ((ctx.reportSummary || '') + ' ' + (ctx.files || []).join(' ')).toLowerCase();
|
|
113
|
+
return criteria.map((c) => {
|
|
114
|
+
if (!ctx.verdictGreen)
|
|
115
|
+
return { text: c.text, met: false };
|
|
116
|
+
const terms = c.text
|
|
117
|
+
.toLowerCase()
|
|
118
|
+
.split(/\s+/)
|
|
119
|
+
.filter((w) => w.length > 4);
|
|
120
|
+
const hit = terms.filter((t) => hay.includes(t)).length;
|
|
121
|
+
return { text: c.text, met: hit >= Math.max(1, Math.ceil(terms.length * 0.3)) };
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Grade the contract against the result (REQUEST_METHOD stage 5). Runs at
|
|
126
|
+
* completion. Real path is a small single-shot assessment; folding this into
|
|
127
|
+
* the existing final-report call (truly no new call) is a noted follow-up.
|
|
128
|
+
* Returns the criteria unchanged (all pending) if grading can't run — never
|
|
129
|
+
* marks met without evidence.
|
|
130
|
+
*/
|
|
131
|
+
export async function gradeAcceptanceCriteria(criteria, ctx) {
|
|
132
|
+
if (!Array.isArray(criteria) || !criteria.length)
|
|
133
|
+
return criteria || [];
|
|
134
|
+
if (isMockMode())
|
|
135
|
+
return mockGrade(criteria, ctx);
|
|
136
|
+
const prompt = GRADE_PROMPT(criteria, ctx);
|
|
137
|
+
for (const binary of selectProviderBinary()) {
|
|
138
|
+
const raw = await new Promise((resolve) => {
|
|
139
|
+
let out = '';
|
|
140
|
+
let settled = false;
|
|
141
|
+
const done = (r) => {
|
|
142
|
+
if (!settled) {
|
|
143
|
+
settled = true;
|
|
144
|
+
resolve(r);
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
const child = spawn(binary, ['--bare', '-p', prompt], { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
148
|
+
const to = setTimeout(() => {
|
|
149
|
+
child.kill('SIGTERM');
|
|
150
|
+
done(null);
|
|
151
|
+
}, CRITERIA_TIMEOUT_MS);
|
|
152
|
+
child.on('error', () => {
|
|
153
|
+
clearTimeout(to);
|
|
154
|
+
done(null);
|
|
155
|
+
});
|
|
156
|
+
child.stdout?.on('data', (c) => {
|
|
157
|
+
out += c.toString('utf-8');
|
|
158
|
+
});
|
|
159
|
+
child.on('exit', (code) => {
|
|
160
|
+
clearTimeout(to);
|
|
161
|
+
done(code === 0 ? out : null);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
if (raw)
|
|
165
|
+
return applyGrades(criteria, raw);
|
|
166
|
+
}
|
|
167
|
+
return criteria; // no grade available → stay pending, never fake a green
|
|
168
|
+
}
|
|
169
|
+
function tryProvider(binary, prompt) {
|
|
170
|
+
return new Promise((resolve) => {
|
|
171
|
+
let stdout = '';
|
|
172
|
+
let settled = false;
|
|
173
|
+
const finish = (result) => {
|
|
174
|
+
if (settled)
|
|
175
|
+
return;
|
|
176
|
+
settled = true;
|
|
177
|
+
resolve(result);
|
|
178
|
+
};
|
|
179
|
+
const child = spawn(binary, ['--bare', '-p', prompt], {
|
|
180
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
181
|
+
});
|
|
182
|
+
const timeout = setTimeout(() => {
|
|
183
|
+
child.kill('SIGTERM');
|
|
184
|
+
if (process.env.CODEHERE_DEBUG === '1') {
|
|
185
|
+
console.error(`[criteria-agent] ${binary} timeout after ${CRITERIA_TIMEOUT_MS}ms`);
|
|
186
|
+
}
|
|
187
|
+
finish(null);
|
|
188
|
+
}, CRITERIA_TIMEOUT_MS);
|
|
189
|
+
child.on('error', () => {
|
|
190
|
+
clearTimeout(timeout);
|
|
191
|
+
finish(null);
|
|
192
|
+
});
|
|
193
|
+
child.stdout?.on('data', (chunk) => {
|
|
194
|
+
stdout += chunk.toString('utf-8');
|
|
195
|
+
});
|
|
196
|
+
child.on('exit', (code) => {
|
|
197
|
+
clearTimeout(timeout);
|
|
198
|
+
if (code !== 0) {
|
|
199
|
+
finish(null);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const criteria = sanitizeCriteria(stdout);
|
|
203
|
+
finish(criteria.length ? criteria : null);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* The contract. Returns [] (not null) when no provider responds so callers can
|
|
209
|
+
* store the field unconditionally; an empty contract simply means the request
|
|
210
|
+
* ran without one (never blocks the run).
|
|
211
|
+
*/
|
|
212
|
+
export async function generateAcceptanceCriteria(firstPrompt, cwd) {
|
|
213
|
+
if (isMockMode())
|
|
214
|
+
return mockCriteria(firstPrompt);
|
|
215
|
+
const prompt = PROMPT_TEMPLATE(firstPrompt, cwd);
|
|
216
|
+
for (const binary of selectProviderBinary()) {
|
|
217
|
+
const result = await tryProvider(binary, prompt);
|
|
218
|
+
if (result)
|
|
219
|
+
return result;
|
|
220
|
+
}
|
|
221
|
+
return [];
|
|
222
|
+
}
|
|
223
|
+
//# sourceMappingURL=generate-acceptance-criteria.js.map
|
|
@@ -21,7 +21,10 @@
|
|
|
21
21
|
* was available, and NEVER implies success — so a completed task always shows a
|
|
22
22
|
* deliverable, and it's honest about which kind it is (see `source`).
|
|
23
23
|
*/
|
|
24
|
-
|
|
24
|
+
// Generous cap — the old 1600 clipped reports mid-code-block (a code fence +
|
|
25
|
+
// prose easily exceeds it). High enough to never trim a real report's code,
|
|
26
|
+
// still bounded against a runaway agent dump.
|
|
27
|
+
const MAX_SUMMARY_CHARS = 20000;
|
|
25
28
|
/**
|
|
26
29
|
* UAT-003 — patterns indicating the agent's own text is actually a network /
|
|
27
30
|
* auth / quota failure, not a real summary. Such "summaries" must NOT be shown
|
|
@@ -31,6 +34,33 @@ const NETWORK_FAILURE_PATTERNS = /(?:all connection attempts failed|ECONNREFUSED
|
|
|
31
34
|
function isLikelyNetworkFailureSummary(text) {
|
|
32
35
|
return NETWORK_FAILURE_PATTERNS.test(text);
|
|
33
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* UAT (real-mode dogfood 2026-06-13) — some CLIs (aider) have no narration
|
|
39
|
+
* channel; their captured "say" text is setup/housekeeping chatter, not a
|
|
40
|
+
* summary of the work ("You can skip this check with --no-gitignore", the
|
|
41
|
+
* `.gitignore` housekeeping, the version banner). Shown as the report it reads
|
|
42
|
+
* as broken — and it buries the honest deterministic facts (files changed +
|
|
43
|
+
* trust score). Strip the housekeeping; if nothing real is left, fall through.
|
|
44
|
+
*/
|
|
45
|
+
const TOOL_NOISE_PATTERNS = [
|
|
46
|
+
/you can skip this check with --no-gitignore/gi,
|
|
47
|
+
/added\s+\.?aider\*?\s+to\s+\.?gitignore/gi,
|
|
48
|
+
/\baider v[\d.]+(?:\s*\([^)]*\))?/gi,
|
|
49
|
+
/\binitialized empty git repository[^\n]*/gi,
|
|
50
|
+
// aider session-banner lines (loop-1 sweep 2026-07-06: real runs whose whole
|
|
51
|
+
// "summary" was this chatter passed the length bar and rendered as the report)
|
|
52
|
+
/\bnote: in-chat filenames are always relative to[^\n.]*\.?/gi,
|
|
53
|
+
/\bcur working dir:[^\n]*/gi,
|
|
54
|
+
/\bgit working dir:[^\n]*/gi,
|
|
55
|
+
/\brepo-map can't include[^\n]*/gi,
|
|
56
|
+
/\bhas it been deleted from the file system but not from git\?/gi,
|
|
57
|
+
];
|
|
58
|
+
function stripToolNoise(text) {
|
|
59
|
+
let t = text;
|
|
60
|
+
for (const re of TOOL_NOISE_PATTERNS)
|
|
61
|
+
t = t.replace(re, ' ');
|
|
62
|
+
return t.replace(/\s+/g, ' ').trim();
|
|
63
|
+
}
|
|
34
64
|
/**
|
|
35
65
|
* ENGINE: the agent's own narration. kind:'say' steps are the words the agent
|
|
36
66
|
* already emitted (reasoning + wrap-up). Joined newest-context-last, they ARE
|
|
@@ -42,7 +72,19 @@ function extractAgentSummary(steps) {
|
|
|
42
72
|
.map((s) => s.label.trim());
|
|
43
73
|
if (!says.length)
|
|
44
74
|
return null;
|
|
45
|
-
|
|
75
|
+
// Inverted pyramid (loop-1 lesson 2026-07-06): agents narrate chronologically,
|
|
76
|
+
// so joined-in-order reports LEAD with opening narration ("Let me check…")
|
|
77
|
+
// while the finding sits at the end — and every summary-of-the-summary
|
|
78
|
+
// downstream (glance, cards, search snippets) inherits the wrong lead. The
|
|
79
|
+
// agent's LAST say is its wrap-up: put it first, keep the earlier narration
|
|
80
|
+
// below under an honest label. Content intact, order presented, not invented.
|
|
81
|
+
const ordered = says.length > 1 ? [says[says.length - 1], 'Earlier notes:', ...says.slice(0, -1)] : says;
|
|
82
|
+
const text = ordered.join('\n\n').slice(0, MAX_SUMMARY_CHARS);
|
|
83
|
+
// A "summary" that is only tool housekeeping (no real prose left after
|
|
84
|
+
// stripping it) is NOT a summary → let the deterministic fallback give the
|
|
85
|
+
// honest facts (files changed + trust score) instead of chatter.
|
|
86
|
+
if (stripToolNoise(text).length < 12)
|
|
87
|
+
return null;
|
|
46
88
|
// Too short to be a real summary (e.g. a one-word ack) → let the fallback give facts.
|
|
47
89
|
return text.length >= 12 ? text : null;
|
|
48
90
|
}
|
|
@@ -56,10 +98,13 @@ function buildDeterministicReport(input) {
|
|
|
56
98
|
const noGit = input.artifacts?.noGitTracking;
|
|
57
99
|
const agentName = input.agent === 'conductor' ? 'the agent codehere chose' : input.agent;
|
|
58
100
|
const parts = [
|
|
59
|
-
`No
|
|
101
|
+
`No written summary for this run. ${agentName} didn't leave one, and codehere doesn't fabricate one. Here are the recorded facts.`,
|
|
60
102
|
`The session ran ${input.steps.length} step${input.steps.length === 1 ? '' : 's'}` +
|
|
61
103
|
(toolSteps.length
|
|
62
|
-
? ` (${toolSteps
|
|
104
|
+
? ` (${toolSteps
|
|
105
|
+
.slice(0, 5)
|
|
106
|
+
.map((s) => s.label)
|
|
107
|
+
.join(', ')}${toolSteps.length > 5 ? ', …' : ''})`
|
|
63
108
|
: '') +
|
|
64
109
|
`.`,
|
|
65
110
|
];
|
|
@@ -78,19 +123,99 @@ function buildDeterministicReport(input) {
|
|
|
78
123
|
}
|
|
79
124
|
return { summary: parts.join('\n\n'), generatedAt: Date.now(), durationMs: 0, source: 'deterministic' };
|
|
80
125
|
}
|
|
126
|
+
/** Per-vendor recovery hints — the SPECIFIC auth/quota fix for EACH agent, so a
|
|
127
|
+
* failure report names the right login command + provider, not a claude-centric
|
|
128
|
+
* or generic guess. Commands/URLs mirror the readiness authHints (Phase 1):
|
|
129
|
+
* codex→`codex login`/platform.openai.com, gemini→AI-Studio free tier, aider→its
|
|
130
|
+
* LiteLLM model provider (free Gemini path), opencode→`opencode auth`. */
|
|
131
|
+
const VENDOR_FIX = {
|
|
132
|
+
'claude-code': {
|
|
133
|
+
label: 'Claude',
|
|
134
|
+
auth: 'codehere runs Claude through your *subscription* (your `claude` login) — it ignores `ANTHROPIC_API_KEY` on purpose. Open a terminal, run `claude`, complete `/login`, then rerun.',
|
|
135
|
+
quota: "If you authenticate with an API key, top up at console.anthropic.com; if you're on a Claude subscription, check your plan's usage limits — then rerun.",
|
|
136
|
+
},
|
|
137
|
+
codex: {
|
|
138
|
+
label: 'Codex',
|
|
139
|
+
auth: 'Run `codex login` with a ChatGPT Plus/Pro account (flat, no per-token charge), or set `OPENAI_API_KEY` — then rerun.',
|
|
140
|
+
quota: 'OpenAI quota exhausted — top up at platform.openai.com, or switch to a free agent (gemini / opencode).',
|
|
141
|
+
},
|
|
142
|
+
gemini: {
|
|
143
|
+
label: 'Gemini',
|
|
144
|
+
auth: 'Set `GEMINI_API_KEY` from the free tier (https://aistudio.google.com/apikey), then rerun.',
|
|
145
|
+
quota: "Gemini's free-tier limit was hit — wait for it to reset, raise your AI Studio limits, or switch agents.",
|
|
146
|
+
},
|
|
147
|
+
aider: {
|
|
148
|
+
label: 'Aider',
|
|
149
|
+
// Provider-neutral on purpose: codehere can't know WHICH provider aider was
|
|
150
|
+
// pointed at, so name the credential for each path rather than leading with one.
|
|
151
|
+
auth: 'Aider needs a model + a matching provider key. Set the key for whatever provider you point it at — a billed `ANTHROPIC_API_KEY` / `OPENAI_API_KEY` (with credit), or a free `GEMINI_API_KEY` run as `--model gemini/gemini-2.5-flash` — then rerun.',
|
|
152
|
+
quota: 'The provider key aider is using has no credit — top up that provider, or switch aider to a free one (e.g. a Gemini key with `--model gemini/…`, or a local Ollama model).',
|
|
153
|
+
},
|
|
154
|
+
opencode: {
|
|
155
|
+
label: 'OpenCode',
|
|
156
|
+
auth: 'Run `opencode auth` once to add a provider (a local Ollama model or a free endpoint = $0), then rerun.',
|
|
157
|
+
quota: 'The provider OpenCode is using hit its limit — run `opencode auth` to switch providers, or point at a local Ollama model.',
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
const GENERIC_FIX = {
|
|
161
|
+
label: 'The agent',
|
|
162
|
+
auth: "Sign in to the agent's provider — run `codehere health` to see which agent and how — then rerun.",
|
|
163
|
+
quota: "Top up or switch the agent's provider; `codehere health` shows the per-agent status. codehere is vendor-neutral.",
|
|
164
|
+
};
|
|
165
|
+
/** Read the captured error/output text and name the SPECIFIC failure + the fix
|
|
166
|
+
* FOR THIS AGENT, instead of a generic "network hiccup" or a claude-only guess.
|
|
167
|
+
* Uses input.errorText (codehere:error events) + steps' errorText/output. The
|
|
168
|
+
* detection patterns are vendor-neutral (each adapter forwards the provider's
|
|
169
|
+
* native auth/quota/rate words); the RECOVERY text is selected per input.agent.
|
|
170
|
+
* Returns null when the text doesn't clearly indicate a known failure — honest:
|
|
171
|
+
* we don't fabricate a specific cause we can't see. */
|
|
172
|
+
function diagnoseFailure(input) {
|
|
173
|
+
const blob = (input.errorText ? [input.errorText] : [])
|
|
174
|
+
.concat((input.steps || []).map((s) => `${s.errorText || ''} ${s.output || ''} ${s.kind === 'say' ? s.label : ''}`))
|
|
175
|
+
.join(' ')
|
|
176
|
+
.toLowerCase();
|
|
177
|
+
if (!blob.trim())
|
|
178
|
+
return null;
|
|
179
|
+
// Prefer the direct agent's fix; for a conductor task, fall back to the
|
|
180
|
+
// specialist it delegated to (so a conductor→codex failure names codex's fix,
|
|
181
|
+
// not the generic one); only then the vendor-neutral generic.
|
|
182
|
+
const fix = VENDOR_FIX[input.agent] ?? (input.delegatedTo ? VENDOR_FIX[input.delegatedTo] : undefined) ?? GENERIC_FIX;
|
|
183
|
+
if (/not logged in|please run.{0,6}\/login|\/login|no valid session|unauthorized|invalid api key|api key not valid|401\b/.test(blob)) {
|
|
184
|
+
return `**${fix.label} isn't logged in.** ${fix.auth}`;
|
|
185
|
+
}
|
|
186
|
+
if (/credit balance is too low|insufficient.{0,12}credit|out of credit|quota|billing/.test(blob)) {
|
|
187
|
+
return `**Out of credits / quota.** ${fix.quota}`;
|
|
188
|
+
}
|
|
189
|
+
if (/rate.?limit|\b429\b|too many requests/.test(blob)) {
|
|
190
|
+
return `**Rate limited by the provider.** Wait a minute and rerun, or switch agents — codehere is vendor-neutral.`;
|
|
191
|
+
}
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
81
194
|
function buildErrorRecoveryReport(input, durationMs) {
|
|
82
195
|
const agentName = input.agent === 'conductor' ? 'the agent codehere chose' : input.agent;
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
196
|
+
const specific = diagnoseFailure(input);
|
|
197
|
+
// When we can't classify the failure but the agent TOLD us why, show its real
|
|
198
|
+
// words — not a generic three-way guess. A product whose pitch is "show the
|
|
199
|
+
// truth" should surface "You've hit your usage limit — try again Jul 14", not
|
|
200
|
+
// "this usually means a network hiccup, an expired login, or a rate limit".
|
|
201
|
+
const realReason = (input.errorText || '').trim();
|
|
202
|
+
// NEVER claim "no files changed" without checking. A failed run that still
|
|
203
|
+
// edited files must say so — claiming otherwise is a lie the dogfood caught
|
|
204
|
+
// (opencode edited the file, codehere reported "no files were changed").
|
|
205
|
+
const files = (input.artifacts?.files || []).filter(Boolean);
|
|
206
|
+
const filesTail = files.length
|
|
207
|
+
? `Note: ${files.length} file${files.length === 1 ? '' : 's'} ${files.length === 1 ? 'was' : 'were'} already changed before it stopped (${files.slice(0, 5).join(', ')}${files.length > 5 ? ', …' : ''}) — review before retrying.`
|
|
208
|
+
: `codehere couldn't confirm any file changes from this attempt.`;
|
|
209
|
+
const summary = specific
|
|
210
|
+
? `The ${agentName} session couldn't complete.\n\n${specific}\n\n${filesTail}`
|
|
211
|
+
: realReason
|
|
212
|
+
? `The ${agentName} session couldn't complete. It reported:\n\n${realReason}\n\n${filesTail}`
|
|
213
|
+
: `The ${agentName} session ended without completing the task. ` +
|
|
214
|
+
`This usually means a network hiccup, an expired login, or a rate limit on the provider.\n\n` +
|
|
215
|
+
`What to try: check your internet, run \`codehere health\` to confirm reachability + auth, ` +
|
|
216
|
+
`or wait a minute and rerun the same prompt. If it keeps happening, try a different agent ` +
|
|
217
|
+
`from the dropdown — codehere is vendor-neutral on purpose.\n\n${filesTail}`;
|
|
218
|
+
return { summary, generatedAt: Date.now(), durationMs, source: 'error-recovery' };
|
|
94
219
|
}
|
|
95
220
|
/**
|
|
96
221
|
* Generate a Final Report. Reuses the agent's own summary; falls back to a
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* anti-hallucination rule as generate-final-report (ground every claim
|
|
17
17
|
* in the structured data).
|
|
18
18
|
*
|
|
19
|
-
* Provider strategy:
|
|
19
|
+
* Provider strategy: env override, else claude (the dead claude-free
|
|
20
20
|
* first, fall back to claude). Returns null if neither is installed.
|
|
21
21
|
*/
|
|
22
22
|
import { spawn } from 'child_process';
|
|
@@ -32,11 +32,13 @@ function buildPrompt(input) {
|
|
|
32
32
|
return renderTemplate(template, input);
|
|
33
33
|
}
|
|
34
34
|
function selectProviderBinary() {
|
|
35
|
-
//
|
|
35
|
+
// MED #27 (atomic audit) · the old chain led with 'claude-free' (the dead
|
|
36
|
+
// FCC/NIM proxy final-report.ts abandoned) — every narration burned its
|
|
37
|
+
// timeout against a down proxy first. Env override → claude, that's it.
|
|
36
38
|
const override = process.env.CODEHERE_NARRATOR_BINARY?.trim();
|
|
37
39
|
if (override)
|
|
38
40
|
return [override];
|
|
39
|
-
return ['claude
|
|
41
|
+
return ['claude'];
|
|
40
42
|
}
|
|
41
43
|
export async function generateNarration(input) {
|
|
42
44
|
if (isMockMode()) {
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* tab + future thread switcher UI.
|
|
8
8
|
*
|
|
9
9
|
* Companion to generate-final-report.ts:
|
|
10
|
-
* -
|
|
10
|
+
* - Provider: env override → claude → graceful skip (dead claude-free hop removed)
|
|
11
11
|
* - Same single-shot, no streaming, returns string
|
|
12
12
|
* - Different prompt (terse title generation, NOT 3-paragraph narrative)
|
|
13
13
|
* - Cheaper budget (~50 tokens per title vs 200 per Final Report paragraph)
|
|
@@ -43,10 +43,13 @@ Rules:
|
|
|
43
43
|
|
|
44
44
|
Title:`;
|
|
45
45
|
function selectProviderBinary() {
|
|
46
|
+
// MED #28 (atomic audit) · same dead 'claude-free' first hop as narration —
|
|
47
|
+
// removed so a title either comes from claude or degrades fast, not after
|
|
48
|
+
// a proxy timeout.
|
|
46
49
|
const override = process.env.CODEHERE_TITLE_BINARY;
|
|
47
50
|
if (override)
|
|
48
51
|
return [override];
|
|
49
|
-
return ['claude
|
|
52
|
+
return ['claude'];
|
|
50
53
|
}
|
|
51
54
|
function tryProvider(binary, prompt) {
|
|
52
55
|
return new Promise((resolve) => {
|
|
@@ -79,7 +79,7 @@ Write the paragraph now. Do not preface with "Update:" or anything similar.
|
|
|
79
79
|
Plain prose. No "I'm currently…" — narrate the AGENT, not yourself.
|
|
80
80
|
`;
|
|
81
81
|
function templateFile() {
|
|
82
|
-
return join(homedir(), '.codehere', 'narrator-prompt.md');
|
|
82
|
+
return join(process.env.CODEHERE_HOME || homedir(), '.codehere', 'narrator-prompt.md');
|
|
83
83
|
}
|
|
84
84
|
/** Read the user-customized template if present and valid. Falls back to
|
|
85
85
|
* baked-in default on missing file, parse failure, or missing required
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tier 2.1 · slice 2b — turn the eligibility decision into a concrete,
|
|
3
|
+
* loop-safe plan for the auto cross-vendor review spawn. Still pure: it decides
|
|
4
|
+
* WHAT to spawn (vendor, prompt, cwd, tag), not how — the live spawn + the
|
|
5
|
+
* real-spend verification are the next slice. Kept pure so the loop guard and
|
|
6
|
+
* the refute framing are unit-testable without a daemon or spend.
|
|
7
|
+
*
|
|
8
|
+
* THE LOOP GUARD is the load-bearing safety here: an auto-review is itself a
|
|
9
|
+
* task that completes and flows through the same completion handler. Without a
|
|
10
|
+
* guard it would trigger its own auto-review, recursing + burning spend forever.
|
|
11
|
+
* A reviewer session is tagged; planAutoReview returns null for tagged sessions.
|
|
12
|
+
*
|
|
13
|
+
* The reviewer runs in REFUTE mode and READ-ONLY: codehere proves the PROCESS
|
|
14
|
+
* (a different vendor independently reviewed + recorded findings), it never
|
|
15
|
+
* adjudicates code correctness (assess-cross-vendor honesty note). The prompt
|
|
16
|
+
* asks the reviewer to try to disprove "done", not to fix anything.
|
|
17
|
+
*/
|
|
18
|
+
import { shouldAutoReview } from './should-auto-review.js';
|
|
19
|
+
/** Tag stamped on an auto-review reviewer session so it never triggers its own
|
|
20
|
+
* auto-review (the loop guard) and so surfaces can label it. */
|
|
21
|
+
export const AUTO_REVIEW_TAG = 'auto-cross-vendor-review';
|
|
22
|
+
/** The refute-mode brief: read-only, adversarial, names the writer's files. */
|
|
23
|
+
function buildRefutePrompt(writerAgent, files) {
|
|
24
|
+
const fileList = files.length <= 10 ? files.join(', ') : `${files.slice(0, 10).join(', ')} (+${files.length - 10} more)`;
|
|
25
|
+
return (`A different agent (${writerAgent}) just changed these files in this repo: ${fileList}. ` +
|
|
26
|
+
`Independently review that work READ-ONLY — do not edit anything. ` +
|
|
27
|
+
`Try to disprove that the task is correctly done: look for bugs, missed edge cases, ` +
|
|
28
|
+
`or claims the diff doesn't actually support. Report what you find (or that it holds up). ` +
|
|
29
|
+
`You are the second vendor in a cross-vendor check; your job is to refute, not to fix.`);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Build the auto-review spawn plan, or null if it shouldn't / can't run.
|
|
33
|
+
* Returns null (with no side effects) for: the loop guard, or any failed
|
|
34
|
+
* eligibility gate. Pure.
|
|
35
|
+
*/
|
|
36
|
+
export function planAutoReview(facts, thresholds) {
|
|
37
|
+
// Loop guard FIRST — a reviewer's own completion must never spawn a review.
|
|
38
|
+
if (facts.isAutoReviewSession)
|
|
39
|
+
return null;
|
|
40
|
+
const inputs = {
|
|
41
|
+
writerAgent: facts.writerAgent,
|
|
42
|
+
installedAgents: facts.installedAgents,
|
|
43
|
+
filesChanged: facts.writerFiles.length,
|
|
44
|
+
budgetRemainingUsd: facts.budgetRemainingUsd,
|
|
45
|
+
};
|
|
46
|
+
const decision = shouldAutoReview(inputs, thresholds);
|
|
47
|
+
if (!decision.eligible || !decision.reviewerVendor)
|
|
48
|
+
return null;
|
|
49
|
+
return {
|
|
50
|
+
reviewerVendor: decision.reviewerVendor,
|
|
51
|
+
prompt: buildRefutePrompt(facts.writerAgent, facts.writerFiles),
|
|
52
|
+
cwd: facts.cwd,
|
|
53
|
+
tag: AUTO_REVIEW_TAG,
|
|
54
|
+
reason: decision.reason,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=plan-auto-review.js.map
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tier 1.2 (verdict-trust punch list, 2026-06-09) · reconcile the AUDIT against
|
|
3
|
+
* the real on-disk effect.
|
|
4
|
+
*
|
|
5
|
+
* The gap this closes: an audit entry records the agent's DECLARED tool call
|
|
6
|
+
* (PreToolUse intent for claude, a post-hoc mirror for the others) — not an
|
|
7
|
+
* independent observation that the write actually landed. An agent (or adapter)
|
|
8
|
+
* that emits a file-write tool_use which never really changed anything lands that
|
|
9
|
+
* fiction in the integrity-checked chain as a first-class entry. The chain then
|
|
10
|
+
* faithfully shows the CLAIM was never casually edited — it never checks the claim
|
|
11
|
+
* against reality.
|
|
12
|
+
*
|
|
13
|
+
* This reconciles the audit's file-write tool calls against the SAME git-diff
|
|
14
|
+
* ground truth verify-claim already uses for file claims. It does NOT mutate the
|
|
15
|
+
* append-only, hash-chained audit (that would break tamper-evidence) — it's a
|
|
16
|
+
* read-only layer that produces an honest verdict the surface can show:
|
|
17
|
+
* - consistent · writes recorded AND files changed on disk
|
|
18
|
+
* - unconfirmed · writes recorded but codehere couldn't tie them to a tracked
|
|
19
|
+
* git change. Honest, NOT an accusation: the writes may have
|
|
20
|
+
* targeted gitignored / scratch / .codehere paths (which the
|
|
21
|
+
* git-diff ground truth excludes), been reverted within the
|
|
22
|
+
* run, or — the case worth a look — never really happened.
|
|
23
|
+
* "couldn't confirm", never "it's fake" (refuter 2026-06-09:
|
|
24
|
+
* a false fabrication accusation on honest work is the worst
|
|
25
|
+
* error class on a proof surface).
|
|
26
|
+
* - no-writes · nothing claimed, nothing to reconcile
|
|
27
|
+
* - unobservable · not a git repo, so codehere can't confirm
|
|
28
|
+
*
|
|
29
|
+
* Count-based by design: toolInput is truncated/heterogeneous across adapters, so
|
|
30
|
+
* per-file path matching is unreliable; the file-write COUNT vs the git-change
|
|
31
|
+
* count is robust. Bash/shell tools can also write files but codehere can't tell,
|
|
32
|
+
* so they're deliberately NOT counted as declared writes (conservative — never
|
|
33
|
+
* fabricates a "declared-not-observed" the user can't act on).
|
|
34
|
+
*/
|
|
35
|
+
/** Tool names that explicitly mutate a file on disk, across every adapter.
|
|
36
|
+
* Derived from the real audit's toolName distribution (Write/Edit/write_file/
|
|
37
|
+
* edit_file observed) plus the standard variants. Shell tools are excluded — a
|
|
38
|
+
* bash `echo > f` can't be distinguished from a read, so counting it would
|
|
39
|
+
* manufacture false "declared-not-observed" verdicts. */
|
|
40
|
+
// Matched case-INSENSITIVELY (adapters emit varied casings: claude 'Edit',
|
|
41
|
+
// codex 'Delete', gemini raw tool_name like 'write_file'/'replace'). A DELETE is
|
|
42
|
+
// a real file mutation that shows in git — omitting it made a codex delete-only
|
|
43
|
+
// run falsely read "no writes recorded" (self-audit 2026-06-09). Keep this broad;
|
|
44
|
+
// over-counting a non-write is far less harmful than a false "nothing happened".
|
|
45
|
+
const WRITE_TOOLS = new Set(
|
|
46
|
+
// SPECIFIC file-mutation tool names only. Bare generic verbs ('replace',
|
|
47
|
+
// 'patch') are deliberately EXCLUDED — a non-file tool named 'replace' (an
|
|
48
|
+
// in-memory string replace) would false-count and produce a false 'consistent'
|
|
49
|
+
// (worse than under-counting, which is an honest 'unconfirmed'). A real write
|
|
50
|
+
// the set misses → 'unconfirmed' (honest), never a fabricated-write laundered
|
|
51
|
+
// into green (self-audit round 2, 2026-06-09).
|
|
52
|
+
[
|
|
53
|
+
'Write',
|
|
54
|
+
'Edit',
|
|
55
|
+
'MultiEdit',
|
|
56
|
+
'NotebookEdit',
|
|
57
|
+
'Delete',
|
|
58
|
+
'write_file',
|
|
59
|
+
'edit_file',
|
|
60
|
+
'create_file',
|
|
61
|
+
'delete_file',
|
|
62
|
+
'str_replace',
|
|
63
|
+
'str_replace_editor',
|
|
64
|
+
'apply_patch',
|
|
65
|
+
].map((t) => t.toLowerCase()));
|
|
66
|
+
export function reconcileAudit(entries, artifacts) {
|
|
67
|
+
const writeToolCalls = entries.filter((e) => WRITE_TOOLS.has((e.toolName || '').toLowerCase())).length;
|
|
68
|
+
const filesObserved = artifacts?.files?.length ?? 0;
|
|
69
|
+
if (artifacts?.noGitTracking) {
|
|
70
|
+
return {
|
|
71
|
+
writeToolCalls,
|
|
72
|
+
filesObserved: 0,
|
|
73
|
+
verdict: 'unobservable',
|
|
74
|
+
note: `${writeToolCalls} file-write tool call${writeToolCalls === 1 ? '' : 's'} recorded, but this folder isn't a git repo — codehere can't confirm them against disk.`,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
if (writeToolCalls === 0) {
|
|
78
|
+
return {
|
|
79
|
+
writeToolCalls,
|
|
80
|
+
filesObserved,
|
|
81
|
+
verdict: 'no-writes',
|
|
82
|
+
note: 'No file-write tool calls recorded — nothing to reconcile against disk.',
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
if (filesObserved === 0) {
|
|
86
|
+
return {
|
|
87
|
+
writeToolCalls,
|
|
88
|
+
filesObserved,
|
|
89
|
+
verdict: 'unconfirmed',
|
|
90
|
+
note: `${writeToolCalls} file-write tool call${writeToolCalls === 1 ? '' : 's'} recorded, but codehere couldn't confirm them against tracked git changes — they may have targeted gitignored/scratch paths or been reverted. Worth a look, not proof of a problem.`,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
writeToolCalls,
|
|
95
|
+
filesObserved,
|
|
96
|
+
verdict: 'consistent',
|
|
97
|
+
note: `${writeToolCalls} file-write tool call${writeToolCalls === 1 ? '' : 's'} recorded · ${filesObserved} file${filesObserved === 1 ? '' : 's'} confirmed changed on disk.`,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=reconcile-audit.js.map
|