@awak-app/simy-cli 0.5.72 → 0.5.73
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +84 -0
- package/package.json +2 -2
- package/src/agent.js +191 -11
- package/src/backend-executable.js +21 -1
- package/src/cli-contract.js +1 -1
- package/src/codex-plugins/simy/hooks/README.md +22 -0
- package/src/codex-plugins/simy/hooks/continuation-policy.mjs +27 -0
- package/src/codex-plugins/simy/hooks/delivery-loop.mjs +179 -34
- package/src/codex-plugins/simy/hooks/outcome-progress.mjs +129 -0
- package/src/coding-run-recovery.js +88 -0
- package/src/coding-run-store.js +2 -2
- package/src/continuation-policy.js +2 -0
- package/src/delivery-hooks/command.js +11 -5
- package/src/delivery-hooks/desktop.js +12 -3
- package/src/delivery-hooks/supervisor.js +14 -10
- package/src/desktop-host.js +8 -6
- package/src/desktop-sqm-settings.js +24 -7
- package/src/device-signing-identity.js +65 -0
- package/src/diagnostic-log.js +68 -0
- package/src/doctor.js +55 -0
- package/src/index.js +7 -0
- package/src/local-task.js +68 -3
- package/src/orchestrator/budget.js +55 -4
- package/src/orchestrator/charter-validation.js +33 -0
- package/src/orchestrator/completion-contract.js +1 -1
- package/src/orchestrator/contract.js +2 -0
- package/src/orchestrator/delivery/references/state-and-receipts.md +21 -3
- package/src/orchestrator/independent-audit.js +9 -0
- package/src/orchestrator/instruction.js +6 -1
- package/src/orchestrator/loop.js +38 -41
- package/src/orchestrator/outcome-monitor.js +58 -38
- package/src/orchestrator/outcome-progress.js +2 -0
- package/src/purpose-local-proof.js +380 -0
- package/src/runner.js +224 -36
- package/src/session-supervision/codex-app-server.js +3 -2
- package/src/session-supervision/index.js +16 -4
- package/src/sqm/bundle-store.js +11 -7
- package/src/sqm/claude-plugin.js +2 -0
- package/src/sqm/command.js +36 -4
- package/src/sqm/diagnostics.js +1 -1
- package/src/sqm/evidence-client.js +23 -7
- package/src/sqm/history-store.js +4 -0
- package/src/sqm/index.js +104 -14
- package/src/sqm/proof.js +3 -23
- package/src/sqm/request-recovery.js +82 -0
- package/src/sqm/review-worker.js +59 -12
- package/src/sqm/session.js +33 -3
- package/src/startup-preflight.js +44 -0
package/src/local-task.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { normalizePurposeVerification } from "./purpose-local-proof.js";
|
|
1
2
|
import { buildWorkflowReportingInstruction } from "./workflow-reporting.js";
|
|
2
3
|
import { execFile } from "node:child_process";
|
|
3
4
|
import { createHash, randomUUID } from "node:crypto";
|
|
@@ -20,6 +21,7 @@ import { promisify } from "node:util";
|
|
|
20
21
|
|
|
21
22
|
import { resolveDesktopExecutorCommand } from "./desktop-executor.js";
|
|
22
23
|
import { providerBudgetTokens } from "./orchestrator/budget.js";
|
|
24
|
+
import { continuationMode } from "./continuation-policy.js";
|
|
23
25
|
import {
|
|
24
26
|
buildLocalTaskArtifactRefs,
|
|
25
27
|
MAX_LOCAL_TASK_ARTIFACT_REFS,
|
|
@@ -257,15 +259,21 @@ export function createLocalTask({
|
|
|
257
259
|
expectedOutputs = [],
|
|
258
260
|
scenarioPack = null,
|
|
259
261
|
providerTokenBudget = DEFAULT_LOCAL_TASK_TOKEN_BUDGET,
|
|
262
|
+
continuationPolicy = "bounded",
|
|
263
|
+
providerTokenCeiling = MAX_LOCAL_TASK_TOKEN_BUDGET,
|
|
260
264
|
timeoutMs = DEFAULT_LOCAL_TASK_TIMEOUT_MS,
|
|
261
265
|
idempotencyKeyHash = null,
|
|
262
266
|
requestFingerprint = null,
|
|
263
267
|
workflowObservationBinding = null,
|
|
264
268
|
workflowStartIntent = null,
|
|
269
|
+
purposeVerification = null,
|
|
265
270
|
}) {
|
|
266
271
|
validateTaskId(id);
|
|
267
272
|
const now = new Date().toISOString();
|
|
268
273
|
const tokenBudget = normalizeTokenBudget(providerTokenBudget);
|
|
274
|
+
const policy = continuationMode(continuationPolicy);
|
|
275
|
+
const tokenCeiling = normalizeTokenBudget(providerTokenCeiling);
|
|
276
|
+
if (policy === "progress" && tokenCeiling < tokenBudget) throw new Error("The hard Provider token ceiling cannot be below the notification threshold.");
|
|
269
277
|
const normalizedTimeout = normalizeTimeoutMs(timeoutMs);
|
|
270
278
|
const normalizedScenarioPack = normalizeLocalTaskScenarioPack(scenarioPack);
|
|
271
279
|
const scenarioExpectedOutputs =
|
|
@@ -284,6 +292,7 @@ export function createLocalTask({
|
|
|
284
292
|
return {
|
|
285
293
|
id,
|
|
286
294
|
kind: "local_task",
|
|
295
|
+
...(purposeVerification ? { purpose_verification: normalizePurposeVerification(purposeVerification) } : {}),
|
|
287
296
|
idempotency_key_hash: idempotencyKeyHash,
|
|
288
297
|
request_fingerprint: requestFingerprint,
|
|
289
298
|
status: "queued",
|
|
@@ -316,6 +325,8 @@ export function createLocalTask({
|
|
|
316
325
|
error: null,
|
|
317
326
|
reason_code: null,
|
|
318
327
|
provider_token_budget: tokenBudget,
|
|
328
|
+
continuation_policy: policy,
|
|
329
|
+
provider_token_ceiling: policy === "progress" ? tokenCeiling : tokenBudget,
|
|
319
330
|
provider_tokens_used: 0,
|
|
320
331
|
token_usage: { records: [] },
|
|
321
332
|
...(normalizedWorkflowObservationBinding
|
|
@@ -368,11 +379,14 @@ export function createLocalTaskRequestFingerprint({
|
|
|
368
379
|
repository,
|
|
369
380
|
expectedOutputs,
|
|
370
381
|
providerTokenBudget,
|
|
382
|
+
continuationPolicy = "bounded",
|
|
383
|
+
providerTokenCeiling = MAX_LOCAL_TASK_TOKEN_BUDGET,
|
|
371
384
|
timeoutMs,
|
|
372
385
|
attachmentManifest,
|
|
373
386
|
scenarioPack,
|
|
374
387
|
workflowObservationBinding = null,
|
|
375
388
|
workflowStartIntent = null,
|
|
389
|
+
purposeVerification = null,
|
|
376
390
|
}) {
|
|
377
391
|
const normalizedModel = normalizeLocalTaskModel(model);
|
|
378
392
|
const manifest = Array.isArray(attachmentManifest)
|
|
@@ -398,6 +412,7 @@ export function createLocalTaskRequestFingerprint({
|
|
|
398
412
|
}
|
|
399
413
|
const payload = {
|
|
400
414
|
instruction: String(instruction || "").trim(),
|
|
415
|
+
...(purposeVerification ? { purpose_verification: normalizePurposeVerification(purposeVerification) } : {}),
|
|
401
416
|
backend: String(backend || ""),
|
|
402
417
|
...(normalizedModel ? { model: normalizedModel } : {}),
|
|
403
418
|
operation: String(operation || ""),
|
|
@@ -406,6 +421,10 @@ export function createLocalTaskRequestFingerprint({
|
|
|
406
421
|
repository: String(repository || "").trim() || null,
|
|
407
422
|
expected_outputs: normalizeExpectedOutputs(expectedOutputs),
|
|
408
423
|
provider_token_budget: normalizeTokenBudget(providerTokenBudget),
|
|
424
|
+
...(continuationMode(continuationPolicy) === "progress" ? {
|
|
425
|
+
continuation_policy: "progress",
|
|
426
|
+
provider_token_ceiling: normalizeTokenBudget(providerTokenCeiling),
|
|
427
|
+
} : {}),
|
|
409
428
|
timeout_ms: normalizeTimeoutMs(timeoutMs),
|
|
410
429
|
attachment_manifest: manifest,
|
|
411
430
|
scenario_pack: normalizeLocalTaskScenarioPack(scenarioPack),
|
|
@@ -426,6 +445,7 @@ export async function startLocalTask(
|
|
|
426
445
|
environment = process.env,
|
|
427
446
|
persist = persistLocalTask,
|
|
428
447
|
beforeProviderInvocation = null,
|
|
448
|
+
beforeSpawn = null,
|
|
429
449
|
session = null,
|
|
430
450
|
apiOrigin = null,
|
|
431
451
|
observationFetch = globalThis.fetch,
|
|
@@ -509,6 +529,7 @@ export async function startLocalTask(
|
|
|
509
529
|
// synchronous spawn boundary so a stopped task can never start Provider work.
|
|
510
530
|
if (task.stop_requested || task.status !== "running") return task;
|
|
511
531
|
|
|
532
|
+
|
|
512
533
|
const observationSender = createWorkflowObservationSender({
|
|
513
534
|
binding: task.workflow_observation_binding,
|
|
514
535
|
startIntent: task.workflow_start_intent,
|
|
@@ -523,6 +544,27 @@ export async function startLocalTask(
|
|
|
523
544
|
path.join(runtimeTempBase, `simy-local-task-${task.id}-`),
|
|
524
545
|
);
|
|
525
546
|
allowCodexRuntimeTemp(command, runtimeTempRoot);
|
|
547
|
+
// Temp preparation is asynchronous too: the final authority check belongs
|
|
548
|
+
// after it, immediately adjacent to the synchronous process spawn boundary.
|
|
549
|
+
if (task.stop_requested || task.status !== "running") {
|
|
550
|
+
await rm(runtimeTempRoot, { recursive: true, force: true }); return task;
|
|
551
|
+
}
|
|
552
|
+
if (beforeSpawn) {
|
|
553
|
+
try { await beforeSpawn(task); }
|
|
554
|
+
catch (error) {
|
|
555
|
+
try {
|
|
556
|
+
if (!task.stop_requested && task.status === "running") {
|
|
557
|
+
updateTask(task, { status: "failed", control_state: "complete", error: errorMessage(error),
|
|
558
|
+
reason_code: error.code || "local_task_invocation_authorization_failed", completed_at: new Date().toISOString() });
|
|
559
|
+
await persist(task);
|
|
560
|
+
}
|
|
561
|
+
} finally { await rm(runtimeTempRoot, { recursive: true, force: true }); }
|
|
562
|
+
return task;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
if (task.stop_requested || task.status !== "running") {
|
|
566
|
+
await rm(runtimeTempRoot, { recursive: true, force: true }); return task;
|
|
567
|
+
}
|
|
526
568
|
|
|
527
569
|
return new Promise((resolve, reject) => {
|
|
528
570
|
let settled = false;
|
|
@@ -572,13 +614,20 @@ export async function startLocalTask(
|
|
|
572
614
|
emitTaskEvent(task, "usage", {
|
|
573
615
|
provider_tokens_used: task.provider_tokens_used,
|
|
574
616
|
provider_token_budget: task.provider_token_budget,
|
|
617
|
+
continuation_policy: task.continuation_policy || "bounded",
|
|
618
|
+
provider_token_ceiling: localTaskTokenCeiling(task),
|
|
575
619
|
});
|
|
620
|
+
if (task.continuation_policy === "progress" && task.provider_tokens_used >= task.provider_token_budget && !task.token_warning_emitted) {
|
|
621
|
+
task.token_warning_emitted = true;
|
|
622
|
+
task.warnings.push("Provider token notification threshold reached. Work continues within the approved task and hard safety ceiling.");
|
|
623
|
+
emitTaskEvent(task, "budget_warning", { provider_tokens_used: task.provider_tokens_used, provider_token_budget: task.provider_token_budget, provider_token_ceiling: localTaskTokenCeiling(task) });
|
|
624
|
+
}
|
|
576
625
|
if (
|
|
577
|
-
task
|
|
626
|
+
localTaskTokensExhausted(task) &&
|
|
578
627
|
!task.stop_requested
|
|
579
628
|
) {
|
|
580
629
|
task.reason_code = "provider_token_budget_exhausted";
|
|
581
|
-
task.error = `Provider token budget exceeded (${task.provider_tokens_used} / ${task
|
|
630
|
+
task.error = `Provider token ${task.continuation_policy === "progress" ? "hard ceiling" : "budget"} exceeded (${task.provider_tokens_used} / ${localTaskTokenCeiling(task)}).`;
|
|
582
631
|
task.stop_requested = true;
|
|
583
632
|
task.control_state = "stopping";
|
|
584
633
|
signalExecutorProcess(child, "SIGINT");
|
|
@@ -692,7 +741,7 @@ export async function startLocalTask(
|
|
|
692
741
|
Boolean(error) ||
|
|
693
742
|
exitCode !== 0 ||
|
|
694
743
|
Boolean(task.error) ||
|
|
695
|
-
task
|
|
744
|
+
localTaskTokensExhausted(task);
|
|
696
745
|
task.status = manuallyStopped ? "stopped" : failed ? "failed" : "succeeded";
|
|
697
746
|
task.control_state = task.status === "stopped" ? "stopped" : "complete";
|
|
698
747
|
task.error ||= error || (exitCode !== 0 ? `Provider exited with code ${exitCode}.` : null);
|
|
@@ -823,6 +872,7 @@ export function localTaskSnapshot(task, { includeEvents = true, includeLocalPath
|
|
|
823
872
|
output: [...task.output],
|
|
824
873
|
...(includeEvents ? { events: structuredClone(task.events) } : {}),
|
|
825
874
|
result: structuredClone(task.result),
|
|
875
|
+
...(task.host_verification ? { host_verification: structuredClone(task.host_verification) } : {}),
|
|
826
876
|
artifacts: task.artifacts.map(publicArtifact),
|
|
827
877
|
attachments: task.attachments.map(publicAttachment),
|
|
828
878
|
expected_outputs: structuredClone(task.expected_outputs),
|
|
@@ -836,6 +886,9 @@ export function localTaskSnapshot(task, { includeEvents = true, includeLocalPath
|
|
|
836
886
|
error: task.error,
|
|
837
887
|
reason_code: task.reason_code,
|
|
838
888
|
provider_token_budget: task.provider_token_budget,
|
|
889
|
+
continuation_policy: task.continuation_policy || "bounded",
|
|
890
|
+
provider_token_ceiling: localTaskTokenCeiling(task),
|
|
891
|
+
token_warning_emitted: task.token_warning_emitted === true,
|
|
839
892
|
provider_tokens_used: task.provider_tokens_used,
|
|
840
893
|
token_usage: structuredClone(task.token_usage),
|
|
841
894
|
...(task.workflow_observation
|
|
@@ -849,6 +902,8 @@ export function localTaskSnapshot(task, { includeEvents = true, includeLocalPath
|
|
|
849
902
|
completed_at: task.completed_at,
|
|
850
903
|
...(includeLocalPaths
|
|
851
904
|
? {
|
|
905
|
+
...(task.purpose_verification ? { purpose_verification: structuredClone(task.purpose_verification),
|
|
906
|
+
purpose_verification_state: structuredClone(task.purpose_verification_state || null) } : {}),
|
|
852
907
|
workspace_path: task.workspace_path,
|
|
853
908
|
task_root: task.task_root,
|
|
854
909
|
inputs_root: task.inputs_root,
|
|
@@ -1057,6 +1112,7 @@ function restoreLocalTask(snapshot) {
|
|
|
1057
1112
|
);
|
|
1058
1113
|
return {
|
|
1059
1114
|
...snapshot,
|
|
1115
|
+
...(snapshot.purpose_verification ? { purpose_verification: normalizePurposeVerification(snapshot.purpose_verification) } : {}),
|
|
1060
1116
|
outputs_root_expected:
|
|
1061
1117
|
snapshot.outputs_root_expected || null,
|
|
1062
1118
|
output: Array.isArray(snapshot.output) ? snapshot.output : [],
|
|
@@ -1243,6 +1299,15 @@ function normalizeExpectedOutputs(value) {
|
|
|
1243
1299
|
}));
|
|
1244
1300
|
}
|
|
1245
1301
|
|
|
1302
|
+
function localTaskTokenCeiling(task) {
|
|
1303
|
+
return task.continuation_policy === "progress" ? normalizeTokenBudget(task.provider_token_ceiling ?? MAX_LOCAL_TASK_TOKEN_BUDGET) : task.provider_token_budget;
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
function localTaskTokensExhausted(task) {
|
|
1307
|
+
const ceiling = localTaskTokenCeiling(task);
|
|
1308
|
+
return task.continuation_policy === "progress" ? task.provider_tokens_used >= ceiling : task.provider_tokens_used > ceiling;
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1246
1311
|
function normalizeTokenBudget(value) {
|
|
1247
1312
|
const budget = Number(value ?? DEFAULT_LOCAL_TASK_TOKEN_BUDGET);
|
|
1248
1313
|
if (!Number.isInteger(budget) || budget < 1 || budget > MAX_LOCAL_TASK_TOKEN_BUDGET) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { clampAttempts } from "./shared.js";
|
|
2
|
+
import { progressLimits } from "../continuation-policy.js";
|
|
2
3
|
|
|
3
4
|
export const DEFAULT_TOKEN_BUDGET = 250_000;
|
|
4
5
|
export const DEFAULT_RETRY_BUDGET = 2;
|
|
@@ -54,6 +55,31 @@ export function increaseProviderTokenBudget(snapshot, value) {
|
|
|
54
55
|
return refreshBudgetState(snapshot);
|
|
55
56
|
}
|
|
56
57
|
|
|
58
|
+
export function extendProgressCeilings(snapshot, changes = {}) {
|
|
59
|
+
const current = budgetState(snapshot);
|
|
60
|
+
if (current.continuation_policy !== "progress") {
|
|
61
|
+
throw new Error("Hard ceilings can be extended only for an explicitly approved progress policy.");
|
|
62
|
+
}
|
|
63
|
+
if (!changes || typeof changes !== "object" || Array.isArray(changes) ||
|
|
64
|
+
Object.keys(changes).some(key => !["provider_token_ceiling", "attempt_ceiling"].includes(key))) {
|
|
65
|
+
throw new Error("Specify only the Provider token ceiling or attempt ceiling.");
|
|
66
|
+
}
|
|
67
|
+
const provided = Object.entries(changes).filter(([, value]) => value !== undefined);
|
|
68
|
+
if (!provided.length) throw new Error("Explicitly increase at least one hard ceiling.");
|
|
69
|
+
for (const [field, value] of provided) {
|
|
70
|
+
const previous = field === "provider_token_ceiling" ? current.hard_token_ceiling : current.hard_attempt_ceiling;
|
|
71
|
+
const used = field === "provider_token_ceiling" ? current.tokens_used : (snapshot.attempts || []).length;
|
|
72
|
+
if (!Number.isSafeInteger(value) || value <= previous || value <= used) {
|
|
73
|
+
throw new Error("Each requested hard ceiling must be an integer higher than its current limit and recorded usage.");
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// Validate every field before changing either ceiling; partial rejection must
|
|
77
|
+
// never leave a larger authorization behind.
|
|
78
|
+
const limits = progressLimits({ ...snapshot.charter, ...Object.fromEntries(provided) });
|
|
79
|
+
for (const [field] of provided) snapshot.charter[field] = limits[field];
|
|
80
|
+
return refreshBudgetState(snapshot);
|
|
81
|
+
}
|
|
82
|
+
|
|
57
83
|
export function budgetState(snapshot, pendingAttempt = null) {
|
|
58
84
|
const attempts = [
|
|
59
85
|
...(Array.isArray(snapshot?.attempts) ? snapshot.attempts : []),
|
|
@@ -67,11 +93,15 @@ export function budgetState(snapshot, pendingAttempt = null) {
|
|
|
67
93
|
const tokensUsed = attempts.reduce(
|
|
68
94
|
(sum, attempt) => sum + tokenUsageTotal(attempt?.token_usage),
|
|
69
95
|
0,
|
|
70
|
-
);
|
|
96
|
+
) + unaccountedProviderUsage(snapshot, attempts);
|
|
71
97
|
const attemptsUsed = attempts.length;
|
|
72
98
|
const retriesUsed = Math.max(attemptsUsed - 1, 0);
|
|
73
|
-
const
|
|
74
|
-
const
|
|
99
|
+
const policy = progressLimits(snapshot?.charter);
|
|
100
|
+
const progress = policy.continuation_policy === "progress";
|
|
101
|
+
const hardTokenCeiling = progress ? policy.provider_token_ceiling : tokenBudget;
|
|
102
|
+
const hardAttemptCeiling = progress ? policy.attempt_ceiling : retryBudget + 1;
|
|
103
|
+
const tokenExhausted = tokensUsed >= hardTokenCeiling;
|
|
104
|
+
const retryExhausted = attemptsUsed >= hardAttemptCeiling;
|
|
75
105
|
return {
|
|
76
106
|
schema_version: 1,
|
|
77
107
|
token_budget: tokenBudget,
|
|
@@ -88,9 +118,26 @@ export function budgetState(snapshot, pendingAttempt = null) {
|
|
|
88
118
|
: retryExhausted
|
|
89
119
|
? "retry_budget_exhausted"
|
|
90
120
|
: null,
|
|
121
|
+
continuation_policy: policy.continuation_policy,
|
|
122
|
+
token_warning: progress && tokensUsed >= tokenBudget,
|
|
123
|
+
retry_warning: progress && attemptsUsed >= retryBudget + 1,
|
|
124
|
+
hard_token_ceiling: hardTokenCeiling,
|
|
125
|
+
hard_attempt_ceiling: hardAttemptCeiling,
|
|
126
|
+
hard_tokens_remaining: Math.max(hardTokenCeiling - tokensUsed, 0),
|
|
91
127
|
};
|
|
92
128
|
}
|
|
93
129
|
|
|
130
|
+
function unaccountedProviderUsage(snapshot, attempts) {
|
|
131
|
+
const checkpoints = snapshot?.metadata?.provider_usage_checkpoints || {};
|
|
132
|
+
return Object.entries(checkpoints).reduce((sum, [id, checkpoint]) => {
|
|
133
|
+
const reported = tokenUsageTotal(checkpoint?.token_usage);
|
|
134
|
+
const accounted = attempts.reduce((total, attempt) => total + tokenUsageTotal({
|
|
135
|
+
records: (Array.isArray(attempt?.token_usage?.records) ? attempt.token_usage.records : []).filter(record => record?.invocation_id === id),
|
|
136
|
+
}), 0);
|
|
137
|
+
return sum + Math.max(0, reported - accounted);
|
|
138
|
+
}, 0);
|
|
139
|
+
}
|
|
140
|
+
|
|
94
141
|
export function refreshBudgetState(snapshot, pendingAttempt = null) {
|
|
95
142
|
const state = budgetState(snapshot, pendingAttempt);
|
|
96
143
|
snapshot.budget = state;
|
|
@@ -102,12 +149,16 @@ export function canStartProvider(snapshot, { attemptNumber, phase }) {
|
|
|
102
149
|
if (state.token_exhausted) {
|
|
103
150
|
return { allowed: false, reason: "token_budget_exhausted", budget: state };
|
|
104
151
|
}
|
|
105
|
-
if (phase === "executor" && attemptNumber > state.
|
|
152
|
+
if (phase === "executor" && attemptNumber > state.hard_attempt_ceiling) {
|
|
106
153
|
return { allowed: false, reason: "retry_budget_exhausted", budget: state };
|
|
107
154
|
}
|
|
108
155
|
return { allowed: true, reason: null, budget: state };
|
|
109
156
|
}
|
|
110
157
|
|
|
158
|
+
export function codingAttemptCeiling(snapshot) {
|
|
159
|
+
return snapshot?.charter?.continuation_policy === "progress" ? budgetState(snapshot).hard_attempt_ceiling : snapshot?.charter?.max_attempts;
|
|
160
|
+
}
|
|
161
|
+
|
|
111
162
|
export function tokenUsageTotal(value) {
|
|
112
163
|
if (!value || typeof value !== "object" || Array.isArray(value)) return 0;
|
|
113
164
|
if (Array.isArray(value.records)) {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export function validateCharter(charter) {
|
|
2
|
+
const findings = [];
|
|
3
|
+
if (charter.risk.requires_design_review && charter.acceptance_criteria_source !== "explicit") {
|
|
4
|
+
findings.push({
|
|
5
|
+
code: "EXPLICIT_ACCEPTANCE_CRITERIA_REQUIRED",
|
|
6
|
+
severity: "blocker",
|
|
7
|
+
target: "acceptance_criteria",
|
|
8
|
+
explanation: "High-risk work requires explicit acceptance criteria.",
|
|
9
|
+
repairability: "manual",
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
if (charter.design_review.required && charter.design_review.status !== "approved") {
|
|
13
|
+
findings.push({
|
|
14
|
+
code: "DESIGN_REVIEW_REQUIRED",
|
|
15
|
+
severity: "blocker",
|
|
16
|
+
target: "design_review",
|
|
17
|
+
explanation:
|
|
18
|
+
"High-risk work requires a design summary, approval owner, and review evidence before implementation.",
|
|
19
|
+
repairability: "manual",
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
if (charter.risk.requires_design_review && charter.required_checks.length === 0) {
|
|
23
|
+
findings.push({
|
|
24
|
+
code: "REQUIRED_CI_CHECKS_MISSING",
|
|
25
|
+
severity: "blocker",
|
|
26
|
+
target: "required_checks",
|
|
27
|
+
explanation: "High-risk work requires explicit CI and security check names.",
|
|
28
|
+
repairability: "manual",
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
return findings;
|
|
32
|
+
}
|
|
33
|
+
|
|
@@ -65,7 +65,7 @@ function criterionIds(value, count) {
|
|
|
65
65
|
if (
|
|
66
66
|
ids.length !== count ||
|
|
67
67
|
new Set(ids).size !== ids.length ||
|
|
68
|
-
ids.some((id) => !criterionIdPattern.test(id))
|
|
68
|
+
ids.some((id) => id === "USER-GOAL" || !criterionIdPattern.test(id))
|
|
69
69
|
) {
|
|
70
70
|
throw new Error("Invalid acceptance criterion IDs");
|
|
71
71
|
}
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
visualEvidenceSurface,
|
|
9
9
|
} from "./shared.js";
|
|
10
10
|
import { budgetState, resolveRunBudgets } from "./budget.js";
|
|
11
|
+
import { progressLimits } from "../continuation-policy.js";
|
|
11
12
|
import { classifyRisk } from "./risk.js";
|
|
12
13
|
import { buildCompletionContract } from "./completion-contract.js";
|
|
13
14
|
import { freezeObjective } from "./outcome-monitor.js";
|
|
@@ -117,6 +118,7 @@ export function createCodingLoopSnapshot({ runId, request, metadata = {} }) {
|
|
|
117
118
|
max_attempts: budgets.max_attempts,
|
|
118
119
|
retry_budget: budgets.retry_budget,
|
|
119
120
|
token_budget: budgets.token_budget,
|
|
121
|
+
...progressLimits(request),
|
|
120
122
|
ui_evidence_root: cleanString(request.ui_evidence_root),
|
|
121
123
|
acceptance_criteria: finalAcceptanceCriteria,
|
|
122
124
|
acceptance_criteria_source: acceptanceCriteriaSource,
|
|
@@ -99,9 +99,27 @@ recommendation, silence, or an implementation pattern.
|
|
|
99
99
|
The controller persists `thread_state.objective_contract` with the baseline,
|
|
100
100
|
current version, digest and explicit human revision lineage. Each attempt stores
|
|
101
101
|
`outcome_progress` with its objective digest, independently supported criterion
|
|
102
|
-
IDs, unproven IDs and assessment availability.
|
|
103
|
-
|
|
104
|
-
|
|
102
|
+
IDs, unproven IDs and assessment availability. `evaluateOutcomeProgress` separates
|
|
103
|
+
current `verified_ids`, `failed_ids`, and `unassessed_ids`; executor test counts,
|
|
104
|
+
commits, evidence hashes and reference paths cannot create progress. A new
|
|
105
|
+
independently verified criterion resets `no_progress_attempts`. A prior proof
|
|
106
|
+
missing from the current review remains unassessed, rather than silently passing.
|
|
107
|
+
The historical verified-ID set is used only for comparison, so repeating an old
|
|
108
|
+
proof after a regression cannot manufacture new progress.
|
|
109
|
+
|
|
110
|
+
Progress continuation uses `{ policy: "progress", maxNoProgressAttempts: 3 }`
|
|
111
|
+
with `assessOutcomeProgress`. Three attempts without new verified outcomes,
|
|
112
|
+
including attempts that never reach independent review, mark `stalled` with a
|
|
113
|
+
specific `stall_reason`. The pure evaluator accepts the same threshold for
|
|
114
|
+
Delivery. Legacy runs retain their original stopping policy until the controller
|
|
115
|
+
selects progress continuation. Partial independent proof can show progress even
|
|
116
|
+
when the overall audit fails; a complete passing audit is still required for
|
|
117
|
+
completion. Human and external blockers remain visible alongside machine repairs
|
|
118
|
+
in `blockers` and `next_actions`; they do not imply every criterion is blocked.
|
|
119
|
+
|
|
120
|
+
Audits bound to a different objective or current candidate evidence identity
|
|
121
|
+
cannot supply progress. Revisions reset the comparison baseline and require a
|
|
122
|
+
fresh independent review. An old review cannot settle a newer
|
|
105
123
|
objective version. Legacy snapshots without an objective contract remain
|
|
106
124
|
readable but lose completion status on restore; their original baseline cannot
|
|
107
125
|
be reconstructed retrospectively. `USER-GOAL` is tracked separately from
|
|
@@ -108,6 +108,7 @@ export function buildIndependentAuditInstruction(charter, attempt) {
|
|
|
108
108
|
"Required keys: passed, summary, findings, criterion_results, visual_review, coverage_review.",
|
|
109
109
|
"coverage_review must contain requirements_complete (boolean), unmapped_requirements (array), observation, and user_outcome with passed, verification_method, evidence_refs, and observed_result. Independently exercise or inspect proof of the original user goal even when supplied acceptance criteria mention only tests, code, or a PR. Use passed=false if the goal is not demonstrated. Explain the comparison of the original request, frozen experience, stable criterion-ID list, implementation and raw user/system proof.",
|
|
110
110
|
"Each finding candidate must contain passed, code, severity, target, explanation, and repairability.",
|
|
111
|
+
"For a failed finding, optionally identify blocker_kind as machine, human, or external. Use human when a specific user decision or credential interaction is required, external for an unavailable outside dependency, and machine when local repair or verification can proceed. One blocked criterion does not imply all remaining work is blocked.",
|
|
111
112
|
"criterion_results must contain one entry per stable criterion ID with criterion_id, passed, verification_method, evidence_refs, and observed_result.",
|
|
112
113
|
"visual_review must contain inspected, artifacts, and acceptance_observations.",
|
|
113
114
|
"Only passed=false entries are findings. Omit passing checks, and never report passed=true evidence as a finding.",
|
|
@@ -121,6 +122,10 @@ export function buildIndependentAudit(execution, charter = null, attempt = null)
|
|
|
121
122
|
const result = execution?.result && typeof execution.result === "object" ? execution.result : {};
|
|
122
123
|
const completionContractV2 =
|
|
123
124
|
charter?.completion_contract?.version === COMPLETION_CONTRACT_VERSION;
|
|
125
|
+
const objectiveBinding = {
|
|
126
|
+
objective_digest: charter?.thread_state?.objective_contract?.current_digest || null,
|
|
127
|
+
objective_version: charter?.thread_state?.objective_contract?.version || null,
|
|
128
|
+
};
|
|
124
129
|
const valid =
|
|
125
130
|
execution?.exitCode === 0 &&
|
|
126
131
|
!execution?.error &&
|
|
@@ -132,6 +137,7 @@ export function buildIndependentAudit(execution, charter = null, attempt = null)
|
|
|
132
137
|
typeof result.visual_review === "object"));
|
|
133
138
|
if (!valid) {
|
|
134
139
|
return {
|
|
140
|
+
...objectiveBinding,
|
|
135
141
|
passed: false,
|
|
136
142
|
summary: cleanString(execution?.error || result.summary) || "Independent audit did not return valid evidence.",
|
|
137
143
|
findings: [
|
|
@@ -159,6 +165,8 @@ export function buildIndependentAudit(execution, charter = null, attempt = null)
|
|
|
159
165
|
target: cleanString(item.target) || "pull_request",
|
|
160
166
|
explanation: cleanString(item.explanation) || "Independent audit finding.",
|
|
161
167
|
repairability: item.repairability === "manual" ? "manual" : "auto",
|
|
168
|
+
blocker_kind: ["machine", "human", "external"].includes(item.blocker_kind)
|
|
169
|
+
? item.blocker_kind : item.repairability === "manual" ? "human" : "machine",
|
|
162
170
|
auto_fix_hint: cleanString(item.auto_fix_hint) || null,
|
|
163
171
|
}));
|
|
164
172
|
const findings = evaluations.filter((item) => item.passed === false);
|
|
@@ -243,6 +251,7 @@ export function buildIndependentAudit(execution, charter = null, attempt = null)
|
|
|
243
251
|
}
|
|
244
252
|
const requiresHuman = findings.some((item) => item.repairability === "manual");
|
|
245
253
|
return {
|
|
254
|
+
...objectiveBinding,
|
|
246
255
|
passed: result.passed === true && findings.length === 0,
|
|
247
256
|
summary: cleanString(result.summary) || "Independent audit completed.",
|
|
248
257
|
findings,
|
|
@@ -39,9 +39,14 @@ export function buildCodingInstruction(
|
|
|
39
39
|
`Risk level: ${charter.risk.level}`,
|
|
40
40
|
`Design summary: ${charter.design_review.summary || "not required"}`,
|
|
41
41
|
`Design approved by: ${charter.design_review.approved_by || "not required"}`,
|
|
42
|
-
`Attempt: ${attemptNumber}/${charter.max_attempts}`,
|
|
42
|
+
`Attempt: ${attemptNumber}/${budget?.hard_attempt_ceiling ?? charter.max_attempts}`,
|
|
43
43
|
`Retry budget: ${Math.max(attemptNumber - 1, 0)}/${charter.retry_budget}`,
|
|
44
44
|
`Token budget remaining: ${budget?.tokens_remaining ?? charter.token_budget}/${charter.token_budget}`,
|
|
45
|
+
...(charter.continuation_policy === "progress" ? [
|
|
46
|
+
"Continuation policy: verified acceptance progress. Token/retry thresholds notify; they do not relax acceptance or authorize new work.",
|
|
47
|
+
`Hard Provider token ceiling: ${budget?.hard_token_ceiling}; remaining: ${budget?.hard_tokens_remaining}. Hard attempt ceiling: ${budget?.hard_attempt_ceiling}.`,
|
|
48
|
+
"Report human/external blockers as soon as observed; continue independent work within the approved objective. Changed metadata alone is not progress.",
|
|
49
|
+
] : []),
|
|
45
50
|
`Local evidence root: ${charter.ui_evidence_root || "not required"}`,
|
|
46
51
|
].join("\n"),
|
|
47
52
|
),
|
package/src/orchestrator/loop.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { validateCharter } from "./charter-validation.js";
|
|
1
2
|
import { redactExecutionText } from "./execution-io.js";
|
|
2
3
|
import { minimumCharter } from "./delivery/scripts/check-minimum-completion.mjs";
|
|
3
4
|
import { auditAttempt, evaluatePrReadiness } from "./audit.js";
|
|
@@ -6,7 +7,7 @@ import {
|
|
|
6
7
|
buildIndependentAuditInstruction,
|
|
7
8
|
} from "./independent-audit.js";
|
|
8
9
|
import { buildCodingInstruction, buildPromptInterventions } from "./instruction.js";
|
|
9
|
-
import { canStartProvider, refreshBudgetState } from "./budget.js";
|
|
10
|
+
import { canStartProvider, codingAttemptCeiling, refreshBudgetState } from "./budget.js";
|
|
10
11
|
import { buildAttempt, mergeTokenUsage } from "./result.js";
|
|
11
12
|
import { evaluateRetryCircuit } from "./retry.js";
|
|
12
13
|
import { assessOutcomeProgress, verifyObjective } from "./outcome-monitor.js";
|
|
@@ -76,7 +77,8 @@ async function runCodingLoopOwned({
|
|
|
76
77
|
|
|
77
78
|
let previousFindings = resume ? snapshot.final_audit?.findings || [] : [];
|
|
78
79
|
const firstAttempt = resume ? snapshot.attempts.length + 1 : 1;
|
|
79
|
-
const
|
|
80
|
+
const progressPolicy = snapshot.charter.continuation_policy === "progress";
|
|
81
|
+
const lastAttempt = progressPolicy ? codingAttemptCeiling(snapshot) : resume ? firstAttempt : snapshot.charter.max_attempts;
|
|
80
82
|
for (
|
|
81
83
|
let attemptNumber = firstAttempt;
|
|
82
84
|
attemptNumber <= lastAttempt;
|
|
@@ -183,8 +185,9 @@ async function runCodingLoopOwned({
|
|
|
183
185
|
if (shouldStop()) return stopCodingLoop(snapshot, onUpdate);
|
|
184
186
|
attempt.implementation_gate = attempt.audit.implementation_gate;
|
|
185
187
|
snapshot.attempts.push(attempt);
|
|
186
|
-
assessOutcomeProgress(snapshot.charter, attempt, previousAttempt, { resetUnavailableCount: resume && attemptNumber === firstAttempt });
|
|
188
|
+
assessOutcomeProgress(snapshot.charter, attempt, previousAttempt, { ...outcomeOptions(snapshot), resetUnavailableCount: resume && attemptNumber === firstAttempt });
|
|
187
189
|
attempt.budget = refreshBudgetState(snapshot);
|
|
190
|
+
recordBudgetWarnings(snapshot, attempt);
|
|
188
191
|
|
|
189
192
|
if (snapshot.budget.token_exhausted) {
|
|
190
193
|
await stopForBudget({
|
|
@@ -234,6 +237,8 @@ async function runCodingLoopOwned({
|
|
|
234
237
|
const auditInstruction = buildIndependentAuditInstruction(snapshot.charter, attempt);
|
|
235
238
|
const reviewedMatrix = attempt.observed_evidence?.minimum_completion?.matrix_sha256;
|
|
236
239
|
const reviewedEvidenceIdentity = independentAuditEvidenceIdentity(attempt);
|
|
240
|
+
const reviewedObjectiveDigest = snapshot.charter.thread_state?.objective_contract?.current_digest;
|
|
241
|
+
const reviewedObjectiveVersion = snapshot.charter.thread_state?.objective_contract?.version;
|
|
237
242
|
const auditExecution = await safeExecution(() =>
|
|
238
243
|
executeIndependentAudit({
|
|
239
244
|
attemptNumber,
|
|
@@ -251,9 +256,16 @@ async function runCodingLoopOwned({
|
|
|
251
256
|
);
|
|
252
257
|
attempt.token_usage = mergeTokenUsage(attempt.token_usage, auditExecution.tokenUsage);
|
|
253
258
|
attempt.budget = refreshBudgetState(snapshot);
|
|
259
|
+
recordBudgetWarnings(snapshot, attempt);
|
|
260
|
+
if (attempt.budget.token_exhausted) {
|
|
261
|
+
await stopForBudget({ snapshot, attempt, reason: "token_budget_exhausted", onUpdate });
|
|
262
|
+
return snapshot;
|
|
263
|
+
}
|
|
254
264
|
|
|
255
265
|
attempt.independent_audit.minimum_matrix_sha256 = reviewedMatrix;
|
|
256
266
|
attempt.independent_audit.evidence_identity_sha256 = reviewedEvidenceIdentity;
|
|
267
|
+
attempt.independent_audit.objective_digest = reviewedObjectiveDigest;
|
|
268
|
+
attempt.independent_audit.objective_version = reviewedObjectiveVersion;
|
|
257
269
|
|
|
258
270
|
// Re-collect after the read-only auditor to catch any mutated HEAD or working tree.
|
|
259
271
|
attempt.observed_evidence = await safeEvidence(collectEvidence, attempt);
|
|
@@ -265,7 +277,7 @@ async function runCodingLoopOwned({
|
|
|
265
277
|
});
|
|
266
278
|
if (shouldStop()) return stopCodingLoop(snapshot, onUpdate);
|
|
267
279
|
attempt.implementation_gate = attempt.audit.implementation_gate;
|
|
268
|
-
assessOutcomeProgress(snapshot.charter, attempt, previousAttempt, { resetUnavailableCount: resume && attemptNumber === firstAttempt });
|
|
280
|
+
assessOutcomeProgress(snapshot.charter, attempt, previousAttempt, { ...outcomeOptions(snapshot), resetUnavailableCount: resume && attemptNumber === firstAttempt });
|
|
269
281
|
|
|
270
282
|
const independentFindings = attempt.independent_audit.findings || [];
|
|
271
283
|
if (!attempt.audit.passed || !attempt.independent_audit.passed) {
|
|
@@ -308,6 +320,10 @@ async function runCodingLoopOwned({
|
|
|
308
320
|
return snapshot;
|
|
309
321
|
}
|
|
310
322
|
|
|
323
|
+
function outcomeOptions(snapshot) {
|
|
324
|
+
return { policy: snapshot.charter.continuation_policy || "bounded", maxNoProgressAttempts: snapshot.charter.max_no_progress_attempts || 3 };
|
|
325
|
+
}
|
|
326
|
+
|
|
311
327
|
async function stopCodingLoop(snapshot, onUpdate) {
|
|
312
328
|
if (snapshot.state === "stopped") return snapshot;
|
|
313
329
|
appendEvent(snapshot, "stopped", "Agentic loop stopped by the local human operator.", {
|
|
@@ -354,6 +370,7 @@ async function recheckPrReadinessOwned({ snapshot, collectEvidence, checkSqm = n
|
|
|
354
370
|
});
|
|
355
371
|
attempt.outcome_progress = assessOutcomeProgress(
|
|
356
372
|
snapshot.charter, attempt, snapshot.attempts.at(-2) ?? null,
|
|
373
|
+
outcomeOptions(snapshot),
|
|
357
374
|
);
|
|
358
375
|
snapshot.implementation_gate_passed = attempt.audit.passed;
|
|
359
376
|
attempt.pr_readiness = evaluatePrReadiness(snapshot.charter, attempt);
|
|
@@ -372,38 +389,6 @@ async function recheckPrReadinessOwned({ snapshot, collectEvidence, checkSqm = n
|
|
|
372
389
|
return snapshot;
|
|
373
390
|
}
|
|
374
391
|
|
|
375
|
-
function validateCharter(charter) {
|
|
376
|
-
const findings = [];
|
|
377
|
-
if (charter.risk.requires_design_review && charter.acceptance_criteria_source !== "explicit") {
|
|
378
|
-
findings.push({
|
|
379
|
-
code: "EXPLICIT_ACCEPTANCE_CRITERIA_REQUIRED",
|
|
380
|
-
severity: "blocker",
|
|
381
|
-
target: "acceptance_criteria",
|
|
382
|
-
explanation: "High-risk work requires explicit acceptance criteria.",
|
|
383
|
-
repairability: "manual",
|
|
384
|
-
});
|
|
385
|
-
}
|
|
386
|
-
if (charter.design_review.required && charter.design_review.status !== "approved") {
|
|
387
|
-
findings.push({
|
|
388
|
-
code: "DESIGN_REVIEW_REQUIRED",
|
|
389
|
-
severity: "blocker",
|
|
390
|
-
target: "design_review",
|
|
391
|
-
explanation:
|
|
392
|
-
"High-risk work requires a design summary, approval owner, and review evidence before implementation.",
|
|
393
|
-
repairability: "manual",
|
|
394
|
-
});
|
|
395
|
-
}
|
|
396
|
-
if (charter.risk.requires_design_review && charter.required_checks.length === 0) {
|
|
397
|
-
findings.push({
|
|
398
|
-
code: "REQUIRED_CI_CHECKS_MISSING",
|
|
399
|
-
severity: "blocker",
|
|
400
|
-
target: "required_checks",
|
|
401
|
-
explanation: "High-risk work requires explicit CI and security check names.",
|
|
402
|
-
repairability: "manual",
|
|
403
|
-
});
|
|
404
|
-
}
|
|
405
|
-
return findings;
|
|
406
|
-
}
|
|
407
392
|
|
|
408
393
|
async function safeExecution(callback) {
|
|
409
394
|
try {
|
|
@@ -515,7 +500,7 @@ async function handleFailedAudit({
|
|
|
515
500
|
previousAttempt: snapshot.attempts.at(-2) ?? null,
|
|
516
501
|
findings,
|
|
517
502
|
});
|
|
518
|
-
if (retryCircuit.open) {
|
|
503
|
+
if (retryCircuit.open && !attempt.outcome_progress?.has_progress) {
|
|
519
504
|
const avoidedAttempts = Math.max(snapshot.charter.max_attempts - attemptNumber, 0);
|
|
520
505
|
const circuitState = {
|
|
521
506
|
state: "open",
|
|
@@ -546,7 +531,7 @@ async function handleFailedAudit({
|
|
|
546
531
|
await publish(snapshot, "blocked", onUpdate);
|
|
547
532
|
return true;
|
|
548
533
|
}
|
|
549
|
-
if (attemptNumber >= snapshot
|
|
534
|
+
if (attemptNumber >= codingAttemptCeiling(snapshot)) {
|
|
550
535
|
await stopForBudget({
|
|
551
536
|
snapshot,
|
|
552
537
|
attempt,
|
|
@@ -590,7 +575,7 @@ async function handleFailedAudit({
|
|
|
590
575
|
await publish(snapshot, "waiting_human", onUpdate);
|
|
591
576
|
return true;
|
|
592
577
|
}
|
|
593
|
-
if (attemptNumber < snapshot
|
|
578
|
+
if (attemptNumber < codingAttemptCeiling(snapshot)) {
|
|
594
579
|
appendEvent(snapshot, "re_instructing", "PR audit produced a repair instruction.", {
|
|
595
580
|
attempt_number: attemptNumber,
|
|
596
581
|
finding_codes: findings.map((finding) => finding.code),
|
|
@@ -601,6 +586,18 @@ async function handleFailedAudit({
|
|
|
601
586
|
return false;
|
|
602
587
|
}
|
|
603
588
|
|
|
589
|
+
function recordBudgetWarnings(snapshot, attempt) {
|
|
590
|
+
if (snapshot.charter.continuation_policy !== "progress") return;
|
|
591
|
+
snapshot.budget_notifications ??= {};
|
|
592
|
+
for (const kind of ["token", "retry"]) {
|
|
593
|
+
if (!snapshot.budget[`${kind}_warning`] || snapshot.budget_notifications[kind]) continue;
|
|
594
|
+
snapshot.budget_notifications[kind] = { attempt_number: attempt.attempt_number, at: new Date().toISOString() };
|
|
595
|
+
appendEvent(snapshot, "budget_warning", "The continuation notification threshold was reached; acceptance progress and hard ceilings still govern further work.", {
|
|
596
|
+
kind, attempt_number: attempt.attempt_number, budget: snapshot.budget,
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
604
601
|
async function stopForBudget({ snapshot, attempt, reason, onUpdate }) {
|
|
605
602
|
const budget = refreshBudgetState(snapshot);
|
|
606
603
|
const finding = budgetFinding(reason, budget);
|
|
@@ -639,7 +636,7 @@ function budgetFinding(reason, budget) {
|
|
|
639
636
|
code: "TOKEN_BUDGET_EXHAUSTED",
|
|
640
637
|
severity: "blocker",
|
|
641
638
|
target: "provider_token_budget",
|
|
642
|
-
explanation: `The run used ${budget.tokens_used} of ${budget.
|
|
639
|
+
explanation: `The run used ${budget.tokens_used} of ${budget.hard_token_ceiling} Provider tokens. The ${budget.continuation_policy === "progress" ? "hard safety ceiling" : "budget"} prevents another provider call. Preserve this run and review its remaining acceptance conditions before resuming. Provider tokens are not billed as SIMY tokens.`,
|
|
643
640
|
repairability: "manual",
|
|
644
641
|
auto_fix_hint: null,
|
|
645
642
|
};
|
|
@@ -649,7 +646,7 @@ function budgetFinding(reason, budget) {
|
|
|
649
646
|
code: "RETRY_BUDGET_EXHAUSTED",
|
|
650
647
|
severity: "blocker",
|
|
651
648
|
target: "retry_budget",
|
|
652
|
-
explanation: `The run used ${budget.retries_used} of ${budget.retry_budget} automatic retries.`,
|
|
649
|
+
explanation: budget.continuation_policy === "progress" ? `The run reached its hard ceiling of ${budget.hard_attempt_ceiling} attempts. Preserve the checkpoint and review remaining acceptance conditions before resuming.` : `The run used ${budget.retries_used} of ${budget.retry_budget} automatic retries.`,
|
|
653
650
|
repairability: "manual",
|
|
654
651
|
auto_fix_hint: null,
|
|
655
652
|
};
|