profclaw 2.3.0 → 2.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 (60) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +5 -2
  3. package/config/pricing.yml +26 -2
  4. package/dist/agents/command-sandbox.d.ts +93 -0
  5. package/dist/agents/command-sandbox.js +235 -0
  6. package/dist/agents/executor-runner.d.ts +135 -0
  7. package/dist/agents/executor-runner.js +303 -0
  8. package/dist/agents/executor.d.ts +2 -0
  9. package/dist/agents/executor.js +35 -2
  10. package/dist/agents/output-truncator.d.ts +59 -0
  11. package/dist/agents/output-truncator.js +128 -0
  12. package/dist/agents/receipt/receipt-cli.d.ts +43 -0
  13. package/dist/agents/receipt/receipt-cli.js +82 -0
  14. package/dist/agents/receipt/receipt-types.d.ts +136 -0
  15. package/dist/agents/receipt/receipt-types.js +19 -0
  16. package/dist/agents/receipt/receipt-verify.d.ts +19 -0
  17. package/dist/agents/receipt/receipt-verify.js +176 -0
  18. package/dist/agents/receipt/receipt-view.d.ts +29 -0
  19. package/dist/agents/receipt/receipt-view.js +227 -0
  20. package/dist/agents/receipt/receipt-writer.d.ts +47 -0
  21. package/dist/agents/receipt/receipt-writer.js +186 -0
  22. package/dist/agents/result-store.d.ts +6 -0
  23. package/dist/agents/result-store.js +43 -2
  24. package/dist/agents/session-diff.d.ts +6 -0
  25. package/dist/agents/session-diff.js +10 -19
  26. package/dist/agents/tool-selector.d.ts +123 -0
  27. package/dist/agents/tool-selector.js +321 -0
  28. package/dist/agents/verified-run-git.d.ts +49 -0
  29. package/dist/agents/verified-run-git.js +82 -0
  30. package/dist/agents/verified-run.d.ts +149 -0
  31. package/dist/agents/verified-run.js +402 -0
  32. package/dist/agents/verifier.d.ts +64 -0
  33. package/dist/agents/verifier.js +186 -0
  34. package/dist/agents/worktree-tools.d.ts +47 -0
  35. package/dist/agents/worktree-tools.js +246 -0
  36. package/dist/chat/agentic-executor.js +14 -6
  37. package/dist/chat/execution/sandbox.d.ts +8 -0
  38. package/dist/chat/execution/sandbox.js +25 -1
  39. package/dist/chat/execution/tools/tts-speak.d.ts +2 -2
  40. package/dist/chat/meta-tool-handlers.d.ts +18 -0
  41. package/dist/chat/meta-tool-handlers.js +51 -0
  42. package/dist/chat/system-prompts.js +6 -1
  43. package/dist/chat/tool-handler.d.ts +2 -0
  44. package/dist/chat/tool-handler.js +47 -3
  45. package/dist/cli/commands/receipt.d.ts +7 -0
  46. package/dist/cli/commands/receipt.js +90 -0
  47. package/dist/cli/commands/run.d.ts +28 -0
  48. package/dist/cli/commands/run.js +204 -0
  49. package/dist/cli/index.js +4 -0
  50. package/dist/costs/token-tracker.d.ts +10 -1
  51. package/dist/costs/token-tracker.js +37 -5
  52. package/dist/providers/ai-sdk.js +33 -37
  53. package/dist/providers/core/models.js +59 -17
  54. package/dist/providers/core/types.d.ts +4 -0
  55. package/dist/providers/prompt-cache.d.ts +68 -0
  56. package/dist/providers/prompt-cache.js +165 -0
  57. package/dist/providers/smart-router.js +11 -3
  58. package/dist/routes/chat.js +22 -5
  59. package/dist/server.js +3 -0
  60. package/package.json +1 -1
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Verified Run
3
+ *
4
+ * "Done means verified, not claimed." Runs an agent toward a goal inside an
5
+ * isolated worktree and only reports success when an objective verifier
6
+ * command (tests, typecheck, lint, build, custom) exits 0.
7
+ *
8
+ * Loop: attempt -> verify -> (pass: stop) | (fail: feed trimmed output back,
9
+ * roll back to the last good checkpoint if the attempt made things worse) ->
10
+ * retry. Stops on pass, max attempts, token or dollar budget, or the circuit
11
+ * breaker. The agent sits behind AgentRunner so the loop is testable without
12
+ * an LLM. Nothing here pushes or opens a PR.
13
+ */
14
+ import { CheckpointManager } from './checkpoint-manager.js';
15
+ import type { Verifier, VerifierResult } from './verifier.js';
16
+ import type { ReceiptSink } from './receipt/receipt-types.js';
17
+ import type { RunWorkspace } from './verified-run-git.js';
18
+ export interface AgentAttemptInput {
19
+ goal: string;
20
+ /** 1-based attempt number */
21
+ attempt: number;
22
+ /** Trimmed verifier failure from the previous attempt, when there is one */
23
+ feedback?: string;
24
+ /** Directory the agent must work in (the isolated worktree) */
25
+ workdir: string;
26
+ verifyCommand: string;
27
+ signal?: AbortSignal;
28
+ }
29
+ export interface AgentAttemptResult {
30
+ summary: string;
31
+ tokensUsed: number;
32
+ costUsd: number;
33
+ /** Model the attempt ran on, when the runner chose one */
34
+ model?: string;
35
+ /** Fraction of prompt tokens served from cache (0 to 1), when known */
36
+ cacheHitRate?: number;
37
+ /** Token breakdown, when the runner knows it (recorded in the run receipt) */
38
+ usage?: {
39
+ inputTokens: number;
40
+ outputTokens: number;
41
+ cacheReadTokens: number;
42
+ cacheWriteTokens: number;
43
+ };
44
+ }
45
+ /** The only seam to an LLM. Real integrations and test fakes implement this. */
46
+ export interface AgentRunner {
47
+ runAttempt(input: AgentAttemptInput): Promise<AgentAttemptResult>;
48
+ }
49
+ export interface VerifiedRunLimits {
50
+ maxAttempts: number;
51
+ /** 0 disables the guard */
52
+ maxTokens: number;
53
+ /** 0 disables the guard */
54
+ maxCostUsd: number;
55
+ breakerThreshold: number;
56
+ breakerWindowMs: number;
57
+ verifyTimeoutMs: number;
58
+ maxOutputChars: number;
59
+ /** 1 runs the verifier once before attempt 1 to measure the starting state, 0 skips it */
60
+ baseline: number;
61
+ }
62
+ export declare const DEFAULT_LIMITS: VerifiedRunLimits;
63
+ /** Precedence: explicit flag > PROFCLAW_RUN_* env var > default. */
64
+ export declare function resolveLimits(flags: Partial<VerifiedRunLimits>, env?: NodeJS.ProcessEnv): VerifiedRunLimits;
65
+ export type StopReason = 'verified' | 'max_attempts' | 'token_budget' | 'cost_budget' | 'circuit_open' | 'aborted' | 'already_passing';
66
+ export type AttemptOutcome = 'verified' | 'improved' | 'unchanged' | 'worse' | 'agent_error';
67
+ export interface AttemptRecord {
68
+ attempt: number;
69
+ outcome: AttemptOutcome;
70
+ agentSummary: string;
71
+ agentError?: string;
72
+ verifier?: VerifierResult;
73
+ /** Failure signal used for better/worse comparison (lower is better) */
74
+ failureScore?: number;
75
+ rolledBack: boolean;
76
+ tokensUsed: number;
77
+ costUsd: number;
78
+ model?: string;
79
+ cacheHitRate?: number;
80
+ }
81
+ export interface VerifiedRunResult {
82
+ runId: string;
83
+ goal: string;
84
+ verifyCommand: string;
85
+ verified: boolean;
86
+ stopReason: StopReason;
87
+ attempts: AttemptRecord[];
88
+ totalTokens: number;
89
+ totalCostUsd: number;
90
+ branch: string;
91
+ workspacePath: string;
92
+ changedFiles: Array<{
93
+ relPath: string;
94
+ status: string;
95
+ }>;
96
+ diff: string;
97
+ reportPath?: string;
98
+ }
99
+ export type RunEvent = {
100
+ type: 'start';
101
+ runId: string;
102
+ branch: string;
103
+ workspacePath: string;
104
+ } | {
105
+ type: 'attempt_start';
106
+ attempt: number;
107
+ } | {
108
+ type: 'attempt_end';
109
+ record: AttemptRecord;
110
+ } | {
111
+ type: 'stop';
112
+ reason: StopReason;
113
+ };
114
+ export interface VerifiedRunOptions {
115
+ goal: string;
116
+ verifyCommand: string;
117
+ agent: AgentRunner;
118
+ /** Builds the verifier for a given workspace directory */
119
+ createVerifier: (cwd: string) => Verifier;
120
+ /** Repo root; the worktree and the report live under .profclaw/ inside it */
121
+ projectRoot: string;
122
+ limits?: Partial<VerifiedRunLimits>;
123
+ env?: NodeJS.ProcessEnv;
124
+ runId?: string;
125
+ branchName?: string;
126
+ /** Injected in tests; defaults to a git worktree */
127
+ workspace?: RunWorkspace;
128
+ checkpoints?: Pick<CheckpointManager, 'save'>;
129
+ /** Directory for evidence.md; defaults to <projectRoot>/.profclaw/runs/<runId> */
130
+ reportDir?: string;
131
+ /** Remove the worktree at the end (the branch is always kept) */
132
+ removeWorktree?: boolean;
133
+ signal?: AbortSignal;
134
+ onEvent?: (event: RunEvent) => void;
135
+ /** Lower is better. Default counts failure-looking lines in verifier output. */
136
+ scoreFailure?: (result: VerifierResult) => number;
137
+ /** Records a tamper-evident receipt of the run when set (see docs/specs/run-receipt-v0.md) */
138
+ receipt?: ReceiptSink;
139
+ /** Tool name and version written to the receipt's run_start */
140
+ toolInfo?: {
141
+ name: string;
142
+ version: string;
143
+ };
144
+ }
145
+ /** Number of failure-looking output lines, minimum 1 for any failed run. */
146
+ export declare function defaultFailureScore(result: VerifierResult): number;
147
+ export declare function runVerifiedGoal(options: VerifiedRunOptions): Promise<VerifiedRunResult>;
148
+ export declare function buildEvidenceReport(result: VerifiedRunResult, limits: VerifiedRunLimits): string;
149
+ //# sourceMappingURL=verified-run.d.ts.map
@@ -0,0 +1,402 @@
1
+ /**
2
+ * Verified Run
3
+ *
4
+ * "Done means verified, not claimed." Runs an agent toward a goal inside an
5
+ * isolated worktree and only reports success when an objective verifier
6
+ * command (tests, typecheck, lint, build, custom) exits 0.
7
+ *
8
+ * Loop: attempt -> verify -> (pass: stop) | (fail: feed trimmed output back,
9
+ * roll back to the last good checkpoint if the attempt made things worse) ->
10
+ * retry. Stops on pass, max attempts, token or dollar budget, or the circuit
11
+ * breaker. The agent sits behind AgentRunner so the loop is testable without
12
+ * an LLM. Nothing here pushes or opens a PR.
13
+ */
14
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
15
+ import { join } from 'node:path';
16
+ import { randomUUID } from 'node:crypto';
17
+ import { logger } from '../utils/logger.js';
18
+ import { ToolCircuitBreaker } from './circuit-breaker.js';
19
+ import { CheckpointManager } from './checkpoint-manager.js';
20
+ import { SessionDiffTracker } from './session-diff.js';
21
+ import { formatFailureFeedback } from './verifier.js';
22
+ import { GitRunWorkspace } from './verified-run-git.js';
23
+ import { sha256Hex } from './receipt/receipt-writer.js';
24
+ export const DEFAULT_LIMITS = {
25
+ maxAttempts: 5,
26
+ maxTokens: 0,
27
+ maxCostUsd: 0,
28
+ breakerThreshold: 3,
29
+ breakerWindowMs: 3_600_000,
30
+ verifyTimeoutMs: 600_000,
31
+ maxOutputChars: 4_000,
32
+ baseline: 1,
33
+ };
34
+ function envNumber(env, key) {
35
+ const raw = env[key];
36
+ if (raw === undefined || raw.trim() === '')
37
+ return undefined;
38
+ const n = Number(raw);
39
+ return Number.isFinite(n) && n >= 0 ? n : undefined;
40
+ }
41
+ /** Precedence: explicit flag > PROFCLAW_RUN_* env var > default. */
42
+ export function resolveLimits(flags, env = process.env) {
43
+ const pick = (key, envKey) => flags[key] ?? envNumber(env, envKey) ?? DEFAULT_LIMITS[key];
44
+ const limits = {
45
+ maxAttempts: pick('maxAttempts', 'PROFCLAW_RUN_MAX_ATTEMPTS'),
46
+ maxTokens: pick('maxTokens', 'PROFCLAW_RUN_MAX_TOKENS'),
47
+ maxCostUsd: pick('maxCostUsd', 'PROFCLAW_RUN_MAX_COST_USD'),
48
+ breakerThreshold: pick('breakerThreshold', 'PROFCLAW_RUN_BREAKER_THRESHOLD'),
49
+ breakerWindowMs: pick('breakerWindowMs', 'PROFCLAW_RUN_BREAKER_WINDOW_MS'),
50
+ verifyTimeoutMs: pick('verifyTimeoutMs', 'PROFCLAW_RUN_VERIFY_TIMEOUT_MS'),
51
+ maxOutputChars: pick('maxOutputChars', 'PROFCLAW_RUN_MAX_OUTPUT_CHARS'),
52
+ baseline: pick('baseline', 'PROFCLAW_RUN_BASELINE'),
53
+ };
54
+ limits.maxAttempts = Math.max(1, Math.floor(limits.maxAttempts));
55
+ limits.breakerThreshold = Math.max(1, Math.floor(limits.breakerThreshold));
56
+ return limits;
57
+ }
58
+ // Failure scoring
59
+ const FAILURE_LINE = /\b(error|errors|fail|failed|failing|failure|assertionerror)\b|✗|✘/i;
60
+ /** Number of failure-looking output lines, minimum 1 for any failed run. */
61
+ export function defaultFailureScore(result) {
62
+ if (result.passed)
63
+ return 0;
64
+ const lines = result.output.split('\n').filter((l) => FAILURE_LINE.test(l)).length;
65
+ return Math.max(1, lines);
66
+ }
67
+ // Run loop
68
+ export async function runVerifiedGoal(options) {
69
+ const limits = resolveLimits(options.limits ?? {}, options.env ?? process.env);
70
+ const runId = options.runId ?? randomUUID().slice(0, 8);
71
+ const score = options.scoreFailure ?? defaultFailureScore;
72
+ const env = options.env ?? process.env;
73
+ const emit = options.onEvent ?? (() => undefined);
74
+ const workspace = options.workspace ?? new GitRunWorkspace(options.projectRoot, runId, options.branchName);
75
+ const checkpoints = options.checkpoints ?? new CheckpointManager(options.projectRoot);
76
+ const breaker = new ToolCircuitBreaker(limits.breakerThreshold, limits.breakerWindowMs);
77
+ const breakerKey = 'verified-run';
78
+ const rcpt = (type, data) => recordReceipt(options.receipt, type, data);
79
+ const baseRef = await workspace.prepare();
80
+ rcpt('run_start', {
81
+ goal: options.goal,
82
+ verifyCommand: options.verifyCommand,
83
+ repoHead: baseRef,
84
+ branch: workspace.branch,
85
+ tool: options.toolInfo ?? { name: 'profclaw', version: 'unknown' },
86
+ });
87
+ emit({ type: 'start', runId, branch: workspace.branch, workspacePath: workspace.path });
88
+ const verifier = options.createVerifier(workspace.path);
89
+ const attempts = [];
90
+ let goodRef = baseRef;
91
+ let bestScore = Number.POSITIVE_INFINITY;
92
+ let feedback;
93
+ let totalTokens = 0;
94
+ let totalCost = 0;
95
+ let stopReason = 'max_attempts';
96
+ const budgetStop = () => {
97
+ if (limits.maxTokens > 0 && totalTokens >= limits.maxTokens)
98
+ return 'token_budget';
99
+ if (limits.maxCostUsd > 0 && totalCost >= limits.maxCostUsd)
100
+ return 'cost_budget';
101
+ return null;
102
+ };
103
+ const breakerOpen = () => breaker.getStatus().get(breakerKey)?.state === 'open';
104
+ // Measure the starting state so attempt 1 can be judged (and rolled back) like any other,
105
+ // and so the agent sees the real failure output from the first attempt.
106
+ let alreadyPassing = false;
107
+ if (limits.baseline > 0) {
108
+ const baseline = await verifier.verify();
109
+ rcpt('verifier_result', verifierData(0, options.verifyCommand, baseline, score(baseline)));
110
+ if (baseline.passed) {
111
+ alreadyPassing = true;
112
+ stopReason = 'already_passing';
113
+ }
114
+ else {
115
+ bestScore = score(baseline);
116
+ feedback = formatFailureFeedback(baseline, options.verifyCommand);
117
+ }
118
+ }
119
+ for (let attempt = 1; attempt <= limits.maxAttempts && !alreadyPassing; attempt++) {
120
+ if (options.signal?.aborted) {
121
+ stopReason = 'aborted';
122
+ break;
123
+ }
124
+ const preStop = budgetStop();
125
+ if (preStop) {
126
+ stopReason = preStop;
127
+ break;
128
+ }
129
+ emit({ type: 'attempt_start', attempt });
130
+ rcpt('attempt_start', { attempt });
131
+ const record = {
132
+ attempt,
133
+ outcome: 'agent_error',
134
+ agentSummary: '',
135
+ rolledBack: false,
136
+ tokensUsed: 0,
137
+ costUsd: 0,
138
+ };
139
+ try {
140
+ const result = await options.agent.runAttempt({
141
+ goal: options.goal,
142
+ attempt,
143
+ feedback,
144
+ workdir: workspace.path,
145
+ verifyCommand: options.verifyCommand,
146
+ signal: options.signal,
147
+ });
148
+ record.agentSummary = result.summary;
149
+ record.tokensUsed = result.tokensUsed;
150
+ record.costUsd = result.costUsd;
151
+ record.model = result.model;
152
+ record.cacheHitRate = result.cacheHitRate;
153
+ totalTokens += result.tokensUsed;
154
+ totalCost += result.costUsd;
155
+ rcpt('usage', {
156
+ attempt,
157
+ inputTokens: result.usage?.inputTokens ?? 0,
158
+ outputTokens: result.usage?.outputTokens ?? 0,
159
+ cacheReadTokens: result.usage?.cacheReadTokens ?? 0,
160
+ cacheWriteTokens: result.usage?.cacheWriteTokens ?? 0,
161
+ totalTokens: result.tokensUsed,
162
+ costUsd: result.costUsd,
163
+ ...(result.model ? { model: result.model } : {}),
164
+ });
165
+ }
166
+ catch (error) {
167
+ record.agentError = error instanceof Error ? error.message : String(error);
168
+ logger.warn('[VerifiedRun] Agent attempt threw', { attempt, error: record.agentError });
169
+ await workspace.restore(goodRef);
170
+ record.rolledBack = true;
171
+ rcpt('rollback', { attempt, reason: `agent error: ${record.agentError}` });
172
+ breaker.recordFailure(breakerKey);
173
+ feedback = `The previous attempt crashed: ${record.agentError}. The workspace was restored to the last good state.`;
174
+ attempts.push(record);
175
+ rcpt('attempt_end', { attempt, outcome: record.outcome, rolledBack: record.rolledBack });
176
+ emit({ type: 'attempt_end', record });
177
+ const stop = budgetStop() ?? (breakerOpen() ? 'circuit_open' : null);
178
+ if (stop) {
179
+ stopReason = stop;
180
+ break;
181
+ }
182
+ continue;
183
+ }
184
+ const verdict = await verifier.verify();
185
+ record.verifier = verdict;
186
+ record.failureScore = score(verdict);
187
+ rcpt('verifier_result', verifierData(attempt, options.verifyCommand, verdict, record.failureScore));
188
+ if (verdict.passed) {
189
+ record.outcome = 'verified';
190
+ await workspace.checkpoint(`attempt ${attempt} verified`);
191
+ attempts.push(record);
192
+ rcpt('attempt_end', { attempt, outcome: record.outcome, rolledBack: false });
193
+ emit({ type: 'attempt_end', record });
194
+ stopReason = 'verified';
195
+ break;
196
+ }
197
+ if (record.failureScore > bestScore) {
198
+ record.outcome = 'worse';
199
+ await workspace.restore(goodRef);
200
+ record.rolledBack = true;
201
+ rcpt('rollback', {
202
+ attempt,
203
+ reason: `failure score ${record.failureScore} is worse than the best so far (${bestScore})`,
204
+ });
205
+ breaker.recordFailure(breakerKey);
206
+ }
207
+ else {
208
+ const improved = record.failureScore < bestScore;
209
+ record.outcome = improved ? 'improved' : 'unchanged';
210
+ goodRef = await workspace.checkpoint(`attempt ${attempt} (${record.outcome})`);
211
+ bestScore = record.failureScore;
212
+ if (improved)
213
+ breaker.recordSuccess(breakerKey);
214
+ else
215
+ breaker.recordFailure(breakerKey);
216
+ await saveExecutorCheckpoint(checkpoints, runId, options.goal, attempt, limits, totalTokens, totalCost, verdict);
217
+ }
218
+ feedback = formatFailureFeedback(verdict, options.verifyCommand);
219
+ attempts.push(record);
220
+ rcpt('attempt_end', { attempt, outcome: record.outcome, rolledBack: record.rolledBack });
221
+ emit({ type: 'attempt_end', record });
222
+ const stop = budgetStop() ?? (breakerOpen() ? 'circuit_open' : null);
223
+ if (stop) {
224
+ stopReason = stop;
225
+ break;
226
+ }
227
+ }
228
+ emit({ type: 'stop', reason: stopReason });
229
+ const changes = await workspace.changedFiles();
230
+ const diff = await buildDiff(changes);
231
+ if (options.receipt) {
232
+ for (const change of changes) {
233
+ rcpt('file_change', await fileChangeData(change, env));
234
+ }
235
+ rcpt('run_end', {
236
+ stopReason,
237
+ verified: stopReason === 'verified',
238
+ totals: { attempts: attempts.length, totalTokens, costUsd: totalCost, filesChanged: changes.length },
239
+ });
240
+ }
241
+ const result = {
242
+ runId,
243
+ goal: options.goal,
244
+ verifyCommand: options.verifyCommand,
245
+ verified: stopReason === 'verified',
246
+ stopReason,
247
+ attempts,
248
+ totalTokens,
249
+ totalCostUsd: totalCost,
250
+ branch: workspace.branch,
251
+ workspacePath: workspace.path,
252
+ changedFiles: changes.map((c) => ({ relPath: c.relPath, status: c.status })),
253
+ diff,
254
+ };
255
+ const reportDir = options.reportDir ?? join(options.projectRoot, '.profclaw', 'runs', runId);
256
+ await mkdir(reportDir, { recursive: true });
257
+ result.reportPath = join(reportDir, 'evidence.md');
258
+ await writeFile(result.reportPath, buildEvidenceReport(result, limits), 'utf-8');
259
+ if (options.removeWorktree)
260
+ await workspace.dispose();
261
+ return result;
262
+ }
263
+ // Receipt helpers
264
+ /** Receipt writing is best effort: a failure is logged and never fails the run. */
265
+ function recordReceipt(sink, type, data) {
266
+ if (!sink)
267
+ return;
268
+ try {
269
+ sink.append(type, data);
270
+ }
271
+ catch (error) {
272
+ logger.warn('[VerifiedRun] Failed to write receipt event', {
273
+ type,
274
+ error: error instanceof Error ? error.message : String(error),
275
+ });
276
+ }
277
+ }
278
+ function verifierData(attempt, command, result, failureScore) {
279
+ return {
280
+ attempt,
281
+ command,
282
+ exitCode: result.exitCode,
283
+ passed: result.passed,
284
+ output: result.output,
285
+ failureScore,
286
+ durationMs: result.durationMs,
287
+ timedOut: result.timedOut,
288
+ };
289
+ }
290
+ /** Largest per-file diff embedded in a receipt (env PROFCLAW_RECEIPT_MAX_DIFF_CHARS). */
291
+ function maxDiffChars(env) {
292
+ const n = Number(env['PROFCLAW_RECEIPT_MAX_DIFF_CHARS']);
293
+ return Number.isFinite(n) && n > 0 ? Math.floor(n) : 200_000;
294
+ }
295
+ async function fileChangeData(change, env) {
296
+ let after = null;
297
+ try {
298
+ after = sha256Hex(await readFile(change.path));
299
+ }
300
+ catch {
301
+ after = null;
302
+ }
303
+ const diff = await buildDiff([change]);
304
+ const cap = maxDiffChars(env);
305
+ return {
306
+ path: change.relPath,
307
+ status: change.status,
308
+ beforeSha256: change.original === null ? null : sha256Hex(change.original),
309
+ afterSha256: after,
310
+ diffSha256: sha256Hex(diff),
311
+ ...(diff.length <= cap ? { diff } : { diffTruncated: true }),
312
+ };
313
+ }
314
+ async function saveExecutorCheckpoint(store, runId, goal, attempt, limits, tokens, cost, verdict) {
315
+ try {
316
+ const now = Date.now();
317
+ await store.save({
318
+ sessionId: `verified-run-${runId}`,
319
+ taskDescription: goal,
320
+ currentStep: attempt,
321
+ totalSteps: limits.maxAttempts,
322
+ messages: [],
323
+ tokensUsed: tokens,
324
+ estimatedCost: cost,
325
+ toolCallHistory: [],
326
+ remainingWork: verdict.output.slice(0, 500),
327
+ createdAt: now,
328
+ updatedAt: now,
329
+ });
330
+ }
331
+ catch (error) {
332
+ // Persistence is best effort: the git checkpoint is the source of truth.
333
+ logger.warn('[VerifiedRun] Failed to persist checkpoint metadata', {
334
+ error: error instanceof Error ? error.message : String(error),
335
+ });
336
+ }
337
+ }
338
+ async function buildDiff(changes) {
339
+ const tracker = new SessionDiffTracker();
340
+ for (const change of changes) {
341
+ if (change.status === 'created' || change.original === null)
342
+ tracker.recordCreated(change.path);
343
+ else
344
+ tracker.recordOriginal(change.path, change.original);
345
+ }
346
+ const raw = await tracker.generateDiff();
347
+ // Show workspace-relative paths in the diff headers.
348
+ let out = raw;
349
+ for (const change of changes)
350
+ out = out.split(change.path).join(change.relPath);
351
+ return out;
352
+ }
353
+ // Evidence report
354
+ export function buildEvidenceReport(result, limits) {
355
+ const lines = [];
356
+ const status = result.verified
357
+ ? 'VERIFIED'
358
+ : result.stopReason === 'already_passing'
359
+ ? 'NOT RUN (verifier already passes)'
360
+ : 'NOT VERIFIED';
361
+ lines.push(`# Evidence report: ${status}`, '');
362
+ lines.push(`- Run: \`${result.runId}\``);
363
+ lines.push(`- Goal: ${result.goal}`);
364
+ lines.push(`- Verifier: \`${result.verifyCommand}\``);
365
+ lines.push(`- Stop reason: ${result.stopReason}`);
366
+ lines.push(`- Branch: \`${result.branch}\` (local only, not pushed)`);
367
+ lines.push(`- Worktree: \`${result.workspacePath}\``, '');
368
+ lines.push('## Cost', '');
369
+ lines.push(`- Attempts: ${result.attempts.length} of ${limits.maxAttempts}`);
370
+ lines.push(`- Tokens: ${result.totalTokens}${limits.maxTokens > 0 ? ` (limit ${limits.maxTokens})` : ''}`);
371
+ lines.push(`- Cost: $${result.totalCostUsd.toFixed(4)}${limits.maxCostUsd > 0 ? ` (limit $${limits.maxCostUsd})` : ''}`, '');
372
+ lines.push('## Attempts', '');
373
+ for (const a of result.attempts) {
374
+ lines.push(`### Attempt ${a.attempt}: ${a.outcome}${a.rolledBack ? ' (rolled back)' : ''}`, '');
375
+ if (a.agentSummary)
376
+ lines.push(`Agent: ${a.agentSummary}`, '');
377
+ if (a.agentError)
378
+ lines.push(`Agent error: ${a.agentError}`, '');
379
+ if (a.verifier) {
380
+ const v = a.verifier;
381
+ lines.push(`Verifier: ${v.passed ? 'PASS' : 'FAIL'}, exit ${v.exitCode === null ? 'n/a' : v.exitCode}${v.timedOut ? ', timed out' : ''}, ${v.durationMs}ms`, '');
382
+ if (v.output)
383
+ lines.push('```', v.output, '```', '');
384
+ }
385
+ lines.push(`Tokens: ${a.tokensUsed}, cost: $${a.costUsd.toFixed(4)}`, '');
386
+ }
387
+ lines.push('## Files changed', '');
388
+ if (result.changedFiles.length === 0)
389
+ lines.push('None.', '');
390
+ else {
391
+ for (const f of result.changedFiles)
392
+ lines.push(`- ${f.status}: \`${f.relPath}\``);
393
+ lines.push('');
394
+ }
395
+ if (result.diff)
396
+ lines.push('## Diff', '', '```diff', result.diff.trimEnd(), '```', '');
397
+ if (result.verified) {
398
+ lines.push('## Next step', '', `Review the branch \`${result.branch}\` and open a PR manually if it looks right. profClaw never pushes or opens PRs on its own.`, '');
399
+ }
400
+ return lines.join('\n');
401
+ }
402
+ //# sourceMappingURL=verified-run.js.map
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Verifier
3
+ *
4
+ * Runs a user-provided verification command (tests, typecheck, lint, build or
5
+ * anything custom) as an objective judge of "done". The command is the only
6
+ * shell input: goal text and model output are never interpolated into it.
7
+ *
8
+ * Safety conventions:
9
+ * - runs with cwd pinned to the run's worktree
10
+ * - hard timeout, process group is killed on expiry
11
+ * - captured output is capped in memory and trimmed for feedback
12
+ * - a small denylist rejects commands that publish or destroy (git push,
13
+ * gh pr create, rm -rf /, curl | sh) because a verifier must only observe
14
+ */
15
+ import type { PlanOptions } from './command-sandbox.js';
16
+ export interface VerifierResult {
17
+ passed: boolean;
18
+ /** Exit code, or null when killed by timeout or signal */
19
+ exitCode: number | null;
20
+ timedOut: boolean;
21
+ durationMs: number;
22
+ /** Trimmed combined stdout + stderr, suitable for feeding back to an agent */
23
+ output: string;
24
+ /** True when the raw output was longer than the trimmed output */
25
+ truncated: boolean;
26
+ }
27
+ export interface VerifierOptions {
28
+ cwd: string;
29
+ timeoutMs: number;
30
+ /** Max characters of output kept for feedback (head and tail are kept) */
31
+ maxOutputChars: number;
32
+ env?: NodeJS.ProcessEnv;
33
+ /**
34
+ * Run the command under the OS sandbox (see command-sandbox.ts). Off by
35
+ * default: user supplied verify commands often need network or write
36
+ * outside the worktree (package caches).
37
+ */
38
+ sandbox?: PlanOptions & {
39
+ allowNetwork?: boolean;
40
+ };
41
+ }
42
+ /** Anything that can judge a workspace. Tests inject fakes. */
43
+ export interface Verifier {
44
+ verify(): Promise<VerifierResult>;
45
+ }
46
+ /** Returns a rejection reason, or null when the command is acceptable. */
47
+ export declare function validateVerifyCommand(command: string): string | null;
48
+ /**
49
+ * Keep the head and tail of long output (failures usually name the problem
50
+ * at the top and summarise at the bottom) and drop the middle.
51
+ */
52
+ export declare function trimOutput(raw: string, maxChars: number): {
53
+ text: string;
54
+ truncated: boolean;
55
+ };
56
+ export declare class CommandVerifier implements Verifier {
57
+ private readonly command;
58
+ private readonly options;
59
+ constructor(command: string, options: VerifierOptions);
60
+ verify(): Promise<VerifierResult>;
61
+ }
62
+ /** Format a result as feedback text for the next agent attempt. */
63
+ export declare function formatFailureFeedback(result: VerifierResult, command: string): string;
64
+ //# sourceMappingURL=verifier.d.ts.map