@awak-app/simy-cli 0.5.71 → 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 +101 -3
- package/package.json +2 -2
- package/src/agent.js +205 -12
- package/src/backend-executable.js +45 -2
- 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 +180 -31
- 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 +17 -12
- package/src/desktop-host.js +19 -8
- 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 +18 -1
- 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/evidence.js +20 -19
- package/src/orchestrator/independent-audit.js +9 -0
- package/src/orchestrator/instruction.js +6 -1
- package/src/orchestrator/loop.js +66 -41
- package/src/orchestrator/outcome-monitor.js +95 -44
- package/src/orchestrator/outcome-progress.js +2 -0
- package/src/orchestrator/recovery.js +3 -0
- package/src/purpose-local-proof.js +380 -0
- package/src/runner.js +227 -19
- package/src/runtime-diagnostics.js +77 -0
- package/src/session-supervision/codex-app-server.js +3 -2
- package/src/session-supervision/index.js +16 -4
- package/src/sqm/bundle-store.js +14 -7
- package/src/sqm/claude-plugin.js +2 -0
- package/src/sqm/command.js +42 -4
- package/src/sqm/diagnostics.js +23 -0
- package/src/sqm/evidence-client.js +24 -7
- package/src/sqm/history-store.js +21 -5
- package/src/sqm/index.js +116 -17
- package/src/sqm/proof.js +4 -23
- package/src/sqm/report.js +9 -2
- package/src/sqm/request-recovery.js +82 -0
- package/src/sqm/review-worker.js +97 -15
- package/src/sqm/session.js +33 -3
- package/src/startup-preflight.js +44 -0
|
@@ -26,7 +26,7 @@ export async function collectPrEvidence({
|
|
|
26
26
|
const baseRefresh = await command(
|
|
27
27
|
runCommand,
|
|
28
28
|
"git",
|
|
29
|
-
["fetch", "--no-tags", "origin", charter.base_branch],
|
|
29
|
+
["fetch", "--no-tags", "origin", charter.base_branch.replace(/^(?:refs\/remotes\/)?origin\//, "")],
|
|
30
30
|
repositoryPath,
|
|
31
31
|
);
|
|
32
32
|
const local = await collectLocalGitEvidence({
|
|
@@ -42,7 +42,8 @@ export async function collectPrEvidence({
|
|
|
42
42
|
const latest = await collectLocalGitEvidence({ baseBranch: charter.base_branch, repositoryPath, runCommand });
|
|
43
43
|
latest.base_ref_refreshed = baseRefresh.ok;
|
|
44
44
|
latest.base_ref_error = baseRefresh.ok ? null : baseRefresh.error;
|
|
45
|
-
if (!latest.available || latest.head_sha !== local.head_sha ||
|
|
45
|
+
if (!latest.available || latest.head_sha !== local.head_sha || latest.base_sha !== local.base_sha ||
|
|
46
|
+
latest.base_ref !== local.base_ref || !latest.working_tree_clean) {
|
|
46
47
|
minimumCompletion.status = "evidence_incomplete";
|
|
47
48
|
minimumCompletion.errors = ["Checkout changed while collecting minimum evidence; verify again."];
|
|
48
49
|
}
|
|
@@ -90,16 +91,17 @@ function observedEvidenceRef(ref, local, github) {
|
|
|
90
91
|
}
|
|
91
92
|
|
|
92
93
|
async function collectLocalGitEvidence({ baseBranch, repositoryPath, runCommand }) {
|
|
93
|
-
const
|
|
94
|
-
|
|
94
|
+
const base = await resolveBaseRef(runCommand, baseBranch, repositoryPath);
|
|
95
|
+
// Pin all comparisons to the same commit, even if another process moves the ref.
|
|
96
|
+
const baseCommit = base.ok ? base.stdout : null;
|
|
97
|
+
const [head, branch, headline, status, changed, mergeBase, baseAncestor] = await Promise.all([
|
|
95
98
|
command(runCommand, "git", ["rev-parse", "HEAD"], repositoryPath),
|
|
96
99
|
command(runCommand, "git", ["branch", "--show-current"], repositoryPath),
|
|
97
100
|
command(runCommand, "git", ["log", "-1", "--pretty=%s"], repositoryPath),
|
|
98
101
|
command(runCommand, "git", ["status", "--porcelain"], repositoryPath),
|
|
99
|
-
|
|
100
|
-
command(runCommand, "git", ["
|
|
101
|
-
command(runCommand, "git", ["merge-base",
|
|
102
|
-
command(runCommand, "git", ["merge-base", "--is-ancestor", baseRef, "HEAD"], repositoryPath),
|
|
102
|
+
base.ok ? command(runCommand, "git", ["diff", "--name-only", "--diff-filter=ACMR", `${baseCommit}...HEAD`], repositoryPath) : base,
|
|
103
|
+
base.ok ? command(runCommand, "git", ["merge-base", baseCommit, "HEAD"], repositoryPath) : base,
|
|
104
|
+
base.ok ? command(runCommand, "git", ["merge-base", "--is-ancestor", baseCommit, "HEAD"], repositoryPath) : base,
|
|
103
105
|
]);
|
|
104
106
|
const statusLines = lines(status.stdout);
|
|
105
107
|
return {
|
|
@@ -110,7 +112,8 @@ async function collectLocalGitEvidence({ baseBranch, repositoryPath, runCommand
|
|
|
110
112
|
working_tree_clean: status.ok && statusLines.length === 0,
|
|
111
113
|
status_lines: statusLines,
|
|
112
114
|
changed_files: lines(changed.stdout),
|
|
113
|
-
|
|
115
|
+
base_ref: base.ref || null,
|
|
116
|
+
base_sha: baseCommit,
|
|
114
117
|
merge_base_sha: cleanString(mergeBase.stdout) || null,
|
|
115
118
|
base_is_ancestor: baseAncestor.ok,
|
|
116
119
|
errors: [head, branch, headline, status, changed, base, mergeBase]
|
|
@@ -119,18 +122,16 @@ async function collectLocalGitEvidence({ baseBranch, repositoryPath, runCommand
|
|
|
119
122
|
};
|
|
120
123
|
}
|
|
121
124
|
|
|
122
|
-
async function
|
|
123
|
-
const refs =
|
|
125
|
+
async function resolveBaseRef(runCommand, baseBranch, repositoryPath) {
|
|
126
|
+
const refs = baseBranch.startsWith("refs/") ? [baseBranch] :
|
|
127
|
+
baseBranch.startsWith("origin/") ? [`refs/remotes/${baseBranch}`] :
|
|
128
|
+
[`refs/remotes/origin/${baseBranch}`, `refs/heads/${baseBranch}`];
|
|
124
129
|
for (const ref of refs) {
|
|
125
|
-
const result = await command(
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
["diff", "--name-only", "--diff-filter=ACMR", `${ref}...HEAD`],
|
|
129
|
-
repositoryPath,
|
|
130
|
-
);
|
|
131
|
-
if (result.ok) return result;
|
|
130
|
+
const result = await command(runCommand, "git",
|
|
131
|
+
["rev-parse", "--verify", "--end-of-options", `${ref}^{commit}`], repositoryPath);
|
|
132
|
+
if (result.ok) return { ...result, ref };
|
|
132
133
|
}
|
|
133
|
-
return { ok: false, stdout: "", error: `Could not
|
|
134
|
+
return { ok: false, stdout: "", error: `Could not resolve base branch ${baseBranch}.` };
|
|
134
135
|
}
|
|
135
136
|
|
|
136
137
|
async function collectGitHubEvidence({ charter, attempt, repositoryPath, runCommand }) {
|
|
@@ -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);
|
|
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);
|
|
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,
|
|
@@ -555,6 +540,34 @@ async function handleFailedAudit({
|
|
|
555
540
|
});
|
|
556
541
|
return true;
|
|
557
542
|
}
|
|
543
|
+
if (attempt.outcome_progress?.assessment_unavailable) {
|
|
544
|
+
const progress = attempt.outcome_progress;
|
|
545
|
+
snapshot.final_audit = {
|
|
546
|
+
...(snapshot.final_audit || {}),
|
|
547
|
+
passed: false,
|
|
548
|
+
summary: "Outcome progress could not be assessed across consecutive attempts.",
|
|
549
|
+
findings: [...(snapshot.final_audit?.findings || []), {
|
|
550
|
+
passed: false,
|
|
551
|
+
code: "OUTCOME_ASSESSMENT_UNAVAILABLE",
|
|
552
|
+
severity: "blocker",
|
|
553
|
+
target: "outcome_monitor",
|
|
554
|
+
explanation: "Two consecutive attempts could not establish an independent outcome assessment. Resolve the recorded verification failures and continue this run; this does not prove the work has stalled.",
|
|
555
|
+
repairability: "manual",
|
|
556
|
+
}],
|
|
557
|
+
auto_repairable: false,
|
|
558
|
+
requires_human: true,
|
|
559
|
+
};
|
|
560
|
+
appendEvent(snapshot, "waiting_human", "Outcome assessment needs recovery before further automatic work.", {
|
|
561
|
+
code: "outcome_assessment_unavailable",
|
|
562
|
+
finding_codes: ["OUTCOME_ASSESSMENT_UNAVAILABLE"],
|
|
563
|
+
attempt_number: attemptNumber,
|
|
564
|
+
consecutive_unassessed_attempts: progress.consecutive_unassessed_attempts,
|
|
565
|
+
unavailable_reason: progress.unavailable_reason,
|
|
566
|
+
unproven_ids: progress.unproven_ids,
|
|
567
|
+
});
|
|
568
|
+
await publish(snapshot, "waiting_human", onUpdate);
|
|
569
|
+
return true;
|
|
570
|
+
}
|
|
558
571
|
if (requiresHuman) {
|
|
559
572
|
appendEvent(snapshot, "waiting_human", "PR audit requires human input.", {
|
|
560
573
|
finding_codes: findings.map((finding) => finding.code),
|
|
@@ -562,7 +575,7 @@ async function handleFailedAudit({
|
|
|
562
575
|
await publish(snapshot, "waiting_human", onUpdate);
|
|
563
576
|
return true;
|
|
564
577
|
}
|
|
565
|
-
if (attemptNumber < snapshot
|
|
578
|
+
if (attemptNumber < codingAttemptCeiling(snapshot)) {
|
|
566
579
|
appendEvent(snapshot, "re_instructing", "PR audit produced a repair instruction.", {
|
|
567
580
|
attempt_number: attemptNumber,
|
|
568
581
|
finding_codes: findings.map((finding) => finding.code),
|
|
@@ -573,6 +586,18 @@ async function handleFailedAudit({
|
|
|
573
586
|
return false;
|
|
574
587
|
}
|
|
575
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
|
+
|
|
576
601
|
async function stopForBudget({ snapshot, attempt, reason, onUpdate }) {
|
|
577
602
|
const budget = refreshBudgetState(snapshot);
|
|
578
603
|
const finding = budgetFinding(reason, budget);
|
|
@@ -611,7 +636,7 @@ function budgetFinding(reason, budget) {
|
|
|
611
636
|
code: "TOKEN_BUDGET_EXHAUSTED",
|
|
612
637
|
severity: "blocker",
|
|
613
638
|
target: "provider_token_budget",
|
|
614
|
-
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.`,
|
|
615
640
|
repairability: "manual",
|
|
616
641
|
auto_fix_hint: null,
|
|
617
642
|
};
|
|
@@ -621,7 +646,7 @@ function budgetFinding(reason, budget) {
|
|
|
621
646
|
code: "RETRY_BUDGET_EXHAUSTED",
|
|
622
647
|
severity: "blocker",
|
|
623
648
|
target: "retry_budget",
|
|
624
|
-
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.`,
|
|
625
650
|
repairability: "manual",
|
|
626
651
|
auto_fix_hint: null,
|
|
627
652
|
};
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
|
|
3
|
-
import { acceptanceCriterionRows
|
|
3
|
+
import { acceptanceCriterionRows } from "./completion-contract.js";
|
|
4
|
+
import { evaluateOutcomeProgress } from "./outcome-progress.js";
|
|
5
|
+
import { independentAuditEvidenceIdentity } from "./pr-preparation.js";
|
|
4
6
|
|
|
5
7
|
function objective(charter) {
|
|
6
8
|
return {
|
|
@@ -17,17 +19,31 @@ function objective(charter) {
|
|
|
17
19
|
};
|
|
18
20
|
}
|
|
19
21
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
+
const CANONICAL_DIGEST = "sha256-canonical-json-v1";
|
|
23
|
+
|
|
24
|
+
function canonicalValue(value) {
|
|
25
|
+
if (Array.isArray(value)) return value.map(canonicalValue);
|
|
26
|
+
if (value && typeof value === "object") {
|
|
27
|
+
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonicalValue(value[key])]));
|
|
28
|
+
}
|
|
29
|
+
return value;
|
|
22
30
|
}
|
|
23
31
|
|
|
32
|
+
export function objectiveValueDigest(value, algorithm) {
|
|
33
|
+
return createHash("sha256").update(JSON.stringify(
|
|
34
|
+
algorithm === CANONICAL_DIGEST ? canonicalValue(value) : value,
|
|
35
|
+
)).digest("hex");
|
|
36
|
+
}
|
|
37
|
+
const digest = objectiveValueDigest;
|
|
38
|
+
|
|
24
39
|
export function freezeObjective(charter) {
|
|
25
40
|
const value = objective(charter);
|
|
26
41
|
charter.thread_state.objective_contract = {
|
|
27
42
|
version: 1,
|
|
43
|
+
digest_algorithm: CANONICAL_DIGEST,
|
|
28
44
|
baseline: structuredClone(value),
|
|
29
45
|
current: structuredClone(value),
|
|
30
|
-
current_digest: digest(value),
|
|
46
|
+
current_digest: digest(value, CANONICAL_DIGEST),
|
|
31
47
|
revisions: [],
|
|
32
48
|
};
|
|
33
49
|
}
|
|
@@ -36,7 +52,7 @@ export function reviseObjective(charter, source) {
|
|
|
36
52
|
const contract = charter.thread_state.objective_contract;
|
|
37
53
|
if (!contract) return;
|
|
38
54
|
const next = objective(charter);
|
|
39
|
-
const nextDigest = digest(next);
|
|
55
|
+
const nextDigest = digest(next, contract.digest_algorithm);
|
|
40
56
|
if (nextDigest === contract.current_digest) return;
|
|
41
57
|
contract.revisions.push({
|
|
42
58
|
version: contract.version + 1,
|
|
@@ -55,6 +71,10 @@ export function verifyObjective(charter) {
|
|
|
55
71
|
const contract = charter.thread_state?.objective_contract;
|
|
56
72
|
if (!contract) return { valid: true, legacy: true };
|
|
57
73
|
try {
|
|
74
|
+
if (contract.digest_algorithm && contract.digest_algorithm !== CANONICAL_DIGEST) {
|
|
75
|
+
return { valid: false, legacy: false, digest: null };
|
|
76
|
+
}
|
|
77
|
+
const contractDigest = (value) => digest(value, contract.digest_algorithm);
|
|
58
78
|
const current = objective(charter);
|
|
59
79
|
const revisions = contract.revisions;
|
|
60
80
|
const valid = Boolean(
|
|
@@ -62,15 +82,15 @@ export function verifyObjective(charter) {
|
|
|
62
82
|
contract.baseline &&
|
|
63
83
|
Array.isArray(revisions) &&
|
|
64
84
|
revisions.length === contract.version - 1 &&
|
|
65
|
-
|
|
66
|
-
|
|
85
|
+
contractDigest(contract.current) === contract.current_digest &&
|
|
86
|
+
contractDigest(current) === contract.current_digest &&
|
|
67
87
|
revisions.every((revision, index) =>
|
|
68
88
|
revision.version === index + 2 &&
|
|
69
|
-
revision.previous_digest ===
|
|
70
|
-
revision.next_digest ===
|
|
71
|
-
(index === 0 ? revision.previous_digest ===
|
|
89
|
+
revision.previous_digest === contractDigest(revision.previous) &&
|
|
90
|
+
revision.next_digest === contractDigest(revision.next) &&
|
|
91
|
+
(index === 0 ? revision.previous_digest === contractDigest(contract.baseline) :
|
|
72
92
|
revision.previous_digest === revisions[index - 1].next_digest)) &&
|
|
73
|
-
(revisions.length === 0 ?
|
|
93
|
+
(revisions.length === 0 ? contractDigest(contract.baseline) === contract.current_digest :
|
|
74
94
|
revisions.at(-1).next_digest === contract.current_digest)
|
|
75
95
|
);
|
|
76
96
|
return { valid, legacy: false, digest: contract.current_digest };
|
|
@@ -81,43 +101,74 @@ export function verifyObjective(charter) {
|
|
|
81
101
|
|
|
82
102
|
// The monitor reads independent criterion assessments, not executor test counts.
|
|
83
103
|
// It records insufficient evidence when the independent review was not reached.
|
|
84
|
-
export function assessOutcomeProgress(charter, attempt, previousAttempt = null) {
|
|
85
|
-
const criteria = acceptanceCriterionRows(charter);
|
|
104
|
+
export function assessOutcomeProgress(charter, attempt, previousAttempt = null, options = {}) {
|
|
86
105
|
const assessment = attempt.independent_audit;
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
106
|
+
const objectiveState = verifyObjective(charter);
|
|
107
|
+
const contract = charter.thread_state?.objective_contract;
|
|
108
|
+
const objectiveDigest = contract?.current_digest || null;
|
|
109
|
+
const boundObjective = assessment?.objective_digest || attempt.outcome_progress?.objective_digest;
|
|
110
|
+
let assessmentReason = null;
|
|
111
|
+
if (!objectiveState.valid) assessmentReason = "objective_contract_invalid";
|
|
112
|
+
else if (boundObjective && boundObjective !== objectiveDigest ||
|
|
113
|
+
assessment?.objective_version != null && assessment.objective_version !== contract?.version) {
|
|
114
|
+
assessmentReason = "independent_objective_stale";
|
|
115
|
+
} else if (assessment?.objective_digest && !assessment.evidence_identity_sha256) {
|
|
116
|
+
assessmentReason = "independent_candidate_unbound";
|
|
117
|
+
} else if (assessment?.evidence_identity_sha256 &&
|
|
118
|
+
assessment.evidence_identity_sha256 !== independentAuditEvidenceIdentity(attempt)) {
|
|
119
|
+
assessmentReason = "independent_candidate_stale";
|
|
120
|
+
} else if (!assessment || (assessment.findings || []).some((finding) =>
|
|
121
|
+
finding.code === "INDEPENDENT_AUDIT_UNAVAILABLE")) {
|
|
122
|
+
assessmentReason = "independent_assessment_unavailable";
|
|
101
123
|
}
|
|
102
|
-
|
|
103
|
-
|
|
124
|
+
const result = evaluateOutcomeProgress({
|
|
125
|
+
criteria: acceptanceCriterionRows(charter),
|
|
126
|
+
objectiveDigest,
|
|
127
|
+
objectiveVersion: contract?.version || null,
|
|
128
|
+
criterionResults: assessment?.criterion_results,
|
|
129
|
+
userOutcome: assessment?.coverage_review?.user_outcome,
|
|
130
|
+
assessmentAvailable: assessmentReason === null,
|
|
131
|
+
assessmentReason,
|
|
132
|
+
findings: [...(attempt.audit?.findings || []), ...(assessment?.findings || [])],
|
|
133
|
+
previous: previousAttempt?.outcome_progress || null,
|
|
134
|
+
maxNoProgressAttempts: options.maxNoProgressAttempts ?? 3,
|
|
135
|
+
});
|
|
136
|
+
// Existing runs keep their original stopping policy until the controller opts
|
|
137
|
+
// into progress continuation. The richer measurement is persisted in both.
|
|
138
|
+
if (options.policy !== "progress") {
|
|
139
|
+
const criteria = acceptanceCriterionRows(charter);
|
|
140
|
+
const rows = Array.isArray(assessment?.criterion_results) ? assessment.criterion_results : [];
|
|
141
|
+
const completeAssessment = assessmentReason === null && attempt.audit?.passed === true &&
|
|
142
|
+
rows.length === criteria.length && criteria.every((criterion) =>
|
|
143
|
+
rows.filter((row) => row.criterion_id === criterion.id).length === 1);
|
|
144
|
+
const previous = previousAttempt?.outcome_progress;
|
|
145
|
+
result.status = completeAssessment ? "assessed" : "insufficient_evidence";
|
|
146
|
+
result.stalled = Boolean(completeAssessment && previous?.status === "assessed" &&
|
|
147
|
+
previous.objective_digest === result.objective_digest && result.unproven_ids.length > 0 &&
|
|
148
|
+
result.assessed_ids.every((id) => previous.assessed_ids.includes(id)) &&
|
|
149
|
+
result.unproven_ids.every((id) => previous.unproven_ids.includes(id)));
|
|
150
|
+
result.stall_reason = result.stalled ? "no_new_verified_outcome" : null;
|
|
151
|
+
result.next_actions = result.next_actions.filter((action) => action !== "reassess_approach");
|
|
152
|
+
if (result.stalled) result.next_actions.push("reassess_approach");
|
|
153
|
+
result.next_action = result.stalled ? "reassess_approach" : result.next_actions[0] || "repair_or_verify";
|
|
104
154
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
status: completeAssessment ? "assessed" : "insufficient_evidence",
|
|
108
|
-
assessed_ids: [...reviewed].sort(),
|
|
109
|
-
unproven_ids: ["USER-GOAL", ...criteria.map((row) => row.id)].filter((id) => !reviewed.has(id)),
|
|
110
|
-
evidence_identity: assessment?.evidence_identity_sha256 || null,
|
|
111
|
-
stalled: false,
|
|
112
|
-
};
|
|
155
|
+
result.evidence_identity = assessment?.evidence_identity_sha256 || null;
|
|
156
|
+
const completeAssessment = result.status === "assessed";
|
|
113
157
|
const previous = previousAttempt?.outcome_progress;
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
previous.
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
158
|
+
const sameObjective = previous?.objective_digest === result.objective_digest;
|
|
159
|
+
const previousUnavailableCount = !options.resetUnavailableCount && sameObjective && previous?.status === "insufficient_evidence"
|
|
160
|
+
? previous.consecutive_unassessed_attempts || 1 : 0;
|
|
161
|
+
result.consecutive_unassessed_attempts = completeAssessment ? 0 : previousUnavailableCount + 1;
|
|
162
|
+
result.unavailable_reason = completeAssessment ? null : attempt.audit?.passed !== true
|
|
163
|
+
? "local_verification_incomplete" : "independent_assessment_incomplete";
|
|
164
|
+
const previousMissingAssessments = !options.resetUnavailableCount && sameObjective &&
|
|
165
|
+
previous?.unavailable_reason === "independent_assessment_incomplete"
|
|
166
|
+
? previous.consecutive_unavailable_assessments || 1 : 0;
|
|
167
|
+
result.consecutive_unavailable_assessments = result.unavailable_reason === "independent_assessment_incomplete"
|
|
168
|
+
? previousMissingAssessments + 1 : 0;
|
|
169
|
+
// Local repair can improve without reaching independent review. Only a repeatedly
|
|
170
|
+
// missing independent assessment after local readiness opens this recovery gate.
|
|
171
|
+
result.assessment_unavailable = options.policy !== "progress" && result.consecutive_unavailable_assessments >= 2;
|
|
121
172
|
attempt.outcome_progress = result;
|
|
122
173
|
return result;
|
|
123
174
|
}
|
|
@@ -114,6 +114,7 @@ function recoveryReasonCode(state, message, detail) {
|
|
|
114
114
|
if (code === "retry_circuit_open" || detail.reason === "identical_commit_and_findings") {
|
|
115
115
|
return "retry_strategy_unchanged";
|
|
116
116
|
}
|
|
117
|
+
if (code === "outcome_assessment_unavailable") return "outcome_assessment_unavailable";
|
|
117
118
|
if (code === "token_budget_exhausted") return "token_budget_exhausted";
|
|
118
119
|
if (code === "retry_budget_exhausted") return "retry_budget_exhausted";
|
|
119
120
|
const findings = strings(detail.finding_codes);
|
|
@@ -148,6 +149,8 @@ function recoveryMessage(reasonCode) {
|
|
|
148
149
|
requirements_incomplete:
|
|
149
150
|
"SIMY needs the missing requirement or approval before implementation can start.",
|
|
150
151
|
human_input_required: "SIMY needs your decision or clarification before it can continue.",
|
|
152
|
+
outcome_assessment_unavailable:
|
|
153
|
+
"SIMY could not assess outcome progress across consecutive attempts. Repair the recorded verification failure and continue this run.",
|
|
151
154
|
token_budget_exhausted:
|
|
152
155
|
"This run reached its Provider token safety limit. Provider tokens are not billed by SIMY. Increase the limit to continue this run.",
|
|
153
156
|
retry_budget_exhausted:
|