@awak-app/simy-cli 0.6.28 → 0.6.30
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/package.json +1 -1
- package/src/cli-contract.js +1 -1
- package/src/codex-plugins/simy/hooks/completion-binding.mjs +14 -0
- package/src/codex-plugins/simy/hooks/consistency-handoff.mjs +38 -0
- package/src/codex-plugins/simy/hooks/delivery-loop.mjs +118 -13
- package/src/codex-plugins/simy/hooks/purpose-cycle.mjs +6 -2
- package/src/codex-plugins/simy/hooks/purpose-evidence.mjs +11 -3
- package/src/codex-plugins/simy/hooks/purpose-rule-review.mjs +10 -3
- package/src/codex-plugins/simy/skills/delivery-loop/SKILL.md +2 -2
- package/src/delivery-hooks/checkpoint-diagnostic.js +157 -0
- package/src/delivery-hooks/command.js +5 -2
- package/src/delivery-hooks/consistency-monitor.js +17 -6
- package/src/delivery-hooks/supervisor.js +25 -12
- package/src/desktop-host.js +2 -2
- package/src/session-supervision/codex-app-server.js +137 -68
- package/src/session-supervision/consistency-auditor.js +126 -28
- package/src/session-supervision/discovery-window.js +13 -1
- package/src/session-supervision/index.js +74 -20
- package/src/session-supervision/purpose-recovery.js +7 -4
- package/src/session-supervision/purpose-verification.js +56 -17
- package/src/session-supervision/session-lineage.js +27 -0
- package/src/sqm/claude-plugin.js +2 -0
package/package.json
CHANGED
package/src/cli-contract.js
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
} from "./durable-local-task-steps-contract.js";
|
|
14
14
|
import { buildExecutionCapabilityContract } from "./execution-capability-contract.js";
|
|
15
15
|
|
|
16
|
-
export const CLI_VERSION = "0.6.
|
|
16
|
+
export const CLI_VERSION = "0.6.30";
|
|
17
17
|
export const CLI_API_CONTRACT_VERSION = 10;
|
|
18
18
|
|
|
19
19
|
export function narrativeImageCapabilityMetadata() {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
const digest = run => createHash('sha256').update(JSON.stringify([run.id, run.objective, run.revision, run.checkpoint,
|
|
3
|
+
run.managed?.taskId, run.managed?.acceptanceCriteria, run.managed?.verified, run.managed?.grants, run.managed?.purpose?.ruleDigest])).digest('hex');
|
|
4
|
+
// A compatibility fence, not a semantic proof or a cryptographic attestation.
|
|
5
|
+
// An older writer retaining an unknown receipt cannot certify its later revision.
|
|
6
|
+
export function bindControllerCompletion(run, now) {
|
|
7
|
+
run.managed.completionBinding = { version:1, digest:digest(run), at:now, source:'canonical_acceptance_controller' };
|
|
8
|
+
}
|
|
9
|
+
export function completionBindingCurrent(run) {
|
|
10
|
+
return run.managed?.completionBinding?.version===1 && run.managed.completionBinding.source==='canonical_acceptance_controller' && run.managed.completionBinding.digest===digest(run);
|
|
11
|
+
}
|
|
12
|
+
export function completionClaimUnbound(record) {
|
|
13
|
+
return ['completed','reported_done'].includes(record?.run?.state) && record.run.managed?.purpose?.enabled === true && !completionBindingCurrent(record.run);
|
|
14
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Durable handoff deadlines concern unowned work, never a live effect lease.
|
|
2
|
+
// Expiry queues diagnosis; it does not replay a write or grant permission.
|
|
3
|
+
export const HANDOFF_RECEIPT_MS = 2 * 60_000;
|
|
4
|
+
export const HANDOFF_PROGRESS_MS = 5 * 60_000;
|
|
5
|
+
export function recordHandoffStage(item, stage, now, details = {}) {
|
|
6
|
+
const previous = item.stage;
|
|
7
|
+
item.stage = stage;
|
|
8
|
+
Object.assign(item, details);
|
|
9
|
+
if (previous !== stage) {
|
|
10
|
+
item.history ||= [];
|
|
11
|
+
item.history.push({ from: previous || null, to: stage, at: now });
|
|
12
|
+
item.history = item.history.slice(-16);
|
|
13
|
+
item.stageStartedAt = now;
|
|
14
|
+
}
|
|
15
|
+
item.owner = stage === 'pending' ? 'independent_auditor' : ['received', 'executing'].includes(stage) ? 'task_controller' : item.owner || 'independent_auditor';
|
|
16
|
+
item.dueAt = ['resolved', 'superseded'].includes(stage) ? null : (item.stageStartedAt || item.createdAt || now) + (stage === 'pending' ? HANDOFF_RECEIPT_MS : HANDOFF_PROGRESS_MS);
|
|
17
|
+
}
|
|
18
|
+
export function handoffHealth(managed, now) {
|
|
19
|
+
const requests = managed?.consistencyOutbox || [];
|
|
20
|
+
const busy = Boolean(managed?.resumeLease || managed?.liveWorkspaceLease || managed?.outcomeAudit?.lease);
|
|
21
|
+
return requests.filter(item => !['resolved', 'superseded'].includes(item.stage)).map(item => ({
|
|
22
|
+
id: item.id, owner: item.owner || 'task_controller', stage: item.stage,
|
|
23
|
+
overdue: !busy && now >= (item.dueAt || (item.observedAt || item.receivedAt || item.createdAt || now) + HANDOFF_PROGRESS_MS),
|
|
24
|
+
ageMs: Math.max(0, now - (item.createdAt || now)), effectDecisionId: item.decisionId || null,
|
|
25
|
+
}));
|
|
26
|
+
}
|
|
27
|
+
export function handoffMetrics(records, now) {
|
|
28
|
+
const requests = records.flatMap(r => r.run?.managed?.consistencyOutbox || []);
|
|
29
|
+
const resolved = requests.filter(r => r.stage === 'resolved' && Number.isFinite(r.verifiedAt) && Number.isFinite(r.createdAt));
|
|
30
|
+
const audits = records.map(r => r.run?.managed?.outcomeAudit).filter(Boolean);
|
|
31
|
+
return { pendingHandoffs: requests.filter(r => !['resolved', 'superseded'].includes(r.stage)).length,
|
|
32
|
+
overdueHandoffs: records.flatMap(r => handoffHealth(r.run?.managed, now)).filter(r => r.overdue).length,
|
|
33
|
+
resolvedHandoffs: resolved.length, recoveryLatencyMs: resolved.length ? resolved.reduce((n,r) => n + Math.max(0,r.verifiedAt-r.createdAt),0) / resolved.length : null,
|
|
34
|
+
auditTokens: audits.reduce((n,a) => n + (a.tokensUsed || 0),0),
|
|
35
|
+
unknownAuditConsumption: audits.filter(a => a.unknownConsumption).length,
|
|
36
|
+
falseCompletionCount: null, unnecessaryConfirmationCount: null, duplicateEffectCount: null, costAmount: null,
|
|
37
|
+
measurementScope: 'retained_controller_receipts', };
|
|
38
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { bindControllerCompletion } from './completion-binding.mjs';
|
|
3
|
+
import { recordHandoffStage } from './consistency-handoff.mjs';
|
|
2
4
|
import { retainApplicableRules } from './purpose-rule-policy.mjs';
|
|
3
5
|
import { reconcileRequirements, retainControllerEvidence, reconcileFindings, findingText, evidenceLedger, currentPurposeObservation } from './purpose-evidence.mjs';
|
|
4
6
|
import { workspaceIdentity } from './purpose-workspace.mjs';
|
|
@@ -7,7 +9,7 @@ import { searchEnabled, failureLimitReached, searchState, recordSearchExperiment
|
|
|
7
9
|
import { createHash, randomUUID } from 'node:crypto';
|
|
8
10
|
import { mkdir, open, readFile, readdir, realpath, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
9
11
|
import { homedir } from 'node:os';
|
|
10
|
-
import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
12
|
+
import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
11
13
|
import { execFile } from 'node:child_process';
|
|
12
14
|
import { promisify } from 'node:util';
|
|
13
15
|
import { acquireWorkspaceLease, releaseWorkspaceLease } from './workspace-lease.mjs';
|
|
@@ -63,9 +65,40 @@ function applyProviderConsent(record, link) {
|
|
|
63
65
|
revision: (record.run.revision || 0) + 1,
|
|
64
66
|
...(record.run.managed ? { managed: { ...record.run.managed, autoRecovery: false } } : {}) } } : {}) };
|
|
65
67
|
}
|
|
68
|
+
async function applyArchiveFence(root, record) {
|
|
69
|
+
if (!record) return record;
|
|
70
|
+
const file = `${recordPath(root, record.provider, record.session)}.archive-fence`;
|
|
71
|
+
let archived;
|
|
72
|
+
try { archived = JSON.parse(await readFile(file, 'utf8')).archived !== false; }
|
|
73
|
+
catch (error) { if (error.code === 'ENOENT') return record; archived = true; }
|
|
74
|
+
if (!archived) return record;
|
|
75
|
+
return { ...record, providerArchived: true, autoContinue: false, tracked: false, monitor: false,
|
|
76
|
+
...(record.run && !TERMINAL.has(record.run.state) ? { run: { ...record.run, state: 'paused',
|
|
77
|
+
reason: 'The original provider conversation was archived.',
|
|
78
|
+
...(record.run.managed ? { managed: { ...record.run.managed, autoRecovery: false } } : {}) } } : {}) };
|
|
79
|
+
}
|
|
80
|
+
export async function recordProviderArchive(root, provider, session, archived, observedAt) {
|
|
81
|
+
await mkdir(root, { recursive: true, mode: 0o700 });
|
|
82
|
+
const file = `${recordPath(root, provider, session)}.archive-fence`;
|
|
83
|
+
const lock = `${file}.lock`, token = await acquireStateLock(lock, processIdentity);
|
|
84
|
+
const temporary = `${file}.${randomUUID()}.tmp`;
|
|
85
|
+
try {
|
|
86
|
+
let previous; try { previous = JSON.parse(await readFile(file, 'utf8')); } catch {}
|
|
87
|
+
if (previous?.observedAt > observedAt) return;
|
|
88
|
+
await writeFile(temporary, JSON.stringify({ provider, session, archived, observedAt }), { mode: 0o600 });
|
|
89
|
+
await rename(temporary, file);
|
|
90
|
+
} finally { await rm(temporary, { force: true }); await releaseStateLock(lock, token); }
|
|
91
|
+
}
|
|
66
92
|
async function currentProviderConsent(root, records) {
|
|
67
93
|
const links = Object.fromEntries(await Promise.all(['codex', 'claude'].map(async provider => [provider, await providerLinkState(root, provider)])));
|
|
68
|
-
|
|
94
|
+
let archiveNames;
|
|
95
|
+
try { archiveNames = new Set((await readdir(root)).filter(name => name.endsWith('.archive-fence'))); }
|
|
96
|
+
catch { /* A failed index read falls back to fresh per-record fencing. */ }
|
|
97
|
+
return Promise.all(records.map(record => {
|
|
98
|
+
const current = applyProviderConsent(record, links[record.provider] || {});
|
|
99
|
+
if (archiveNames && !archiveNames.has(`${basename(recordPath(root, record.provider, record.session))}.archive-fence`)) return current;
|
|
100
|
+
return applyArchiveFence(root, current);
|
|
101
|
+
}));
|
|
69
102
|
}
|
|
70
103
|
export async function linkedProvider(root, provider) {
|
|
71
104
|
return (await providerLinkState(root, provider)).connected === true;
|
|
@@ -175,6 +208,34 @@ async function acquireStateLock(lock, inspectProcess, depth = 0) {
|
|
|
175
208
|
await releaseStateLock(reclaim, token);
|
|
176
209
|
}
|
|
177
210
|
}
|
|
211
|
+
export async function withSessionParentLock(root, operation, inspectProcess = processIdentity) {
|
|
212
|
+
await mkdir(root, { recursive: true, mode: 0o700 });
|
|
213
|
+
const lock = join(root, 'session-lineage.lock');
|
|
214
|
+
// Distinct SessionStart hooks share this lock. Wait within the hook timeout
|
|
215
|
+
// for short writers, without reclaiming unknown owners or replaying work.
|
|
216
|
+
const deadline = Date.now() + 500;
|
|
217
|
+
let token;
|
|
218
|
+
for (;;) {
|
|
219
|
+
try { token = await acquireStateLock(lock, inspectProcess); break; }
|
|
220
|
+
catch (error) {
|
|
221
|
+
if (Date.now() >= deadline || !/^(?:Delivery state is busy; retry the command\.|Delivery state lock owner is unavailable; inspect the lock before retrying\.)$/.test(error.message)) throw error;
|
|
222
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
try { return await operation(); } finally { await releaseStateLock(lock, token); }
|
|
226
|
+
}
|
|
227
|
+
export async function validateSessionParent(root, child, parent) {
|
|
228
|
+
const key = item => `${item.provider}:${item.session}`;
|
|
229
|
+
const seen = new Set([key(child)]);
|
|
230
|
+
let cursor = parent;
|
|
231
|
+
for (let depth = 0; cursor; depth++) {
|
|
232
|
+
if (depth >= 256 || seen.has(key(cursor))) throw new Error('Parent relationship would create a cycle or exceed the ancestry limit.');
|
|
233
|
+
seen.add(key(cursor));
|
|
234
|
+
const ancestor = await readRecord(root, cursor.provider, cursor.session);
|
|
235
|
+
if (!ancestor?.observedAt && !ancestor?.admission?.validatedAt) throw new Error('The parent ancestry must contain observed provider conversations.');
|
|
236
|
+
cursor = ancestor.parentSession;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
178
239
|
export async function updateRecord(root, provider, session, mutate, { inspectProcess = processIdentity } = {}) {
|
|
179
240
|
await mkdir(root, { recursive: true, mode: 0o700 });
|
|
180
241
|
const file = recordPath(root, provider, session), lock = `${file}.lock`;
|
|
@@ -184,7 +245,7 @@ export async function updateRecord(root, provider, session, mutate, { inspectPro
|
|
|
184
245
|
let temporary;
|
|
185
246
|
try {
|
|
186
247
|
const stored = await read(file);
|
|
187
|
-
const previous = applyProviderConsent(stored, await providerLinkState(root, provider));
|
|
248
|
+
const previous = await applyArchiveFence(root, applyProviderConsent(stored, await providerLinkState(root, provider)));
|
|
188
249
|
const previousBytes = stored === null ? null : JSON.stringify(stored);
|
|
189
250
|
const next = await mutate(previous);
|
|
190
251
|
if (next) {
|
|
@@ -210,7 +271,7 @@ export async function readRecord(root = rootDirectory(), provider, session) {
|
|
|
210
271
|
const path = recordPath(root, provider, session);
|
|
211
272
|
try {
|
|
212
273
|
const record = await read(path);
|
|
213
|
-
return record?.schemaVersion === 1 && record.provider === provider && record.session === session ? applyProviderConsent(record, await providerLinkState(root, provider)) : null;
|
|
274
|
+
return record?.schemaVersion === 1 && record.provider === provider && record.session === session ? applyArchiveFence(root, applyProviderConsent(record, await providerLinkState(root, provider))) : null;
|
|
214
275
|
} catch { return null; } // Match listRecords' treatment of unreadable records.
|
|
215
276
|
}
|
|
216
277
|
const RECORD_NAME = /^[a-f0-9]{64}\.json$/;
|
|
@@ -608,6 +669,9 @@ function managedSpec(spec, session, provider, observedProcess, admission, now) {
|
|
|
608
669
|
continuationPolicy: continuationMode(config.continuationPolicy),
|
|
609
670
|
autoRecovery, resumeModel, autoRecoveryUnavailableReason: config.autoRecovery === true && !autoRecovery ? 'Exact Codex process, session UUID, and resumeModel are required.' : null,
|
|
610
671
|
autoRecoverySandbox: config.autoRecoverySandbox || 'read-only', resumeChannel: config.resumeChannel || null,
|
|
672
|
+
...(appServerChannel && admission.nativeSandbox && admission.nativeSandboxSource ? {
|
|
673
|
+
providerSandbox: structuredClone(admission.nativeSandbox), providerSandboxSource: structuredClone(admission.nativeSandboxSource) } :
|
|
674
|
+
appServerChannel ? { providerSandbox: null, providerSandboxSource: { type: 'unavailable_at_enrollment' } } : {}),
|
|
611
675
|
capability: appServerChannel && autoRecovery ? 'codex_app_server' : autoRecovery ? 'codex_exec_resume' : 'live_hook',
|
|
612
676
|
awaitingInitialHook: provider === 'claude' && Boolean(observedProcess?.identity) && Boolean(autonomyPolicy),
|
|
613
677
|
autonomyPolicy, plan: null, decision: null, decisionHistory: [], workHistory: [], escalation: null,
|
|
@@ -623,6 +687,7 @@ function freezeEnrollment(run, now) {
|
|
|
623
687
|
ledger.charter ||= { objective: run.objective, criteria: structuredClone(run.managed.acceptanceCriteria), approvalScopes: [...run.managed.approvalScopes], frozenAt: now };
|
|
624
688
|
}
|
|
625
689
|
run.managed.enrollment = { objective: run.objective, acceptanceDigest: digest(JSON.stringify(run.managed.acceptanceCriteria)),
|
|
690
|
+
...(run.managed.providerSandboxSource ? {nativeAuthorityDigest:digest(JSON.stringify([run.managed.providerSandbox,run.managed.providerSandboxSource]))} : {}),
|
|
626
691
|
deliveryLevel: structuredClone(run.managed.deliveryLevel || null), frozenAt: now };
|
|
627
692
|
}
|
|
628
693
|
export function pendingAcceptance(run) {
|
|
@@ -943,7 +1008,7 @@ export async function checkpointDelivery({ root = rootDirectory(), provider, ses
|
|
|
943
1008
|
delete run.managed.recoveryDiagnosisDecisionId;
|
|
944
1009
|
}
|
|
945
1010
|
for (const request of run.managed?.consistencyOutbox || []) if (request.decisionId && request.decisionId === checkpoint.actionResult?.decisionId) {
|
|
946
|
-
request
|
|
1011
|
+
recordHandoffStage(request, checkpoint.actionResult.status === 'observed' ? 'inspected' : 'inspection_failed', now);
|
|
947
1012
|
request.observedAt = now; request.result = checkpoint.actionResult.status;
|
|
948
1013
|
}
|
|
949
1014
|
notifyState(run, now);
|
|
@@ -991,6 +1056,29 @@ export async function settlePurposeVerification({ root, provider, session, expec
|
|
|
991
1056
|
'Required operation, effect reconciliation or original-purpose contract remains unresolved. Preserve the waiting unit and continue independent authorized work.'] };
|
|
992
1057
|
}
|
|
993
1058
|
if(!externalReviewIsCurrent(managed,review,now)) review={...review,passed:false,needsHuman:false,findings:[...review.findings,'Controller external evidence expired before settlement; reacquire the same authorized observations and independently re-evaluate without replaying writes.']};
|
|
1059
|
+
// Validate counterevidence against a disposable ledger before mutations.
|
|
1060
|
+
// Verifier contract failures are verifier work, never worker checkpoint errors.
|
|
1061
|
+
if (!review.controllerEvidenceFailure) {
|
|
1062
|
+
try { reconcileFindings(structuredClone(managed), review, now); }
|
|
1063
|
+
catch (error) {
|
|
1064
|
+
if (!/^(Reopening a closed finding|Finding resolution needs|Independent review left unresolved findings|A finding cannot be open and resolved\.)/.test(error.message)) throw error;
|
|
1065
|
+
review = { ...review, passed: false, needsHuman: false, controllerEvidenceFailure: {
|
|
1066
|
+
code: 'independent_finding_evidence_invalid', targets: [...(review.resolutions || []).map(row => `resolution:${row.id}`), ...(review.findings || []).filter(row => row.reopenReason).map(row => `reopen:${row.id}`)],
|
|
1067
|
+
instruction: error.message + ' Repeat isolated verification and reconcile existing findings with primary reads. For reopening use the exact prior closure read scope.',
|
|
1068
|
+
requiredReads: (managed.purpose.evidence?.findings || []).filter(row => row.status !== 'open').map(row => ({ id: row.id, closedReads: row.closedReads || [] }))
|
|
1069
|
+
} };
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
if (review.controllerEvidenceFailure) {
|
|
1073
|
+
const prior = state.verifierFailures;
|
|
1074
|
+
const key = digest(JSON.stringify([currentDigest, review.controllerEvidenceFailure.code, review.controllerEvidenceFailure.targets]));
|
|
1075
|
+
state.verifierFailures = { key, attempts: prior?.key === key ? prior.attempts + 1 : 1, at: now, failure: review.controllerEvidenceFailure };
|
|
1076
|
+
state.review = review;
|
|
1077
|
+
run.revision += 1; run.updatedAt = now;
|
|
1078
|
+
queuePurposeVerification(run, `${review.controllerEvidenceFailure.instruction} Invalid targets: ${(review.controllerEvidenceFailure.targets || []).join(', ')}. Use a narrow direct read containing the cited excerpt within 2000 characters.`, now);
|
|
1079
|
+
managed.nextAttemptAt = now + Math.min(60_000, 1000 * 2 ** Math.min(state.verifierFailures.attempts - 1, 6));
|
|
1080
|
+
notifyState(run, now); return record;
|
|
1081
|
+
}
|
|
994
1082
|
const openFindings = reconcileFindings(managed, review, now);
|
|
995
1083
|
const units=evidenceLedger(managed).units;
|
|
996
1084
|
for (const result of (review.passed || review.needsHuman) ? review.workUnits || [] : []) {
|
|
@@ -1018,11 +1106,9 @@ export async function settlePurposeVerification({ root, provider, session, expec
|
|
|
1018
1106
|
for (const unit of evidenceLedger(managed).units || []) if (unit.status === 'observed' && unit.criterionIds.every(id => managed.verified[id])) unit.status = 'verified';
|
|
1019
1107
|
run.state = 'completed'; run.reason = review.summary; state.phase = 'completed';
|
|
1020
1108
|
run.checkpoint = { status: 'completed', summary: review.summary, evidence: review.criteria.map(c => c.evidence) };
|
|
1109
|
+
bindControllerCompletion(run, now);
|
|
1021
1110
|
managed.autoRecovery = false;
|
|
1022
|
-
|
|
1023
|
-
request.stage = 'resolved'; request.verifiedAt = now; request.reviewerSession = review.session;
|
|
1024
|
-
request.workspaceDigest = review.workspaceDigest;
|
|
1025
|
-
}
|
|
1111
|
+
settleCompletionHandoffs(run, now, review);
|
|
1026
1112
|
} else if (pending.length && pending.every(c => c.check.type === 'manual')) {
|
|
1027
1113
|
run.state = 'verification_pending'; run.reason = 'Only explicitly human acceptance remains.'; state.phase = 'waiting';
|
|
1028
1114
|
run.checkpoint = { status: 'completed', summary: review.summary }; managed.autoRecovery = false;
|
|
@@ -1219,6 +1305,17 @@ function purposeCompletionBarrier(run) {
|
|
|
1219
1305
|
const pending=ledger.units.filter(unit=>unit.required && unit.purposeRevision===ledger.purposeRevision && !['verified','superseded'].includes(unit.status));
|
|
1220
1306
|
return pending.length ? 'Required work units still waiting: '+pending.map(unit=>unit.id).join(', ') : null;
|
|
1221
1307
|
}
|
|
1308
|
+
function settleCompletionHandoffs(run, now, review) {
|
|
1309
|
+
const managed = run.managed;
|
|
1310
|
+
for (const request of managed.consistencyOutbox || []) {
|
|
1311
|
+
if (request.targetTask !== managed.taskId || request.objectiveDigest !== digest(JSON.stringify([run.objective, managed.acceptanceCriteria])) || request.version !== managed.controllerContractVersion) {
|
|
1312
|
+
recordHandoffStage(request, 'superseded', now, { reason: 'contract_changed' });
|
|
1313
|
+
continue;
|
|
1314
|
+
}
|
|
1315
|
+
recordHandoffStage(request, 'resolved', now, { verifiedAt: now, reviewerSession: review.session });
|
|
1316
|
+
request.workspaceDigest = review.workspaceDigest;
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1222
1319
|
export async function verifyDelivery({ root = rootDirectory(), provider, session, taskId, criterionId, now = Date.now() }) {
|
|
1223
1320
|
const started=Date.now(), initialNow=now;
|
|
1224
1321
|
return updateRecord(root, provider, session, async record => {
|
|
@@ -1244,7 +1341,7 @@ export async function verifyDelivery({ root = rootDirectory(), provider, session
|
|
|
1244
1341
|
if (!pendingAcceptance(run).length) {
|
|
1245
1342
|
const barrier=purposeCompletionBarrier(run);
|
|
1246
1343
|
if(barrier) queuePurposeVerification(run, 'Human decision recorded; '+barrier+' Independently reconcile the required operation before completing.', now);
|
|
1247
|
-
else {run.state = 'completed'; run.reason = run.checkpoint.summary;}
|
|
1344
|
+
else {run.state = 'completed'; run.reason = run.checkpoint.summary; bindControllerCompletion(run, now); settleCompletionHandoffs(run, now, run.managed.purpose?.search?.review || {});}
|
|
1248
1345
|
}
|
|
1249
1346
|
}
|
|
1250
1347
|
notifyState(run, now);
|
|
@@ -1519,7 +1616,7 @@ export async function handleHook({ input, provider, root = rootDirectory(), now
|
|
|
1519
1616
|
const event = input.hook_event_name;
|
|
1520
1617
|
const identity = observedProcess === undefined && (['SessionStart', 'UserPromptSubmit', 'Stop', 'StopFailure', 'SessionEnd', 'Interrupt'].includes(event)) ? await providerProcess(provider) : observedProcess;
|
|
1521
1618
|
let output = {}, boundaryLease = null, claimedWorkspace = null;
|
|
1522
|
-
|
|
1619
|
+
const applyObservation = () => updateRecord(root, provider, input.session_id, async old => {
|
|
1523
1620
|
const currentLink = await providerLinkState(root, provider);
|
|
1524
1621
|
if (!currentLink.connected || currentLink.epoch !== link.epoch) return old;
|
|
1525
1622
|
if (old && old.cwd !== cwd) return old;
|
|
@@ -1531,7 +1628,11 @@ export async function handleHook({ input, provider, root = rootDirectory(), now
|
|
|
1531
1628
|
const parentId = env.SIMY_PARENT_SESSION || (parentProvider === 'codex' ? env.CODEX_THREAD_ID : null);
|
|
1532
1629
|
if (['codex', 'claude'].includes(parentProvider) && /^[0-9a-f-]{36}$/i.test(parentId || '') &&
|
|
1533
1630
|
!(parentProvider === provider && parentId === input.session_id)) {
|
|
1534
|
-
|
|
1631
|
+
const parent = { provider: parentProvider, session: parentId };
|
|
1632
|
+
try {
|
|
1633
|
+
await validateSessionParent(root, { provider, session: input.session_id }, parent);
|
|
1634
|
+
record.parentSession = parent;
|
|
1635
|
+
} catch { /* Unverified or cyclic ancestry never changes navigation. */ }
|
|
1535
1636
|
}
|
|
1536
1637
|
}
|
|
1537
1638
|
const owner = record.run?.managed?.liveHookOwner;
|
|
@@ -1673,7 +1774,11 @@ export async function handleHook({ input, provider, root = rootDirectory(), now
|
|
|
1673
1774
|
record.run.updatedAt = now;
|
|
1674
1775
|
notifyState(record.run, now);
|
|
1675
1776
|
return record;
|
|
1676
|
-
});
|
|
1777
|
+
});
|
|
1778
|
+
try {
|
|
1779
|
+
if (event === 'SessionStart') await withSessionParentLock(root, applyObservation, inspectProcess);
|
|
1780
|
+
else await applyObservation();
|
|
1781
|
+
} catch (error) {
|
|
1677
1782
|
if (claimedWorkspace?.release) await claimedWorkspace.release();
|
|
1678
1783
|
throw error;
|
|
1679
1784
|
}
|
|
@@ -42,9 +42,13 @@ export function validatePurposeReview(value, plan, ruleIds = [], previousRules =
|
|
|
42
42
|
result.priorities.some(item => !Number.isInteger(item.rank) || item.rank < 1 || item.rank > options.length) ||
|
|
43
43
|
new Set(result.priorities.map(item => item.rank)).size !== options.length ||
|
|
44
44
|
options.length && result.priorities.find(item => item.rank === 1)?.optionId !== plan.selectedOptionId) throw new Error('Purpose priorities must rank every option and select the highest priority.');
|
|
45
|
-
if (!Array.isArray(value.rules) || value.rules.length > ruleIds.length
|
|
45
|
+
if (!Array.isArray(value.rules) || value.rules.length > ruleIds.length) throw new Error('Every purpose rule needs one disposition.');
|
|
46
|
+
// Requirement references use rule:<id>; both spellings identify the same
|
|
47
|
+
// frozen clause. Normalize the label, never the status or evidence.
|
|
48
|
+
const suppliedRules=value.rules.map(item=>({...item,id:typeof item?.id==='string' && item.id.startsWith('rule:')?item.id.slice(5):item?.id}));
|
|
49
|
+
if(new Set(suppliedRules.map(item=>item.id)).size!==suppliedRules.length)throw new Error('Every purpose rule needs one disposition.');
|
|
46
50
|
const merged = new Map(previousRules.filter(item => ruleIds.includes(item.id)).map(item => [item.id, item]));
|
|
47
|
-
for (const item of
|
|
51
|
+
for (const item of suppliedRules) { if (!ruleIds.includes(item?.id)) throw new Error('Invalid purpose rule receipt.'); merged.set(item.id, item); }
|
|
48
52
|
if (!selectiveRuleReview(managed) && merged.size !== ruleIds.length) throw new Error('Every purpose rule needs one disposition.');
|
|
49
53
|
result.rules = (selectiveRuleReview(managed) ? [...merged.values()] : ruleIds.map(id => merged.get(id))).map(item => {
|
|
50
54
|
if (!ruleIds.includes(item?.id) || !['satisfied', 'failed', 'unverified', 'not_applicable'].includes(item.status) || !clean(item.basis, 160)) throw new Error('Invalid purpose rule receipt.');
|
|
@@ -117,7 +117,7 @@ export function normalizeFindings(value, managed) {
|
|
|
117
117
|
if (typeof item === 'string') return { id: `legacy:${digest(item).slice(0, 16)}`, requirementId: null, source: 'legacy_review', owner: 'worker', gap: text(item, 1000), verification: 'Inspect the reported gap', status: 'open' };
|
|
118
118
|
const row = ledger.requirements.find(row => row.id === item?.requirementId);
|
|
119
119
|
if (!row || !/^[\w:.-]{1,100}$/.test(item?.id) || !owners.includes(item.owner) || !text(item.gap) || !text(item.verification) || !text(item.source)) throw new Error('Review finding must identify a known requirement, source, owner, observation and verification.');
|
|
120
|
-
return { id: item.id, requirementId: row.id, source: text(item.source), owner: item.owner, gap: text(item.gap, 1000), verification: text(item.verification), reopenReason: text(item.reopenReason), ...(item.observation && ['observer', 'capability'].includes(item.owner) ? { observation: { type: text(item.observation.type, 40), ...(item.observation.url ? { url: text(item.observation.url, 2000) } : {}), ...(item.observation.path ? { path: text(item.observation.path, 500) } : {}), ...(Array.isArray(item.observation.fields) ? { fields: item.observation.fields.slice(0, 5).map(field => text(field, 201)) } : {}) } } : {}), status: 'open' };
|
|
120
|
+
return { id: item.id, requirementId: row.id, source: text(item.source), owner: item.owner, gap: text(item.gap, 1000), verification: text(item.verification), reopenReason: text(item.reopenReason), reopenEvidenceRefs: Array.isArray(item.reopenEvidenceRefs) ? item.reopenEvidenceRefs.slice(0, 8).map(row => ({ target: text(row.target, 2000), digest: text(row.digest, 64), scope: row.scope })) : [], ...(item.observation && ['observer', 'capability'].includes(item.owner) ? { observation: { type: text(item.observation.type, 40), ...(item.observation.url ? { url: text(item.observation.url, 2000) } : {}), ...(item.observation.path ? { path: text(item.observation.path, 500) } : {}), ...(Array.isArray(item.observation.fields) ? { fields: item.observation.fields.slice(0, 5).map(field => text(field, 201)) } : {}) } } : {}), status: 'open' };
|
|
121
121
|
});
|
|
122
122
|
}
|
|
123
123
|
|
|
@@ -129,11 +129,19 @@ export function reconcileFindings(managed, review, now) {
|
|
|
129
129
|
const prior = ledger.findings.find(row => row.id === resolution.id);
|
|
130
130
|
if (!prior || !['resolved', 'refuted'].includes(resolution.status) || !text(resolution.evidence)) throw new Error('Finding resolution needs a known ID and independent evidence.');
|
|
131
131
|
if (findings.some(row => row.id === resolution.id)) throw new Error('A finding cannot be open and resolved.');
|
|
132
|
-
|
|
132
|
+
const boundReads = (resolution.evidenceRefs || []).filter(link => /^[a-f0-9]{64}$/.test(link.digest) && (review.capturedReads || []).some(row => row.target === link.target && row.digest === link.digest && row.scope === link.scope));
|
|
133
|
+
if (!boundReads.length) throw new Error('Finding resolution needs current captured finding-specific evidence references.');
|
|
134
|
+
Object.assign(prior, { status: resolution.status, evidence: text(resolution.evidence, 1000), checkedAt: now, workspaceDigest: review.workspaceDigest, closedReads: boundReads.map(row => ({ target: row.target, digest: row.digest, scope: row.scope })) });
|
|
133
135
|
}
|
|
134
136
|
for (const finding of findings) {
|
|
135
137
|
const prior = ledger.findings.find(row => row.id === finding.id);
|
|
136
|
-
if (prior && prior.status !== 'open'
|
|
138
|
+
if (prior && prior.status !== 'open') {
|
|
139
|
+
const currentReads = review.capturedReads || [];
|
|
140
|
+
const newBoundEvidence = finding.reopenEvidenceRefs.some(link => /^[a-f0-9]{64}$/.test(link.digest) &&
|
|
141
|
+
currentReads.some(row => row.target === link.target && row.digest === link.digest && row.scope === link.scope) &&
|
|
142
|
+
(prior.closedReads || []).some(row => row.target === link.target && row.scope === link.scope && row.digest !== link.digest));
|
|
143
|
+
if (!finding.reopenReason || !newBoundEvidence) throw new Error('Reopening a closed finding needs new independently observed counterevidence linked to the finding, not a paraphrased reason or missing observation.');
|
|
144
|
+
}
|
|
137
145
|
if (prior) Object.assign(prior, finding, { checkedAt: now, workspaceDigest: review.workspaceDigest });
|
|
138
146
|
else ledger.findings.push({ ...finding, checkedAt: now, workspaceDigest: review.workspaceDigest });
|
|
139
147
|
}
|
|
@@ -49,15 +49,22 @@ export function assessRuleReview(review, managed) {
|
|
|
49
49
|
source: 'Independent canonical rule assessment', owner, gap: `Resolve applicable failed/unverified controls: ${values.slice(0, 25).join(', ')}`,
|
|
50
50
|
verification: owner === 'worker' ? 'Repair the observed result and independently recheck.' : 'Acquire missing independent evidence or repair the owning observation/controller capability; do not require worker-authored proof.', status: 'open' });
|
|
51
51
|
const complete = !missing.length && !unknown && !unresolved.length;
|
|
52
|
-
const resolutions = [...(review.resolutions || [])]
|
|
52
|
+
const resolutions = [...(review.resolutions || [])];
|
|
53
53
|
const generatedIds = new Set(['independent-rule-coverage', 'independent-rule-obligations',
|
|
54
54
|
...(managed.purpose.evidence?.findings || []).filter(row => /^independent-rule-obligations-[a-z]+$/.test(row.id)).map(row => row.id),
|
|
55
55
|
...findings.filter(row => /^independent-rule-obligations-[a-z]+$/.test(row.id)).map(row => row.id)]);
|
|
56
56
|
for (const id of generatedIds) {
|
|
57
57
|
const current = findings.find(item => item.id === id);
|
|
58
58
|
const prior = managed.purpose.evidence?.findings?.find(item => item.id === id);
|
|
59
|
-
if (!current && !missing.length && prior?.status === 'open'
|
|
60
|
-
|
|
59
|
+
if (!current && !missing.length && prior?.status === 'open' && !resolutions.some(row => row.id === id)) {
|
|
60
|
+
const resolution = { id, status: 'resolved', evidence: 'Current independent assessment covers this obligation with no remaining gap.', evidenceRefs: (review.capturedReads || []).filter(read => rules.filter(rule => rule.id === prior.requirementId?.replace(/^rule:/, '')).some(rule => rule.evidenceRefs.some(link => link.target === read.target && (link.ref === read.ref || link.ref === `read:${read.target}` && (review.capturedReads || []).filter(row => row.target === read.target).length === 1)))) };
|
|
61
|
+
if (resolution.evidenceRefs.length) resolutions.push(resolution);
|
|
62
|
+
else review = { ...review, passed: false, needsHuman: false, controllerEvidenceFailure: { code: 'generated_resolution_evidence_missing', targets: [`resolution:${id}`], instruction: 'Independently acquire finding-specific primary evidence for the prior rule applicability gap before resolving it. A not_applicable label alone cannot close an existing finding.' } };
|
|
63
|
+
}
|
|
64
|
+
if (current && prior && prior.status !== 'open') {
|
|
65
|
+
current.reopenReason = 'Current independent rule assessment no longer supports the prior resolution.';
|
|
66
|
+
current.reopenEvidenceRefs = (review.findings || []).find(row => row.id === id)?.reopenEvidenceRefs || [];
|
|
67
|
+
}
|
|
61
68
|
}
|
|
62
69
|
return { ...review, rules, findings, resolutions, passed: review.passed && complete, needsHuman: review.needsHuman && complete,
|
|
63
70
|
ruleCoverage: { total: inventory.length, mode: selective ? 'applicable' : 'full', unevaluated: inventory.length - rules.length, assessed: ids.length - missing.length, missing: missing.length, unresolved: unresolved.length, unknownIds: unknown,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: delivery-loop
|
|
3
|
-
description: Continue an explicitly requested SIMY Delivery Loop
|
|
3
|
+
description: Continue an explicitly requested SIMY Delivery Loop with bounded checkpoints, evidence-based autonomous recovery, and preserved native permissions and user stops.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# SIMY Delivery Loop
|
|
@@ -39,7 +39,7 @@ For missing information use `blocker: "missing_information"`. For a failed attem
|
|
|
39
39
|
For completed work use `{"status":"completed","summary":"Actual result","acceptanceMet":true,"evidence":["Exact checks and observed results"]}` only after checking the user's acceptance criteria. For standard tracking this ends as `reported_done`, never verified `completed`. In managed tracking SIMY independently checks frozen file predicates or waits for explicit user verification of manual criteria. Evidence is a report, not independent certification: preserve failed checks and deployment gaps. The hook must not infer success from optimistic closing text.
|
|
40
40
|
|
|
41
41
|
4. The Stop hook evaluates checkpoints and supplies the next instruction within the original scope while progress is verified. Missing checkpoints count toward the repeated-failure circuit. A human interrupt stops the loop. Approval/input events block it. Provider errors/exits may require reopening the existing conversation; these hooks cannot restart a dead provider process.
|
|
42
|
-
5. Once a blocker is resolved with current evidence, continue already-authorized work without an additional continuation request. AI handles reversible decisions by investigating, selecting and verifying a suitable option; use LLM judgment first and HC-01–HC-102 as critique lenses. A matching irreversible-operation approval or necessary answer is sufficient to continue that operation. User-directed pause/stop requires an explicit resume; actual provider permissions, rejected actions and budgets remain authoritative.
|
|
42
|
+
5. Once a blocker is resolved with current evidence, continue already-authorized work without an additional continuation request. AI handles reversible decisions by investigating, selecting and verifying a suitable option; use LLM judgment first and HC-01–HC-102 as critique lenses. A matching irreversible-operation approval or necessary answer is sufficient to continue that operation. User-directed pause/stop requires an explicit resume; actual provider permissions, rejected actions and budgets remain authoritative. `simy delivery resume --provider <provider> --session <id>` retains task identity, usage, failure and progress history and does not reset existing hard time, turn or token budgets. A budget boundary is not a request for human judgment about an ordinary implementation choice. `simy delivery stop` permanently ends this loop. `simy delivery status` shows observed hook activity and loop states; old observations do not prove hooks remain enabled today.
|
|
43
43
|
|
|
44
44
|
If the user selected **Continue this conversation** in SIMY desktop, the exact session already has a managed task. Do not run `delivery start` or ask the user to restate its goal. Follow the Stop hook's distinct `session_understanding` contract first: inspect only the original conversation context, report the original user goal and grounded progress, and submit the JSON checkpoint via `--file -`. Do not execute the next assessed work action during understanding. If context is materially unclear, report one focused question and wait for a matching `session_answer`; generic Resume is insufficient. A model-reported finished goal without an independent check is shown as `reported_done`, not verified `completed`.
|
|
45
45
|
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// Only fixed controller schema messages may be sent back to an AI worker.
|
|
2
|
+
// Provider bodies, user text and arbitrary exception messages remain private.
|
|
3
|
+
const known = new Set([
|
|
4
|
+
"A concrete nextStep is required.",
|
|
5
|
+
"A finding cannot be open and resolved.",
|
|
6
|
+
"A provider and exact session ID are required.",
|
|
7
|
+
"A structured session understanding is required.",
|
|
8
|
+
"Acceptance file must be relative to the task directory.",
|
|
9
|
+
"Already-finished understanding must have no remaining action.",
|
|
10
|
+
"An assessed plan needs a diagnosis and one to five alternatives.",
|
|
11
|
+
"An outcome already satisfied without changes permits only read-only verification.",
|
|
12
|
+
"Answer the current session question before resuming.",
|
|
13
|
+
"Association job changed; checkpoint discarded.",
|
|
14
|
+
"Association lease changed; checkpoint discarded.",
|
|
15
|
+
"Authorization blocker requires a scoped approval request.",
|
|
16
|
+
"Authorization request must match the selected assessed option.",
|
|
17
|
+
"Authorization request requires the proposed nextStep.",
|
|
18
|
+
"Automatic continuation requires the original verified provider channel.",
|
|
19
|
+
"Automatic continuation was disabled before enrollment.",
|
|
20
|
+
"Checkpoint needs an actionResult bound to the selected decision.",
|
|
21
|
+
"Checkpoint status must be in_progress, completed, blocked, or error.",
|
|
22
|
+
"Checkpoint summary is required.",
|
|
23
|
+
"Claude live hook changed before enrollment; refresh the session.",
|
|
24
|
+
"Clear understanding needs an objective, remaining work, and an unambiguous next action.",
|
|
25
|
+
"Clear understanding requires an observed read result.",
|
|
26
|
+
"Completion requires acceptanceMet and concrete verification evidence.",
|
|
27
|
+
"Confirm the exact external dependency condition before resuming.",
|
|
28
|
+
"Delivery Loop not found.",
|
|
29
|
+
"Delivery level needs the user selection reference, requested/effective levels 1-5, and assurance-ascending-v2 numbering.",
|
|
30
|
+
"Delivery state exceeds 256 KiB; reduce task details.",
|
|
31
|
+
"Delivery state is busy; retry the command.",
|
|
32
|
+
"Delivery state lock owner is unavailable; inspect the lock before retrying.",
|
|
33
|
+
"Delivery state reclaim lock requires inspection.",
|
|
34
|
+
"Dependency wait changed or confirmation is empty; refresh its status.",
|
|
35
|
+
"Duplicate acceptance criterion ID.",
|
|
36
|
+
"Duplicate plan option ID.",
|
|
37
|
+
"Each acceptance criterion needs a description.",
|
|
38
|
+
"Empty purpose observation.",
|
|
39
|
+
"Every purpose rule needs one disposition.",
|
|
40
|
+
"Exact session changed before automatic continuation.",
|
|
41
|
+
"External dependency needs waitCondition {condition, owner, nextCheck} and a nextStep.",
|
|
42
|
+
"Finding ledger capacity reached; reconcile existing finding IDs.",
|
|
43
|
+
"Finding resolution needs a known ID and independent evidence.",
|
|
44
|
+
"Finding resolution needs current captured finding-specific evidence references.",
|
|
45
|
+
"Hook input too large.",
|
|
46
|
+
"Independent review lease changed.",
|
|
47
|
+
"Independent review left unresolved findings.",
|
|
48
|
+
"Independent review provenance is missing.",
|
|
49
|
+
"Inferred file check needs a relative task path.",
|
|
50
|
+
"Inferred file check needs expected text.",
|
|
51
|
+
"Inferred file_equals needs bounded exact UTF-8 text.",
|
|
52
|
+
"Inferred session goal needs a user-message basis.",
|
|
53
|
+
"Initial action must be the selected assessed option.",
|
|
54
|
+
"Invalid acceptance criterion ID.",
|
|
55
|
+
"Invalid actionDueAt timestamp.",
|
|
56
|
+
"Invalid approval policy.",
|
|
57
|
+
"Invalid autoRecoverySandbox.",
|
|
58
|
+
"Invalid autonomy policy.",
|
|
59
|
+
"Invalid delivery budget.",
|
|
60
|
+
"Invalid managed task specification.",
|
|
61
|
+
"Invalid plan option ID.",
|
|
62
|
+
"Invalid purpose rule receipt.",
|
|
63
|
+
"Invalid requirement updates.",
|
|
64
|
+
"Invalid resumeModel.",
|
|
65
|
+
"Invalid work unit metadata.",
|
|
66
|
+
"Loop is stopped or awaiting a human resume.",
|
|
67
|
+
"Managed continuation requires a concrete proposedAction with kind, scope, and summary.",
|
|
68
|
+
"Managed recovery lease changed; checkpoint discarded.",
|
|
69
|
+
"Managed task identity changed; refresh its status.",
|
|
70
|
+
"Managed task is not awaiting acceptance verification.",
|
|
71
|
+
"Managed task is paused or awaiting acceptance verification.",
|
|
72
|
+
"Managed tasks require acceptanceCriteria.",
|
|
73
|
+
"Missing information blocker needs a specific user question.",
|
|
74
|
+
"No active session understanding turn.",
|
|
75
|
+
"No matching active Delivery Loop.",
|
|
76
|
+
"No matching verified provider session observed here. Select an exact discovered session or explicitly bind its task execution directory.",
|
|
77
|
+
"Objective and concrete next step are required.",
|
|
78
|
+
"Observed action result needs concrete evidence.",
|
|
79
|
+
"Only a blocked, paused, or recoverable loop can resume.",
|
|
80
|
+
"Only a nonterminal assessed managed task can change autonomy policy.",
|
|
81
|
+
"Only a running or queued managed task can pause.",
|
|
82
|
+
"Only manual criteria can be verified by the user.",
|
|
83
|
+
"Only the exact nonterminal ordinary loop can be upgraded; resolve its blocker or resume its original task first.",
|
|
84
|
+
"Original budget exhausted; automatic continuation cannot reset it.",
|
|
85
|
+
"Pause a managed task before changing its continuation policy.",
|
|
86
|
+
"Pause the task and supply an explicit user purpose revision with sourceRef, objective and acceptanceCriteria.",
|
|
87
|
+
"Pending approval changed; refresh its status.",
|
|
88
|
+
"Plan recommendation must identify an assessed option and reason.",
|
|
89
|
+
"Progress continuation requires frozen managed acceptance criteria.",
|
|
90
|
+
"Proposed action differs from selected assessed option.",
|
|
91
|
+
"Provider token budgets require the host-observed Codex app-server channel.",
|
|
92
|
+
"Purpose continuation is not enabled.",
|
|
93
|
+
"Purpose priorities are required.",
|
|
94
|
+
"Purpose priorities must rank every option and select the highest priority.",
|
|
95
|
+
"Purpose review is required.",
|
|
96
|
+
"Purpose review needs an explicit no-change assessment.",
|
|
97
|
+
"Purpose review needs bounded observations.",
|
|
98
|
+
"Purpose revision history requires archival before another revision.",
|
|
99
|
+
"Read-only recovery diagnosis needs human review.",
|
|
100
|
+
"Read-only understanding cannot be dispatched under current policy.",
|
|
101
|
+
"Recovery diagnosis needs an enrolled task scope.",
|
|
102
|
+
"Reopening a closed finding needs new independently observed counterevidence linked to the finding, not a paraphrased reason or missing observation.",
|
|
103
|
+
"Replanning needs a supported original-session channel, a rejected assessed approval or safe research wait, and available budget.",
|
|
104
|
+
"Replanning needs an enrolled task scope.",
|
|
105
|
+
"Required unit resolution needs independent criterion-linked evidence.",
|
|
106
|
+
"Requirement update must reference an enrolled criterion or canonical rule.",
|
|
107
|
+
"Requirement updates need source ID, observation, owner and reason.",
|
|
108
|
+
"Research replanning requires the original verified live hook owner.",
|
|
109
|
+
"Resolve the exact pending approval before resuming.",
|
|
110
|
+
"Restore the original provider conversation before changing Delivery Loop.",
|
|
111
|
+
"Restore the original provider conversation before starting Delivery Loop.",
|
|
112
|
+
"Retained candidates require explicit archival before another purpose revision.",
|
|
113
|
+
"Review finding must identify a known requirement, source, owner, observation and verification.",
|
|
114
|
+
"Safe read diagnosis is unavailable; keep the waiting action blocked.",
|
|
115
|
+
"Select an explicit continuation policy.",
|
|
116
|
+
"Selected action must match an assessed option.",
|
|
117
|
+
"Selected action requires a fresh scoped user decision.",
|
|
118
|
+
"Session question changed or answer is empty; refresh its status.",
|
|
119
|
+
"Session understanding deadline expired.",
|
|
120
|
+
"Session understanding has too many items.",
|
|
121
|
+
"Session understanding lease changed; checkpoint discarded.",
|
|
122
|
+
"Session understanding needs its separate structured checkpoint.",
|
|
123
|
+
"Session understanding requires an assessed read-only initial action.",
|
|
124
|
+
"Specify authorization, missing_information, or external_dependency.",
|
|
125
|
+
"State exceeds limit.",
|
|
126
|
+
"Task execution directory must be a directory.",
|
|
127
|
+
"Task handoff is locked; continue in the received target session.",
|
|
128
|
+
"Task handoff is locked; use delivery handoff_cancel or retry the same target to complete the transfer.",
|
|
129
|
+
"This session already has a Delivery Loop. Resume or stop it; budgets cannot be reset by starting again.",
|
|
130
|
+
"This task cannot change supervision while completed, executing, or handed off.",
|
|
131
|
+
"Unclear understanding needs one focused question.",
|
|
132
|
+
"Understanding basis needs a reference or excerpt.",
|
|
133
|
+
"Understanding basis needs a source.",
|
|
134
|
+
"Understanding checkpoint needs a summary.",
|
|
135
|
+
"Understanding criterion needs a description and provenance.",
|
|
136
|
+
"Understanding needs bounded context evidence.",
|
|
137
|
+
"Understanding result must match the selected read decision.",
|
|
138
|
+
"Understood next action differs from selected assessed option.",
|
|
139
|
+
"Unit belongs to an old purpose revision.",
|
|
140
|
+
"Unsupported acceptance check.",
|
|
141
|
+
"Unsupported control.",
|
|
142
|
+
"Unsupported inferred acceptance check.",
|
|
143
|
+
"Upgrade must preserve the original objective and task directory.",
|
|
144
|
+
"Upgrade requires read-only reconciliation and frozen acceptance of the original objective.",
|
|
145
|
+
"User-grounded criterion needs a user-message basis.",
|
|
146
|
+
"Waiting recovery needs an enrolled scope.",
|
|
147
|
+
"Work frontier needs reconciliation before more dispatch.",
|
|
148
|
+
"Work unit references unknown criterion.",
|
|
149
|
+
"file_contains needs expected text.",
|
|
150
|
+
"file_equals needs bounded exact UTF-8 text."
|
|
151
|
+
]);
|
|
152
|
+
export function safeCheckpointDiagnostic(error) {
|
|
153
|
+
const message = error?.message;
|
|
154
|
+
if (known.has(message)) return message;
|
|
155
|
+
if (['gap','hypothesis','refutation','necessity'].some(key => message === `Purpose review needs ${key}.`)) return message;
|
|
156
|
+
return 'Checkpoint did not satisfy the controller schema; reread the supplied checkpoint contract and preserve the same task and action identity.';
|
|
157
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { linkSessionParent } from '../session-supervision/session-lineage.js';
|
|
1
2
|
import { adoptAutonomousPolicy } from '../session-supervision/autonomous-upgrade.js';
|
|
2
3
|
import { deliveryEvidence } from './evidence.js';
|
|
3
4
|
import { readFile } from 'node:fs/promises';
|
|
@@ -79,6 +80,7 @@ export async function runDeliveryCommand(argv, { root = rootDirectory(), cwd = p
|
|
|
79
80
|
output('simy delivery runtime install|status|workflows — install an immutable local candidate, inspect its version/digest, or refresh existing plugin sources/cache while preserving enabled state and hook trust. Never enables a task.');
|
|
80
81
|
output('simy delivery runtime doctor|desktop-install|desktop-activate|watch — audit live host/state/monitor alignment, or prepare a separate local desktop using the verified fixed runtime. Does not start or resume tasks.');
|
|
81
82
|
output('simy delivery runtime policy-adopt [--task-id ID [--criterion-id ID]] — explicitly delegate decisions within existing permissions. Manual acceptance stays manual unless the user names the exact task and criterion to delegate its verifier.');
|
|
83
|
+
output('simy delivery link_parent --provider codex|claude --session CHILD --parent-provider codex|claude --parent-session PARENT — record an explicit observed navigation relationship without starting work.');
|
|
82
84
|
output('simy delivery supervise [--once] — service already opted-in tasks with this runtime; refuses an active desktop host.');
|
|
83
85
|
output('simy delivery evidence --provider codex|claude --session ID — read-only execution and acceptance receipts.');
|
|
84
86
|
output('simy delivery activity [--watch] [--json] [--provider codex|claude] [--session ID] — local observations every 60 seconds; not productive time.');
|
|
@@ -88,11 +90,12 @@ export async function runDeliveryCommand(argv, { root = rootDirectory(), cwd = p
|
|
|
88
90
|
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.');
|
|
89
91
|
return 0;
|
|
90
92
|
}
|
|
91
|
-
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']);
|
|
93
|
+
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', 'parent-provider', 'parent-session']);
|
|
92
94
|
const options = parseDeliveryOptions(args, allowed);
|
|
93
95
|
const base = { root, cwd, provider: options.provider, session: options.session };
|
|
94
96
|
let result;
|
|
95
|
-
if (action === '
|
|
97
|
+
if (action === 'link_parent') result = await linkSessionParent({ ...base, parentProvider: options['parent-provider'], parentSession: options['parent-session'] });
|
|
98
|
+
else if (action === 'evidence') {
|
|
96
99
|
const record = await readRecord(root, options.provider, options.session);
|
|
97
100
|
if (!record) throw new Error('Exact provider session record not found.');
|
|
98
101
|
result = deliveryEvidence(record, root);
|