@awak-app/simy-cli 0.6.9 → 0.6.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -0
- package/package.json +1 -1
- package/src/agent.js +29 -2
- package/src/cli-contract.js +1 -1
- package/src/codex-plugins/simy/hooks/delivery-loop.mjs +110 -30
- package/src/codex-plugins/simy/hooks/workspace-lease.mjs +32 -5
- package/src/codex-plugins/simy/skills/delivery-loop/SKILL.md +4 -2
- package/src/coding-run-recovery.js +44 -0
- package/src/coding-run-store.js +9 -2
- package/src/delivery-hooks/command.js +5 -5
- package/src/delivery-hooks/desktop.js +5 -2
- package/src/delivery-hooks/supervisor.js +4 -3
- package/src/orchestrator/evidence.js +10 -2
- package/src/orchestrator/loop.js +99 -44
- package/src/orchestrator/risk.js +17 -2
- package/src/runner.js +63 -27
- package/src/session-supervision/analysis-manager.js +36 -9
- package/src/session-supervision/codex-app-server.js +73 -8
- package/src/session-supervision/continuation-analysis.js +49 -12
- package/src/session-supervision/conversation-context.js +7 -0
- package/src/session-supervision/index.js +71 -17
- package/src/session-supervision/statistics.js +138 -60
- package/src/sqm/failure-circuit.js +105 -0
- package/src/sqm/provider-diagnostics.js +1 -1
- package/src/sqm/review-worker.js +20 -5
|
@@ -6,6 +6,11 @@ import { objectiveValueDigest, verifyObjective } from "./orchestrator/outcome-mo
|
|
|
6
6
|
export function recoverySnapshot(snapshot) {
|
|
7
7
|
const verified = snapshot?.charter && verifyObjective(snapshot.charter);
|
|
8
8
|
const sanitized = redactExecutionValue(snapshot);
|
|
9
|
+
const receipt = snapshot?.metadata?.completed_worker_result;
|
|
10
|
+
if (receipt && JSON.stringify(receipt.attempt?.raw_output) !==
|
|
11
|
+
JSON.stringify(sanitized.metadata?.completed_worker_result?.attempt?.raw_output)) {
|
|
12
|
+
sanitized.metadata.completed_worker_result.result_redacted = true;
|
|
13
|
+
}
|
|
9
14
|
const contract = sanitized?.charter?.thread_state?.objective_contract;
|
|
10
15
|
const objectiveDigest = value => objectiveValueDigest(value, contract?.digest_algorithm);
|
|
11
16
|
if (!contract || verified.legacy || !verified.valid) return sanitized;
|
|
@@ -28,6 +33,45 @@ export function recoverySnapshot(snapshot) {
|
|
|
28
33
|
return sanitized;
|
|
29
34
|
}
|
|
30
35
|
|
|
36
|
+
// A terminal provider result is a receipt, not acceptance evidence. It only
|
|
37
|
+
// authorizes re-collecting and auditing this same attempt without replaying it.
|
|
38
|
+
export function completedWorkerResult(snapshot) {
|
|
39
|
+
const receipt = snapshot?.metadata?.completed_worker_result;
|
|
40
|
+
if (!receipt) return null;
|
|
41
|
+
const objective = verifyObjective(snapshot.charter);
|
|
42
|
+
const attempt = receipt.attempt;
|
|
43
|
+
const previous = snapshot.attempts?.filter(item => item.attempt_number !== attempt?.attempt_number) || [];
|
|
44
|
+
if (receipt.version !== 1 || receipt.run_id !== snapshot.id || !receipt.invocation_id ||
|
|
45
|
+
!objective.valid || objective.legacy || receipt.objective_digest !== objective.digest ||
|
|
46
|
+
receipt.result_redacted || receipt.persistence_failed || !Number.isInteger(attempt?.attempt_number) ||
|
|
47
|
+
attempt.attempt_number !== previous.length + 1 || attempt.status !== "completed" ||
|
|
48
|
+
!attempt.raw_output || typeof attempt.raw_output !== "object" || Array.isArray(attempt.raw_output) ||
|
|
49
|
+
!Object.keys(attempt.raw_output).length) return null;
|
|
50
|
+
return receipt;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Remote ledgers deliberately omit the private result. A later echo must not
|
|
54
|
+
// erase that receipt, while a revised objective or a later attempt supersedes it.
|
|
55
|
+
export function preserveCompletedWorkerResult(local, remote) {
|
|
56
|
+
const receipt = completedWorkerResult(local);
|
|
57
|
+
if (!receipt) return remote;
|
|
58
|
+
const remoteState = remote.metadata?.pr_lifecycle_state || remote.state;
|
|
59
|
+
const unconsumedStates = ["dispatching", "coding", "collecting_evidence", "auditing", "independent_auditing"];
|
|
60
|
+
if (!unconsumedStates.includes(remoteState) &&
|
|
61
|
+
!(remoteState === "failed" && remote.metadata?.controller_result_recovery)) return remote;
|
|
62
|
+
const remoteAttempt = remote.attempts?.find(item => item.attempt_number === receipt.attempt.attempt_number);
|
|
63
|
+
if (remoteAttempt?.audit?.passed === false || remoteAttempt?.independent_audit || remoteAttempt?.pr_readiness ||
|
|
64
|
+
(remoteAttempt?.commit_sha && remoteAttempt.commit_sha !== receipt.attempt.commit_sha)) return remote;
|
|
65
|
+
const merged = structuredClone(remote);
|
|
66
|
+
merged.metadata ??= {};
|
|
67
|
+
merged.metadata.completed_worker_result = structuredClone(receipt);
|
|
68
|
+
merged.metadata.recovery_actor = structuredClone(local.metadata.recovery_actor);
|
|
69
|
+
if (local.metadata.execution_device_id) merged.metadata.execution_device_id = local.metadata.execution_device_id;
|
|
70
|
+
if (!completedWorkerResult(merged)) delete merged.metadata.completed_worker_result;
|
|
71
|
+
if (local.metadata.local_stop_requested) merged.metadata.local_stop_requested = true;
|
|
72
|
+
return merged;
|
|
73
|
+
}
|
|
74
|
+
|
|
31
75
|
export function recoveryActorBinding(session, apiOrigin, request) {
|
|
32
76
|
return {
|
|
33
77
|
device_id: session?.device_id || request?.execution_device_id || null,
|
package/src/coding-run-store.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
|
-
import { mkdir, readFile, readdir, rename
|
|
3
|
+
import { mkdir, open, readFile, readdir, rename } from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
|
|
6
6
|
import { recoverySnapshot } from "./coding-run-recovery.js";
|
|
@@ -30,8 +30,15 @@ export async function saveCodingRunSnapshot(root, snapshot) {
|
|
|
30
30
|
});
|
|
31
31
|
const target = `${key}.json`;
|
|
32
32
|
const temporary = `${target}.${process.pid}.${randomUUID()}.tmp`;
|
|
33
|
-
await
|
|
33
|
+
const handle = await open(temporary, "wx", 0o600);
|
|
34
|
+
try { await handle.writeFile(`${document}\n`); await handle.sync(); }
|
|
35
|
+
finally { await handle.close(); }
|
|
34
36
|
await rename(temporary, target);
|
|
37
|
+
// Persist the directory entry too on platforms that support directory fsync.
|
|
38
|
+
if (process.platform !== "win32") {
|
|
39
|
+
const directoryHandle = await open(directory, "r");
|
|
40
|
+
try { await directoryHandle.sync(); } finally { await directoryHandle.close(); }
|
|
41
|
+
}
|
|
35
42
|
});
|
|
36
43
|
writes.set(key, current);
|
|
37
44
|
try { await current; } finally {
|
|
@@ -23,12 +23,12 @@ export async function runDeliveryCommand(argv, { root = rootDirectory(), cwd = p
|
|
|
23
23
|
output('simy delivery activity [--watch] [--json] [--provider codex|claude] [--session ID] — local observations every 60 seconds; not productive time.');
|
|
24
24
|
output('A task stops after repeated failures, and pauses for review when no progress is confirmed. Resume starts a new progress window.');
|
|
25
25
|
output('simy delivery continuation_policy --provider PROVIDER --session ID --task-id ID --continuation-policy bounded|progress');
|
|
26
|
-
output('simy delivery continue --provider codex|claude --session ID [--max-no-progress-minutes 30] [--max-failures 3]\nsimy delivery dependency_ready --provider PROVIDER --session ID --task-id ID --wait-id ID --answer TEXT\nsimy delivery handoff|handoff_cancel --provider codex --session ID --task-id ID --target-session ID');
|
|
27
|
-
output('simy delivery start --provider codex|claude --session ID --objective TEXT --next-step TEXT [--managed-file task.json] [--max-failures 3]\nsimy delivery checkpoint --provider codex|claude --session ID --file checkpoint.json|-\nsimy delivery status\nsimy delivery stop|pause|resume --provider codex|claude --session ID [--task-id ID]\nsimy delivery replan --provider PROVIDER --session ID --task-id ID\nsimy delivery approve|reject --provider PROVIDER --session ID --task-id ID --approval-id ID\nsimy delivery verify --provider PROVIDER --session ID --task-id ID --criterion-id ID\nsimy delivery set_policy --provider PROVIDER --session ID --task-id ID --autonomy-policy strict|balanced|autonomous\nEnable and review SIMY hooks in your provider first.
|
|
26
|
+
output('simy delivery continue --provider codex|claude --session ID [--execution-cwd PATH] [--max-no-progress-minutes 30] [--max-failures 3]\nsimy delivery dependency_ready --provider PROVIDER --session ID --task-id ID --wait-id ID --answer TEXT\nsimy delivery handoff|handoff_cancel --provider codex --session ID --task-id ID --target-session ID');
|
|
27
|
+
output('simy delivery start --provider codex|claude --session ID --objective TEXT --next-step TEXT [--managed-file task.json] [--execution-cwd PATH] [--max-failures 3]\nsimy delivery checkpoint --provider codex|claude --session ID --file checkpoint.json|-\nsimy delivery status\nsimy delivery stop|pause|resume --provider codex|claude --session ID [--task-id ID]\nsimy delivery replan --provider PROVIDER --session ID --task-id ID\nsimy delivery approve|reject --provider PROVIDER --session ID --task-id ID --approval-id ID\nsimy delivery verify --provider PROVIDER --session ID --task-id ID --criterion-id ID\nsimy delivery set_policy --provider PROVIDER --session ID --task-id ID --autonomy-policy strict|balanced|autonomous\nEnable and review SIMY hooks in your provider first. Standard start tracks checkpoints and reports done without certifying acceptance. Managed start freezes the original objective, optional user-selected deliveryLevel, and acceptance specification. Desktop Continue infers the original conversation goal first. Continue adopts an eligible ordinary loop with the same goal and failure budget; its first managed action reconciles prior work read-only. --execution-cwd explicitly binds the task directory while provider identity retains its original folder. Resume preserves original task identity, history and approvals.');
|
|
28
28
|
return 0;
|
|
29
29
|
}
|
|
30
30
|
const options = {};
|
|
31
|
-
const allowed = new Set(['provider', 'session', 'objective', 'next-step', 'max-failures', 'max-no-progress-minutes', 'continuation-policy', 'file', 'managed-file', 'task-id', 'approval-id', 'criterion-id', 'autonomy-policy', 'wait-id', 'answer', 'target-session']);
|
|
31
|
+
const allowed = new Set(['provider', 'session', 'objective', 'next-step', 'max-failures', 'max-no-progress-minutes', 'continuation-policy', 'file', 'managed-file', 'execution-cwd', 'task-id', 'approval-id', 'criterion-id', 'autonomy-policy', 'wait-id', 'answer', 'target-session']);
|
|
32
32
|
for (let i = 0; i < args.length; i += 2) {
|
|
33
33
|
const key = args[i]?.replace(/^--/, '');
|
|
34
34
|
if (!args[i]?.startsWith('--') || !allowed.has(key) || args[i + 1] === undefined || Object.hasOwn(options, key)) throw new Error('Invalid delivery option. Use simy delivery --help.');
|
|
@@ -37,7 +37,7 @@ export async function runDeliveryCommand(argv, { root = rootDirectory(), cwd = p
|
|
|
37
37
|
const base = { root, cwd, provider: options.provider, session: options.session };
|
|
38
38
|
let result;
|
|
39
39
|
if (action === 'status') result = (await listRecords(root)).filter(r => (!options.provider || r.provider === options.provider) && (!options.session || r.session === options.session));
|
|
40
|
-
else if (action === 'continue') result = await continueSession({ ...base, autonomyPolicy: options['autonomy-policy'], continuationPolicy: options['continuation-policy'],
|
|
40
|
+
else if (action === 'continue') result = await continueSession({ ...base, executionCwd: options['execution-cwd'], autonomyPolicy: options['autonomy-policy'], continuationPolicy: options['continuation-policy'],
|
|
41
41
|
...Object.fromEntries([['max-failures', 'maxFailures'], ['max-no-progress-minutes', 'maxNoProgressMinutes']]
|
|
42
42
|
.filter(([key]) => options[key] !== undefined).map(([key, field]) => [field, Number(options[key])])) });
|
|
43
43
|
else if (action === 'handoff') result = await handoffSession({ ...base, taskId: options['task-id'], targetSession: options['target-session'] });
|
|
@@ -53,7 +53,7 @@ export async function runDeliveryCommand(argv, { root = rootDirectory(), cwd = p
|
|
|
53
53
|
if (!managed || typeof managed !== 'object') throw new Error('--max-no-progress-minutes requires --managed-file; use delivery continue for context-first supervision.');
|
|
54
54
|
managed = { ...managed, maxNoProgressMinutes: Number(options['max-no-progress-minutes']) };
|
|
55
55
|
}
|
|
56
|
-
result = await startDelivery({ ...base, objective: options.objective, nextStep: options['next-step'], managed, continuationPolicy: options['continuation-policy'],
|
|
56
|
+
result = await startDelivery({ ...base, executionCwd: options['execution-cwd'], objective: options.objective, nextStep: options['next-step'], managed, continuationPolicy: options['continuation-policy'],
|
|
57
57
|
...(options['max-failures'] === undefined ? {} : { maxFailures: Number(options['max-failures']) }) });
|
|
58
58
|
}
|
|
59
59
|
else if (action === 'checkpoint') {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
|
-
import { canReplanDelivery, deliveryStop, deliveryStopBlocksResume, reconcileRecords, rootDirectory } from '../codex-plugins/simy/hooks/delivery-loop.mjs';
|
|
3
|
+
import { canReplanDelivery, pendingAcceptance, deliveryStop, deliveryStopBlocksResume, reconcileRecords, rootDirectory } from '../codex-plugins/simy/hooks/delivery-loop.mjs';
|
|
4
4
|
import { originalSessionTarget } from '../session-supervision/original-session-target.js';
|
|
5
5
|
|
|
6
6
|
export const HOOK_SETUP = Object.freeze({
|
|
@@ -28,7 +28,7 @@ export async function deliveryHookStatus({ root = rootDirectory(), now = Date.no
|
|
|
28
28
|
const latest = records.filter(r => r.provider === provider && Number.isFinite(r.observedAt)).sort((a, b) => b.observedAt - a.observedAt)[0];
|
|
29
29
|
return [provider, { ...HOOK_SETUP[provider], state: latest ? 'observed' : 'not_observed', lastObservedAt: latest?.observedAt || null }];
|
|
30
30
|
})),
|
|
31
|
-
runs: records.filter(r => r.run).sort((a, b) => b.run.updatedAt - a.run.updatedAt).slice(0, 30).map(r => ({
|
|
31
|
+
runs: records.filter(r => r.run && r.providerArchived !== true).sort((a, b) => b.run.updatedAt - a.run.updatedAt).slice(0, 30).map(r => ({
|
|
32
32
|
provider: r.provider, session: r.session, originalSessionTarget: originalSessionTarget(r.provider, r.session),
|
|
33
33
|
objective: r.run.objective, state: r.run.state, reason: r.run.reason,
|
|
34
34
|
continuations: r.run.continuations,
|
|
@@ -37,6 +37,9 @@ export async function deliveryHookStatus({ root = rootDirectory(), now = Date.no
|
|
|
37
37
|
stopCode: deliveryStop(r.run, now)?.code ?? null,
|
|
38
38
|
outcomeProgress: r.run.managed?.outcomeProgress || null,
|
|
39
39
|
needsAttention: ['blocked', 'failed', 'recovery_required', 'budget_exhausted', 'verification_pending'].includes(r.run.state), updatedAt: r.run.updatedAt,
|
|
40
|
+
mode: r.run.managed ? 'managed' : 'standard_loop',
|
|
41
|
+
deliveryLevel: r.run.managed?.deliveryLevel || null, enrollment: r.run.managed?.enrollment || null,
|
|
42
|
+
pendingAcceptance: pendingAcceptance(r.run),
|
|
40
43
|
managed: Boolean(r.run.managed), taskId: r.run.managed?.taskId || null, actionId: r.run.managed?.actionId || null,
|
|
41
44
|
actionDueAt: r.run.managed?.actionDueAt ?? null,
|
|
42
45
|
actionAssociation: associationStatus(r.run.managed?.actionAssociation),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { dirname } from 'node:path';
|
|
4
|
-
import { automaticContinuationAllowed, assessedCheckpointContract, checkpointDelivery, decideStop, deliveryProgressContext, deliveryStop, listRecords, readRecord, managedBudgetReason, notifyState, processIdentity, reconcileRecords, updateRecord, verifyAutomaticCriteria } from '../codex-plugins/simy/hooks/delivery-loop.mjs';
|
|
4
|
+
import { automaticContinuationAllowed, assessedCheckpointContract, checkpointDelivery, decideStop, deliveryProgressContext, deliveryStop, listRecords, readRecord, managedBudgetReason, notifyState, processIdentity, sharesTaskWorkspace, taskDirectory, reconcileRecords, updateRecord, verifyAutomaticCriteria } from '../codex-plugins/simy/hooks/delivery-loop.mjs';
|
|
5
5
|
import { resolveBackendExecutable } from '../backend-executable.js';
|
|
6
6
|
import { executeCodexAppServer } from '../session-supervision/codex-app-server.js';
|
|
7
7
|
import { acquireWorkspaceLease, leaseOwnerDead } from '../session-supervision/workspace-lease.js';
|
|
@@ -75,8 +75,9 @@ export async function executeCodexResume(record, { signal, root, leaseId, execut
|
|
|
75
75
|
if (!binary) throw new Error('Codex executable unavailable.');
|
|
76
76
|
await assertActiveLease(root, record.provider, record.session, leaseId, signal, currentTime);
|
|
77
77
|
const run = record.run;
|
|
78
|
-
const context = { taskId: run.managed.taskId, session: record.session, objective: run.objective, nextStep: run.nextStep,
|
|
78
|
+
const context = { taskId: run.managed.taskId, session: record.session, sessionCwd: record.cwd, executionCwd: taskDirectory(record), objective: run.objective, nextStep: run.nextStep,
|
|
79
79
|
lastDependencyResolution: run.managed.lastDependencyResolution,
|
|
80
|
+
...(run.managed.upgradedFrom ? { adoptedLoop: run.managed.upgradedFrom, priorCheckpoint: run.checkpoint } : {}),
|
|
80
81
|
remainingWork: run.managed.remainingWork, acceptanceCriteria: run.managed.acceptanceCriteria, proposedAction: run.managed.proposedAction,
|
|
81
82
|
currentDecision: run.managed.autonomyPolicy ? run.managed.decision : undefined, diagnosis: run.managed.plan?.diagnosis,
|
|
82
83
|
progress: deliveryProgressContext(run) };
|
|
@@ -252,7 +253,7 @@ export async function tickManagedTasks({ root, now = Date.now(), execute, inspec
|
|
|
252
253
|
result = { code: -3, busy: true };
|
|
253
254
|
} else if (resource.busy) result = { code: -3, busy: true };
|
|
254
255
|
else if (['local_edit', 'local_test'].includes(claimed.run.managed.decision?.action.kind) &&
|
|
255
|
-
(await listRecords(root)).some(other => other
|
|
256
|
+
(await listRecords(root)).some(other => sharesTaskWorkspace(other, claimed) && other.run?.managed?.executionUncertain)) result = { code: -3, busy: true };
|
|
256
257
|
if (!result) {
|
|
257
258
|
await authorizeClaim();
|
|
258
259
|
const runner = execute || (appServer ? executeCodexAppServer : executeCodexResume);
|
|
@@ -47,7 +47,15 @@ export async function collectPrEvidence({
|
|
|
47
47
|
minimumCompletion.status = "evidence_incomplete";
|
|
48
48
|
minimumCompletion.errors = ["Checkout changed while collecting minimum evidence; verify again."];
|
|
49
49
|
}
|
|
50
|
+
const collectionErrors = [
|
|
51
|
+
...(!baseRefresh.ok ? [baseRefresh.error] : []),
|
|
52
|
+
...(!latest.available ? latest.errors : []),
|
|
53
|
+
// No reported PR is an implementation gap; an unreadable reported PR is
|
|
54
|
+
// a collector failure. Never send the worker to repair a failed gh read.
|
|
55
|
+
...(github.collection_error ? [github.collection_error] : []),
|
|
56
|
+
].filter(Boolean);
|
|
50
57
|
return {
|
|
58
|
+
...(collectionErrors.length ? { collection_error: collectionErrors.join("; ") } : {}),
|
|
51
59
|
minimum_completion: minimumCompletion,
|
|
52
60
|
collected_at: new Date().toISOString(),
|
|
53
61
|
local: latest,
|
|
@@ -160,13 +168,13 @@ async function collectGitHubEvidence({ charter, attempt, repositoryPath, runComm
|
|
|
160
168
|
["pr", "view", target, "--json", fields],
|
|
161
169
|
repositoryPath,
|
|
162
170
|
);
|
|
163
|
-
if (!result.ok) return unavailable(result.error);
|
|
171
|
+
if (!result.ok) return { ...unavailable(result.error), collection_error: result.error };
|
|
164
172
|
|
|
165
173
|
let payload;
|
|
166
174
|
try {
|
|
167
175
|
payload = JSON.parse(result.stdout);
|
|
168
176
|
} catch {
|
|
169
|
-
return unavailable("GitHub PR evidence was not valid JSON.");
|
|
177
|
+
return { ...unavailable("GitHub PR evidence was not valid JSON."), collection_error: "GitHub PR evidence was not valid JSON." };
|
|
170
178
|
}
|
|
171
179
|
const checks = normalizeChecks(payload.statusCheckRollup);
|
|
172
180
|
const requiredChecks = stringArray(charter.required_checks);
|
package/src/orchestrator/loop.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { completedWorkerResult } from "../coding-run-recovery.js";
|
|
1
3
|
import { validateCharter } from "./charter-validation.js";
|
|
2
4
|
import { redactExecutionText } from "./execution-io.js";
|
|
3
5
|
import { minimumCharter } from "./delivery/scripts/check-minimum-completion.mjs";
|
|
@@ -26,7 +28,7 @@ async function runCodingLoopOwned({
|
|
|
26
28
|
shouldStop = () => false,
|
|
27
29
|
consumeHumanGuidance = () => "",
|
|
28
30
|
}) {
|
|
29
|
-
if (!resume && snapshot.attempts.length > 0) {
|
|
31
|
+
if (!resume && snapshot.attempts.length > 0 && !snapshot.metadata?.completed_worker_result) {
|
|
30
32
|
return recheckPrReadinessOwned({ snapshot, collectEvidence, checkSqm, onUpdate });
|
|
31
33
|
}
|
|
32
34
|
snapshot.implementation_gate_passed = false;
|
|
@@ -76,7 +78,16 @@ async function runCodingLoopOwned({
|
|
|
76
78
|
}
|
|
77
79
|
|
|
78
80
|
let previousFindings = resume ? snapshot.final_audit?.findings || [] : [];
|
|
79
|
-
const
|
|
81
|
+
const receipt = completedWorkerResult(snapshot);
|
|
82
|
+
if (snapshot.metadata?.completed_worker_result && !receipt) {
|
|
83
|
+
snapshot.final_audit = humanGate("The saved worker result cannot be safely reused.", [{
|
|
84
|
+
code: "WORKER_RESULT_UNVERIFIED", severity: "blocker", target: "worker_result",
|
|
85
|
+
explanation: "Reconcile the saved result and objective before continuing; do not replay unknown external effects.", repairability: "manual",
|
|
86
|
+
}]);
|
|
87
|
+
await publish(snapshot, "waiting_human", onUpdate);
|
|
88
|
+
return snapshot;
|
|
89
|
+
}
|
|
90
|
+
const firstAttempt = receipt?.attempt.attempt_number ?? (resume ? snapshot.attempts.length + 1 : 1);
|
|
80
91
|
const lastAttempt = codingAttemptCeiling(snapshot);
|
|
81
92
|
for (
|
|
82
93
|
let attemptNumber = firstAttempt;
|
|
@@ -88,7 +99,8 @@ async function runCodingLoopOwned({
|
|
|
88
99
|
attemptNumber,
|
|
89
100
|
phase: "executor",
|
|
90
101
|
});
|
|
91
|
-
|
|
102
|
+
const recoveredAttempt = attemptNumber === firstAttempt ? receipt?.attempt : null;
|
|
103
|
+
if (!recoveredAttempt && !executorBudget.allowed) {
|
|
92
104
|
await stopForBudget({
|
|
93
105
|
snapshot,
|
|
94
106
|
attempt: snapshot.attempts.at(-1) ?? null,
|
|
@@ -107,7 +119,7 @@ async function runCodingLoopOwned({
|
|
|
107
119
|
const retainedGuidance = redactExecutionText(attemptGuidance.trim(), { maxChars: Infinity }).text;
|
|
108
120
|
if (history.at(-1) !== retainedGuidance) history.push(retainedGuidance);
|
|
109
121
|
}
|
|
110
|
-
const previousAttempt = snapshot.attempts.at(-1) ?? null;
|
|
122
|
+
const previousAttempt = snapshot.attempts.filter(item => item.attempt_number < attemptNumber).at(-1) ?? null;
|
|
111
123
|
const promptInterventions = buildPromptInterventions(snapshot.charter, {
|
|
112
124
|
attemptNumber,
|
|
113
125
|
previousFindings,
|
|
@@ -121,37 +133,48 @@ async function runCodingLoopOwned({
|
|
|
121
133
|
promptInterventions,
|
|
122
134
|
});
|
|
123
135
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
136
|
+
let attempt = recoveredAttempt;
|
|
137
|
+
if (!attempt) {
|
|
138
|
+
appendEvent(snapshot, "dispatching", `Dispatching local coding attempt ${attemptNumber}.`, {
|
|
139
|
+
backend: snapshot.charter.backend, attempt_number: attemptNumber, instruction,
|
|
140
|
+
});
|
|
141
|
+
await publish(snapshot, "dispatching", onUpdate);
|
|
142
|
+
appendEvent(snapshot, "coding", `${snapshot.charter.backend} is executing locally.`, {
|
|
143
|
+
backend: snapshot.charter.backend, attempt_number: attemptNumber, instruction,
|
|
144
|
+
});
|
|
145
|
+
await publish(snapshot, "coding", onUpdate);
|
|
146
|
+
const recordCompletion = async execution => {
|
|
147
|
+
attempt = buildAttempt({ attemptNumber, charter: snapshot.charter, instruction, promptInterventions, execution });
|
|
148
|
+
if (attempt.status !== "completed" || Array.isArray(attempt.raw_output) || !Object.keys(attempt.raw_output).length) return;
|
|
149
|
+
snapshot.metadata ??= {};
|
|
150
|
+
snapshot.metadata.completed_worker_result = {
|
|
151
|
+
version: 1, run_id: snapshot.id, invocation_id: execution.invocationId || randomUUID(),
|
|
152
|
+
objective_digest: verifyObjective(snapshot.charter).digest, attempt,
|
|
153
|
+
recorded_at: new Date().toISOString(),
|
|
154
|
+
};
|
|
155
|
+
// Do not enter collectors until the terminal result has been committed
|
|
156
|
+
// to private storage by onUpdate. A failed write must never become a
|
|
157
|
+
// synthetic executor failure that dispatches another worker.
|
|
158
|
+
try { await publish(snapshot, "coding", onUpdate); }
|
|
159
|
+
catch (error) {
|
|
160
|
+
snapshot.metadata.completed_worker_result.persistence_failed = true;
|
|
161
|
+
error.code = "worker_result_persistence_failed";
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
const execution = await safeExecution(() => executeAttempt({
|
|
166
|
+
attemptNumber, backend: snapshot.charter.backend, instruction,
|
|
167
|
+
charter: snapshot.charter, onExecutionComplete: recordCompletion,
|
|
168
|
+
}));
|
|
169
|
+
if (!attempt) await recordCompletion(execution);
|
|
170
|
+
} else {
|
|
171
|
+
appendEvent(snapshot, "collecting_evidence", "Recovering the completed worker result without dispatching another worker.", {
|
|
172
|
+
code: "completed_worker_result_recovered", attempt_number: attemptNumber,
|
|
173
|
+
invocation_id: receipt.invocation_id,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
146
176
|
if (shouldStop()) return stopCodingLoop(snapshot, onUpdate);
|
|
147
|
-
|
|
148
|
-
attemptNumber,
|
|
149
|
-
charter: snapshot.charter,
|
|
150
|
-
instruction,
|
|
151
|
-
promptInterventions,
|
|
152
|
-
execution,
|
|
153
|
-
});
|
|
154
|
-
attempt.budget = refreshBudgetState(snapshot, attempt);
|
|
177
|
+
attempt.budget = refreshBudgetState(snapshot, snapshot.attempts.some(item => item.attempt_number === attemptNumber) ? null : attempt);
|
|
155
178
|
|
|
156
179
|
appendEvent(snapshot, "collecting_evidence", `Collecting attempt ${attemptNumber} evidence.`, {
|
|
157
180
|
attempt_number: attemptNumber,
|
|
@@ -162,7 +185,11 @@ async function runCodingLoopOwned({
|
|
|
162
185
|
tests_run: attempt.tests_run,
|
|
163
186
|
});
|
|
164
187
|
await publish(snapshot, "collecting_evidence", onUpdate);
|
|
165
|
-
attempt.observed_evidence = await safeEvidence(collectEvidence, attempt);
|
|
188
|
+
attempt.observed_evidence = await safeEvidence(collectEvidence, attempt, { retries: snapshot.metadata?.completed_worker_result ? 2 : 0, shouldStop });
|
|
189
|
+
if (shouldStop()) return stopCodingLoop(snapshot, onUpdate);
|
|
190
|
+
if (attempt.observed_evidence.collection_error && snapshot.metadata?.completed_worker_result) {
|
|
191
|
+
return waitForCollector(snapshot, attempt, onUpdate);
|
|
192
|
+
}
|
|
166
193
|
attempt.sqm = await safeSqm(checkSqm, attempt);
|
|
167
194
|
if (shouldStop()) return stopCodingLoop(snapshot, onUpdate);
|
|
168
195
|
|
|
@@ -183,7 +210,9 @@ async function runCodingLoopOwned({
|
|
|
183
210
|
});
|
|
184
211
|
if (shouldStop()) return stopCodingLoop(snapshot, onUpdate);
|
|
185
212
|
attempt.implementation_gate = attempt.audit.implementation_gate;
|
|
186
|
-
snapshot.attempts.
|
|
213
|
+
const recordedIndex = snapshot.attempts.findIndex(item => item.attempt_number === attemptNumber);
|
|
214
|
+
if (recordedIndex >= 0) snapshot.attempts[recordedIndex] = attempt;
|
|
215
|
+
else snapshot.attempts.push(attempt);
|
|
187
216
|
assessOutcomeProgress(snapshot.charter, attempt, previousAttempt, { ...outcomeOptions(snapshot), resetUnavailableCount: resume && attemptNumber === firstAttempt });
|
|
188
217
|
attempt.budget = refreshBudgetState(snapshot);
|
|
189
218
|
recordBudgetWarnings(snapshot, attempt);
|
|
@@ -200,6 +229,7 @@ async function runCodingLoopOwned({
|
|
|
200
229
|
|
|
201
230
|
if (!attempt.audit.passed) {
|
|
202
231
|
snapshot.final_audit = attempt.audit;
|
|
232
|
+
delete snapshot.metadata?.completed_worker_result;
|
|
203
233
|
const terminal = await handleFailedAudit({
|
|
204
234
|
snapshot,
|
|
205
235
|
attempt,
|
|
@@ -267,7 +297,11 @@ async function runCodingLoopOwned({
|
|
|
267
297
|
attempt.independent_audit.objective_version = reviewedObjectiveVersion;
|
|
268
298
|
|
|
269
299
|
// Re-collect after the read-only auditor to catch any mutated HEAD or working tree.
|
|
270
|
-
attempt.observed_evidence = await safeEvidence(collectEvidence, attempt);
|
|
300
|
+
attempt.observed_evidence = await safeEvidence(collectEvidence, attempt, { retries: snapshot.metadata?.completed_worker_result ? 2 : 0, shouldStop });
|
|
301
|
+
if (shouldStop()) return stopCodingLoop(snapshot, onUpdate);
|
|
302
|
+
if (attempt.observed_evidence.collection_error && snapshot.metadata?.completed_worker_result) {
|
|
303
|
+
return waitForCollector(snapshot, attempt, onUpdate);
|
|
304
|
+
}
|
|
271
305
|
attempt.sqm = await safeSqm(checkSqm, attempt, attempt.sqm);
|
|
272
306
|
if (shouldStop()) return stopCodingLoop(snapshot, onUpdate);
|
|
273
307
|
attempt.audit = await auditAttempt(snapshot.charter, attempt, {
|
|
@@ -282,6 +316,7 @@ async function runCodingLoopOwned({
|
|
|
282
316
|
if (!attempt.audit.passed || !attempt.independent_audit.passed) {
|
|
283
317
|
const findings = [...attempt.audit.findings, ...independentFindings];
|
|
284
318
|
snapshot.final_audit = aggregateAudit(attempt, false, findings);
|
|
319
|
+
delete snapshot.metadata?.completed_worker_result;
|
|
285
320
|
const terminal = await handleFailedAudit({
|
|
286
321
|
snapshot,
|
|
287
322
|
attempt,
|
|
@@ -296,6 +331,7 @@ async function runCodingLoopOwned({
|
|
|
296
331
|
continue;
|
|
297
332
|
}
|
|
298
333
|
|
|
334
|
+
delete snapshot.metadata?.completed_worker_result;
|
|
299
335
|
snapshot.implementation_gate_passed = true;
|
|
300
336
|
attempt.pr_readiness = evaluatePrReadiness(snapshot.charter, attempt);
|
|
301
337
|
attempt.pr_gate = attempt.pr_readiness;
|
|
@@ -389,10 +425,23 @@ async function recheckPrReadinessOwned({ snapshot, collectEvidence, checkSqm = n
|
|
|
389
425
|
}
|
|
390
426
|
|
|
391
427
|
|
|
428
|
+
async function waitForCollector(snapshot, attempt, onUpdate) {
|
|
429
|
+
snapshot.final_audit = humanGate("Evidence collection failed; the completed worker result is retained for retry.", [{
|
|
430
|
+
code: "EVIDENCE_COLLECTION_UNAVAILABLE", severity: "blocker", target: "evidence_collector",
|
|
431
|
+
explanation: attempt.observed_evidence.collection_error, repairability: "manual",
|
|
432
|
+
}]);
|
|
433
|
+
appendEvent(snapshot, "waiting_human", "Retry evidence collection for the same completed attempt.", {
|
|
434
|
+
code: "evidence_collection_retry_required", attempt_number: attempt.attempt_number,
|
|
435
|
+
});
|
|
436
|
+
await publish(snapshot, "waiting_human", onUpdate);
|
|
437
|
+
return snapshot;
|
|
438
|
+
}
|
|
439
|
+
|
|
392
440
|
async function safeExecution(callback) {
|
|
393
441
|
try {
|
|
394
442
|
return await callback();
|
|
395
443
|
} catch (error) {
|
|
444
|
+
if (error?.code === "worker_result_persistence_failed") throw error;
|
|
396
445
|
return {
|
|
397
446
|
exitCode: null,
|
|
398
447
|
logs: [],
|
|
@@ -402,14 +451,19 @@ async function safeExecution(callback) {
|
|
|
402
451
|
}
|
|
403
452
|
}
|
|
404
453
|
|
|
405
|
-
async function safeEvidence(collector, attempt) {
|
|
454
|
+
async function safeEvidence(collector, attempt, { retries = 0, shouldStop = () => false } = {}) {
|
|
406
455
|
if (!collector) return unavailableEvidence("Evidence collector is not configured.");
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
456
|
+
for (let retry = 0; ; retry++) {
|
|
457
|
+
if (retry > 0 && shouldStop()) return unavailableEvidence("Evidence collection stopped by the operator.");
|
|
458
|
+
try {
|
|
459
|
+
const evidence = await collector(attempt);
|
|
460
|
+
if (evidence?.collection_error && retry < retries) continue;
|
|
461
|
+
return evidence;
|
|
462
|
+
}
|
|
463
|
+
catch (error) {
|
|
464
|
+
if (retry < retries) continue;
|
|
465
|
+
return unavailableEvidence(error instanceof Error ? error.message : "Evidence collection failed.");
|
|
466
|
+
}
|
|
413
467
|
}
|
|
414
468
|
}
|
|
415
469
|
|
|
@@ -430,6 +484,7 @@ async function safeSqm(checker, attempt, previous = null, phase = "min") {
|
|
|
430
484
|
|
|
431
485
|
function unavailableEvidence(error) {
|
|
432
486
|
return {
|
|
487
|
+
collection_error: error,
|
|
433
488
|
collected_at: new Date().toISOString(),
|
|
434
489
|
local: {
|
|
435
490
|
available: false,
|
package/src/orchestrator/risk.js
CHANGED
|
@@ -34,8 +34,7 @@ export function classifyRisk(request) {
|
|
|
34
34
|
const text = [
|
|
35
35
|
cleanString(request.requirement),
|
|
36
36
|
...stringArray(request.acceptance_criteria),
|
|
37
|
-
|
|
38
|
-
].join("\n");
|
|
37
|
+
].map(proposedWork).join("\n");
|
|
39
38
|
const tags = [];
|
|
40
39
|
let level = /\b(docs?|readme|comment|typo)\b|文書|ドキュメント|文档/i.test(text)
|
|
41
40
|
? "low"
|
|
@@ -61,3 +60,19 @@ export function classifyRisk(request) {
|
|
|
61
60
|
],
|
|
62
61
|
};
|
|
63
62
|
}
|
|
63
|
+
|
|
64
|
+
// Prohibitions describe the boundary, not a proposed sensitive change. Only
|
|
65
|
+
// remove narrowly recognized negative operations; ambiguous wording remains
|
|
66
|
+
// subject to the conservative keyword rules and inherited risk is unchanged.
|
|
67
|
+
function proposedWork(text) {
|
|
68
|
+
const target = "(?:auth(?:entication|orization)?|permissions?|tenant(?:s)?|security|oauth|sessions?|database|schema|billing|payments?|secrets?)";
|
|
69
|
+
const operation = "(?:change|modify|alter|update|expand|delete|remove)";
|
|
70
|
+
return text.split(/([.!?;\n。!?;])/u).map(clause => {
|
|
71
|
+
// A conditional/exception is not an unconditional negative constraint.
|
|
72
|
+
if (/\b(?:unless|except|if|but)\b|ただし|場合|除非|但是|如果/i.test(clause)) return clause;
|
|
73
|
+
return clause
|
|
74
|
+
.replace(new RegExp(`\\b(?:do not|don't|never|without)\\s+${operation}\\s+(?:(?:the|existing|current)\\s+)*${target}\\b`, "gi"), "")
|
|
75
|
+
.replace(/(?:認証|認可|権限|データベース|決済|個人情報)(?:は|を)(?:変更|拡張|削除)しない/g, "")
|
|
76
|
+
.replace(/(?:不|不得|不要)(?:修改|更改|改变|扩大|删除)(?:现有)?(?:权限|认证|授权|数据库|支付|个人信息)/g, "");
|
|
77
|
+
}).join("");
|
|
78
|
+
}
|