@awak-app/simy-cli 0.7.16 → 0.7.18
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 +180 -26
- package/package.json +3 -2
- package/src/agent.js +128 -7
- package/src/bounded-local-task-subtask-pool.js +32 -6
- package/src/bounded-local-task-subtasks-contract.js +1 -1
- package/src/cli-contract.js +1 -1
- package/src/codex-plugins/simy/.codex-plugin/plugin.json +2 -2
- package/src/codex-plugins/simy/hooks/README.md +8 -3
- package/src/codex-plugins/simy/hooks/delivery-loop.mjs +261 -67
- package/src/codex-plugins/simy/hooks/hooks.json +1 -1
- package/src/codex-plugins/simy/hooks/loop-kernel/conformance.json +136 -0
- package/src/codex-plugins/simy/hooks/loop-kernel/index.mjs +42 -0
- package/src/codex-plugins/simy/hooks/loop-kernel/outcome-review.mjs +31 -0
- package/src/codex-plugins/simy/hooks/loop-kernel/pin.json +6 -0
- package/src/codex-plugins/simy/hooks/loop-kernel/policy.json +141 -0
- package/src/codex-plugins/simy/hooks/loop-kernel/task-pool.mjs +34 -0
- package/src/codex-plugins/simy/hooks/loop-kernel/work-frontier.mjs +132 -0
- package/src/codex-plugins/simy/hooks/loop-kernel.mjs +6 -0
- package/src/codex-plugins/simy/hooks/outcome-progress.mjs +15 -3
- package/src/codex-plugins/simy/hooks/purpose-completion-contract.mjs +10 -0
- package/src/codex-plugins/simy/hooks/purpose-evidence.mjs +8 -6
- package/src/codex-plugins/simy/hooks/result-cycle.mjs +32 -6
- package/src/codex-plugins/simy/hooks/work-frontier.mjs +2 -0
- package/src/codex-plugins/simy/skills/diagnose/SKILL.md +10 -4
- package/src/coding-run-recovery.js +4 -2
- package/src/delivery-hooks/claude-workflow.md +10 -7
- package/src/delivery-hooks/command.js +45 -10
- package/src/delivery-hooks/consult.js +60 -0
- package/src/delivery-hooks/diagnosis.js +2 -2
- package/src/delivery-hooks/legacy-hook-routing.js +46 -8
- package/src/delivery-hooks/local-runtime.js +50 -1
- package/src/delivery-hooks/local-supervisor.js +1 -1
- package/src/delivery-hooks/repair-continuation.js +80 -0
- package/src/delivery-hooks/repair.js +164 -0
- package/src/delivery-hooks/supervisor-service.js +3 -1
- package/src/delivery-hooks/supervisor.js +88 -12
- package/src/desktop-access.js +2 -1
- package/src/desktop-host.js +62 -2
- package/src/durable-local-task-worker.js +372 -58
- package/src/execution-capability-contract.js +15 -1
- package/src/index.js +3 -3
- package/src/local-photos-read.swift +33 -0
- package/src/local-photos.js +47 -0
- package/src/local-task-attachment-store.js +31 -0
- package/src/local-task-destination.js +174 -0
- package/src/local-task.js +119 -7
- package/src/native-delegation-contract.js +68 -0
- package/src/native-provider-identity.js +60 -0
- package/src/orchestrator/budget.js +22 -11
- package/src/orchestrator/independent-audit.js +9 -18
- package/src/orchestrator/loop.js +27 -59
- package/src/orchestrator/recovery.js +32 -2
- package/src/provider-delegation.js +336 -0
- package/src/runner.js +2 -1
- package/src/session-supervision/artifact-observer.js +138 -0
- package/src/session-supervision/claude-sessions.js +5 -3
- package/src/session-supervision/codex-app-server.js +20 -5
- package/src/session-supervision/completion-artifacts.js +44 -0
- package/src/session-supervision/consistency-auditor.js +8 -11
- package/src/session-supervision/continuation-analysis.js +46 -28
- package/src/session-supervision/discovery-window.js +1 -1
- package/src/session-supervision/final-response-recovery.js +5 -3
- package/src/session-supervision/index.js +25 -8
- package/src/session-supervision/purpose-recovery.js +12 -3
- package/src/session-supervision/purpose-verification.js +9 -57
- package/src/session-supervision/recovery-frontier.js +55 -0
- package/src/session-supervision/remote-hosts.js +1 -1
- package/src/session-supervision/stop-notifications.js +8 -8
- package/src/sqm/agent-skill.js +1 -1
- package/src/sqm/checkers.js +12 -6
- package/src/sqm/claude-plugin-update.js +22 -8
- package/src/sqm/claude-plugin.js +4 -0
- package/src/sqm/local-tests.js +2 -2
- package/src/sqm/plugin-cache.js +67 -0
- package/src/sqm/proof.js +25 -0
- package/src/sqm/report.js +1 -0
- package/src/sqm/workflow-onboarding.js +3 -39
- package/src/sqm/workflow-update.js +17 -6
- package/src/team-status-fence-store.js +32 -0
- package/src/team-threads.js +478 -0
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { decideLoop, compactWorkHistory } from './loop-kernel.mjs';
|
|
3
|
+
import { completionContractIdentity } from './purpose-completion-contract.mjs';
|
|
2
4
|
import { captureDispatchProvenance } from './update-dispatch-identity.mjs';
|
|
3
5
|
import { MAX_STATE_BYTES } from './state-limits.mjs';
|
|
4
|
-
import { continueResultCycle, resultEvent } from './result-cycle.mjs';
|
|
6
|
+
import { continueResultCycle, resultEvent, resultFollowThroughInstruction, correctiveFollowupInstruction } from './result-cycle.mjs';
|
|
5
7
|
import { currentRecoveryIncident, recordRecoveryIncident, recordRecoveryStage, settleRecoveryIncidents, resolveRecoveryReceipt } from './recovery-incident.mjs';
|
|
6
8
|
import { rememberPerspectives, perspectiveScope, recallPerspectives, perspectiveInstruction } from './perspective-memory.mjs';
|
|
7
9
|
import { recordHypothesisSheet } from './hypothesis-sheet.mjs';
|
|
@@ -276,7 +278,16 @@ export async function updateRecord(root, provider, session, mutate, { inspectPro
|
|
|
276
278
|
if (serialized === previousBytes) return next;
|
|
277
279
|
temporary = `${file}.${randomUUID()}.tmp`;
|
|
278
280
|
await writeFile(temporary, serialized, { mode: 0o600 });
|
|
279
|
-
|
|
281
|
+
// Windows readers or antivirus can briefly deny replacing the JSON.
|
|
282
|
+
// Keep the writer lock and the same complete payload while retrying;
|
|
283
|
+
// never rerun mutate or delete the last durable state to force a save.
|
|
284
|
+
for (let attempt = 0; ; attempt++) {
|
|
285
|
+
try { await rename(temporary, file); break; }
|
|
286
|
+
catch (error) {
|
|
287
|
+
if (error.code !== 'EPERM' || attempt >= 4) throw error;
|
|
288
|
+
await new Promise(resolve => setTimeout(resolve, 25 * (attempt + 1)));
|
|
289
|
+
}
|
|
290
|
+
}
|
|
280
291
|
}
|
|
281
292
|
return next;
|
|
282
293
|
} finally { if (temporary) await rm(temporary, { force: true }); await releaseStateLock(lock, lockToken); }
|
|
@@ -464,7 +475,7 @@ export function evaluateAssessedPlan(plan, policy, scopes, now = Date.now(), { a
|
|
|
464
475
|
const localWrite = action.kind === 'local_edit' || allowLocalTestWrites && action.kind === 'local_test' && facts.externalEffect === 'local';
|
|
465
476
|
const autonomous = policy === 'autonomous' && llmDelegated;
|
|
466
477
|
const external = autonomous && allowReversibleExternal && action.kind === 'external' && facts.externalEffect === 'external';
|
|
467
|
-
let reason = null, waitKind = 'research';
|
|
478
|
+
let reason = null, waitKind = autonomous ? 'research' : 'human';
|
|
468
479
|
if (!scopes.includes(action.scope)) { reason = 'Action scope is outside the task enrollment.'; waitKind = 'authority'; }
|
|
469
480
|
else if (facts.inObjective !== true) reason = 'Establish objective fit or choose work within the original task.';
|
|
470
481
|
else if (facts.reversibility === 'irreversible') { reason = 'Irreversible consequences require an exact user decision.'; waitKind = 'human'; }
|
|
@@ -495,11 +506,12 @@ export function canExecuteAutonomousExternal(managed) {
|
|
|
495
506
|
// Shared by live Stop hooks and cold exact-session recovery. Keep the result
|
|
496
507
|
// contract identical across transports; the host still verifies acceptance.
|
|
497
508
|
export function assessedCheckpointContract(decision, managed = null) {
|
|
498
|
-
const observationField = flexibleReasoning(managed) ? 'Plan diagnosis, recommendation and its reason are optional when the selected action is clear. Optional observations may be omitted; do not create evidence files, citations or extra reports. Actual tool results are captured by the controller.' : '
|
|
499
|
-
return `${minimalSufficientChangeInstruction({ turn: 'assessed' })}\nReturn ONLY one JSON object with top-level status:"in_progress|completed|blocked|error" and summary:"what happened". Use the key status, NEVER kind, for an ordinary work checkpoint. Record actionResult:{decisionId:"${decision.id}",status:"observed|failed|not_run",summary:"what actually happened"} for THIS selected action. ${observationField} ${actionReversibilityInstruction} This is provider-reported, not independent verification. For in_progress/error include nextStep, remainingWork, and plan:{diagnosis,alternatives:[{id,action:{kind,scope,summary},assessment:{risk,reversibility,externalEffect,blastRadius,uncertainty,inObjective,userChoice},expectedVerification,rollback}],selectedOptionId,recommendation:{optionId,reason},userQuestion?} for the NEXT action; select one assessed option but do not execute it in this turn. Valid action.kind values are exactly read, local_edit, local_test, external, destructive, scope_change, unknown; use local_edit for writing local files. Risk is low|medium|high|unknown; reversibility reversible|irreversible|unknown; externalEffect none|local|external|unknown; blastRadius single_file|repository|multi_system|unknown; uncertainty low|medium|high|unknown. Missing facts are unknown. Completion claims go to SIMY independent inspection of the actual frozen acceptance conditions; no worker-authored proof is certification. Blocked requires blocker set to exactly authorization, missing_information, or external_dependency, plus nextStep. For authorization, include an assessed plan and set approval to an exact copy of the selected plan alternative action object: kind, scope, and summary strings MUST be identical, with no paraphrasing.
|
|
509
|
+
const observationField = flexibleReasoning(managed) ? 'Plan diagnosis, recommendation and its reason are optional when the selected action is clear. Optional observations may be omitted; do not create evidence files, citations or extra reports. Actual tool results are captured by the controller.' : 'Return the observed result; evidence attachments and extra reports are optional.';
|
|
510
|
+
return `${minimalSufficientChangeInstruction({ turn: 'assessed' })}\nReturn ONLY one JSON object with top-level status:"in_progress|completed|blocked|error" and summary:"what happened". Use the key status, NEVER kind, for an ordinary work checkpoint. Record actionResult:{decisionId:"${decision.id}",status:"observed|failed|not_run",summary:"what actually happened"} for THIS selected action. ${observationField} ${actionReversibilityInstruction} This is provider-reported, not independent verification. For in_progress/error include nextStep, remainingWork, and plan:{diagnosis,alternatives:[{id,action:{kind,scope,summary},assessment:{risk,reversibility,externalEffect,blastRadius,uncertainty,inObjective,userChoice},expectedVerification,rollback}],selectedOptionId,recommendation:{optionId,reason},userQuestion?} for the NEXT action; select one assessed option but do not execute it in this turn. Valid action.kind values are exactly read, local_edit, local_test, external, destructive, scope_change, unknown; use local_edit for writing local files. Risk is low|medium|high|unknown; reversibility reversible|irreversible|unknown; externalEffect none|local|external|unknown; blastRadius single_file|repository|multi_system|unknown; uncertainty low|medium|high|unknown. Missing facts are unknown. Successful diagnosis or repair is only an action result, not completion of the original user task. If original work remains, return in_progress with remainingWork and the next assessed plan, preserving the original objective, corrections and acceptance criteria. Do not repeat effects already performed. Completion claims go to SIMY independent inspection of the actual frozen acceptance conditions; no worker-authored proof is certification. Blocked requires blocker set to exactly authorization, missing_information, or external_dependency, plus nextStep. For authorization, include an assessed plan and set approval to an exact copy of the selected plan alternative action object: kind, scope, and summary strings MUST be identical, with no paraphrasing. ${informationWaitInstruction} external_dependency requires waitCondition:{condition,owner,nextCheck} with the observable resumption condition, dependency owner, and bounded recheck instruction, plus nextStep.`;
|
|
511
|
+
|
|
500
512
|
}
|
|
501
513
|
export function understandingCheckpointContract(decision) {
|
|
502
|
-
return `${minimalSufficientChangeInstruction({ turn: 'understanding' })}\nThis is an UNDERSTANDING turn, not a work turn. Read the existing conversation context and available read-only evidence; ${decision.purpose ? "Authorized read-only API and web research is allowed within existing provider permissions; do not send private material to external services. Do not edit files, write to external services, or start the proposed next action." : "do not edit files, call external services, or start the proposed next action."} Return ONLY JSON {kind:"session_understanding",summary:"...",actionResult:{decisionId:"${decision.id}",status:"observed|failed|not_run",summary:"...",evidence:["..."]},sessionUnderstanding:{state:"clear|ambiguous|context_missing|already_done",objective:"current user goal",constraints:["..."],done:["..."],remaining:["..."],nextAction:{kind:"read|local_edit|local_test|external|destructive|scope_change|unknown",scope:"task",summary:"..."},criteria:[{description:"...",source:"user_grounded|proposed",basisIndex:0,check:{type:"manual|semantic|file_exists|file_contains|file_equals",path:"relative/path",text:"expected text"}}],ambiguities:["..."],question:"one focused user question if unclear",basis:[{source:"user|assistant|artifact",turnRef:"if available",snippet:"short relevant excerpt"}]},plan:{diagnosis:"...",alternatives:[{id:"...",action:{kind,scope,summary},assessment:{risk,reversibility,externalEffect,blastRadius,uncertainty,inObjective,userChoice},expectedVerification:"...",rollback:"..."}],selectedOptionId:"...",recommendation:{optionId:"...",reason:"..."}}}. For clear, choose one NEXT action but do not execute it. Set sessionUnderstanding.nextAction to an exact copy of the selected plan alternative's action object: kind, scope, and summary strings MUST be identical, with no paraphrasing. Complete every known assessment field: risk low|medium|high|unknown; reversibility reversible|irreversible|unknown; externalEffect none|local|external|unknown; blastRadius single_file|repository|multi_system|unknown; uncertainty low|medium|high|unknown; inObjective and userChoice booleans. For a read-only single-file action with no effects, use reversible, none, single_file when those facts are known; unknown facts require AI investigation; reversible preferences are delegated under autonomous policy. Write objective and criteria in the original user's language. Lead objective with the intended user experience (who can do what in which situation), when grounded in the request. Criteria must describe observable outcomes for that experience, not merely code written or tests passing. Keep technical-only requests technical; do not invent new UX obligations. Preserve done and remaining as the recorded stages of the whole requested work, distinguishing reported implementation from verified outcomes. Use only the original user's goal; constraints[] must contain only constraints from the original user, not these SIMY turn instructions. Do not invent new work or acceptance obligations. Include a user-grounded criterion only with a basisIndex pointing to a user message. Use file_equals for a user-requested file whose complete UTF-8 contents must be exact (including whitespace/newlines), file_contains only for a requested substring, and file_exists only for existence. ${decision.search ? "For semantic outcomes use check.type semantic; reserve manual for an explicit requirement for a human decision. Missing verification tooling is work to do, not a manual condition." : "For semantic conditions use manual."} Never emit file_equals with null, omitted, guessed, or placeholder text. If exact expected contents are unknown, choose read to learn them and use semantic for purpose search or manual otherwise. Do not invent a file check from repository hints alone. If the original work is already done, choose already_done with remaining:[], nextAction:null, no plan, and no new goal. First recover missing context with authorized reads and infer reversible defaults from the original request. Select read investigation when facts remain unknown. Only if the original goal cannot be grounded at all or irreversible personal consequences require a decision, set one ambiguity and one focused question, nextAction:null, and omit plan. Provider statements about past work and semantic success are not independent verification. Optionally include sessionUnderstanding.matchingContext:{deliverables:["concrete requested output"],projectHints:["project name"],repositoryUrls:[],relatedUrls:[],actionIds:[],sourceRefs:[{platform:"slack",externalThreadId:"exact thread",channelExternalId:"exact channel"}]}. Extract only explicit conversation evidence; IDs and sources are retrieval signals, never authenticated identity. Do not infer URLs or identifiers.`;
|
|
514
|
+
return `${minimalSufficientChangeInstruction({ turn: 'understanding' })}\n${correctiveFollowupInstruction}\nThis is an UNDERSTANDING turn, not a work turn. Read the existing conversation context and available read-only evidence; ${decision.purpose ? "Authorized read-only API and web research is allowed within existing provider permissions; do not send private material to external services. Do not edit files, write to external services, or start the proposed next action." : "do not edit files, call external services, or start the proposed next action."} Return ONLY JSON {kind:"session_understanding",summary:"...",actionResult:{decisionId:"${decision.id}",status:"observed|failed|not_run",summary:"...",evidence:["..."]},sessionUnderstanding:{state:"clear|ambiguous|context_missing|already_done",objective:"current user goal",constraints:["..."],done:["..."],remaining:["..."],nextAction:{kind:"read|local_edit|local_test|external|destructive|scope_change|unknown",scope:"task",summary:"..."},criteria:[{description:"...",source:"user_grounded|proposed",basisIndex:0,check:{type:"manual|semantic|file_exists|file_contains|file_equals",path:"relative/path",text:"expected text"}}],ambiguities:["..."],question:"one focused user question if unclear",basis:[{source:"user|assistant|artifact",turnRef:"if available",snippet:"short relevant excerpt"}]},plan:{diagnosis:"...",alternatives:[{id:"...",action:{kind,scope,summary},assessment:{risk,reversibility,externalEffect,blastRadius,uncertainty,inObjective,userChoice},expectedVerification:"...",rollback:"..."}],selectedOptionId:"...",recommendation:{optionId:"...",reason:"..."}}}. For clear, choose one NEXT action but do not execute it. Set sessionUnderstanding.nextAction to an exact copy of the selected plan alternative's action object: kind, scope, and summary strings MUST be identical, with no paraphrasing. Complete every known assessment field: risk low|medium|high|unknown; reversibility reversible|irreversible|unknown; externalEffect none|local|external|unknown; blastRadius single_file|repository|multi_system|unknown; uncertainty low|medium|high|unknown; inObjective and userChoice booleans. For a read-only single-file action with no effects, use reversible, none, single_file when those facts are known; unknown facts require AI investigation; reversible preferences are delegated under autonomous policy. Write objective and criteria in the original user's language. Lead objective with the intended user experience (who can do what in which situation), when grounded in the request. Criteria must describe observable outcomes for that experience, not merely code written or tests passing. Keep technical-only requests technical; do not invent new UX obligations. Preserve done and remaining as the recorded stages of the whole requested work, distinguishing reported implementation from verified outcomes. Use only the original user's goal; constraints[] must contain only constraints from the original user, not these SIMY turn instructions. Do not invent new work or acceptance obligations. Include a user-grounded criterion only with a basisIndex pointing to a user message. Use file_equals for a user-requested file whose complete UTF-8 contents must be exact (including whitespace/newlines), file_contains only for a requested substring, and file_exists only for existence. ${decision.search ? "For semantic outcomes use check.type semantic; reserve manual for an explicit requirement for a human decision. Missing verification tooling is work to do, not a manual condition." : "For semantic conditions use manual."} Never emit file_equals with null, omitted, guessed, or placeholder text. If exact expected contents are unknown, choose read to learn them and use semantic for purpose search or manual otherwise. Do not invent a file check from repository hints alone. If the original work is already done, choose already_done with remaining:[], nextAction:null, no plan, and no new goal. First recover missing context with authorized reads and infer reversible defaults from the original request. Select read investigation when facts remain unknown. Only if the original goal cannot be grounded at all or irreversible personal consequences require a decision, set one ambiguity and one focused question, nextAction:null, and omit plan. Provider statements about past work and semantic success are not independent verification. Optionally include sessionUnderstanding.matchingContext:{deliverables:["concrete requested output"],projectHints:["project name"],repositoryUrls:[],relatedUrls:[],actionIds:[],sourceRefs:[{platform:"slack",externalThreadId:"exact thread",channelExternalId:"exact channel"}]}. Extract only explicit conversation evidence; IDs and sources are retrieval signals, never authenticated identity. Do not infer URLs or identifiers.`;
|
|
503
515
|
}
|
|
504
516
|
function boundedContextList(value, maxItems = 8, maxLength = 300) {
|
|
505
517
|
if (!Array.isArray(value) || value.length > maxItems) throw new Error('Session understanding has too many items.');
|
|
@@ -516,7 +528,9 @@ function boundedMatchingContext(value) {
|
|
|
516
528
|
}
|
|
517
529
|
function sessionUnderstanding(value) {
|
|
518
530
|
if (!value || typeof value !== 'object' || Array.isArray(value) || !['clear', 'ambiguous', 'context_missing', 'already_done'].includes(value.state)) throw new Error('A structured session understanding is required.');
|
|
519
|
-
|
|
531
|
+
// The checkpoint already has a byte limit. Preserve valid user references
|
|
532
|
+
// and their indices instead of discarding all context when there are >6.
|
|
533
|
+
const basis = Array.isArray(value.basis) ? value.basis.map(item => {
|
|
520
534
|
if (!['user', 'assistant', 'artifact'].includes(item?.source)) throw new Error('Understanding basis needs a source.');
|
|
521
535
|
const turnRef = contextText(item.turnRef, 100) || null;
|
|
522
536
|
const snippet = contextText(item.snippet, 240) || null;
|
|
@@ -588,11 +602,13 @@ function recordPurposeProgress(run, now, countAttempt = true) {
|
|
|
588
602
|
}
|
|
589
603
|
function recordDecision(managed, plan, now) {
|
|
590
604
|
let decision = evaluateAssessedPlan(plan, managed.autonomyPolicy, managed.approvalScopes, now, { allowLocalTestWrites: searchEnabled(managed), llmDelegated: searchEnabled(managed), allowReversibleExternal: searchEnabled(managed) && managed.resumeChannel === 'codex_app_server' });
|
|
591
|
-
|
|
605
|
+
const dependencyPending = option => (option?.dependsOn || []).some(id =>
|
|
606
|
+
!managed.purpose?.evidence?.units?.some(unit => unit.id === id && unit.status === 'verified' && unit.purposeRevision === managed.purpose.evidence.purposeRevision));
|
|
607
|
+
if (searchEnabled(managed) && (decision.outcome === 'wait' || dependencyPending(plan.alternatives.find(row => row.id === plan.selectedOptionId)))) {
|
|
592
608
|
rememberSearchWait(managed, decision.waitKind === 'research' ? 'research' : 'authorization', { action: decision.action, reason: decision.reason }, now);
|
|
593
609
|
const priorities = new Map((managed.purpose.lastReview?.priorities || []).map(row => [row.optionId, row.rank]));
|
|
594
610
|
for (const candidate of [...plan.alternatives].sort((a, b) => (priorities.get(a.id) ?? 99) - (priorities.get(b.id) ?? 99))) {
|
|
595
|
-
if (candidate.id === plan.selectedOptionId || !candidate.independent) continue;
|
|
611
|
+
if (candidate.id === plan.selectedOptionId || !candidate.independent || dependencyPending(candidate)) continue;
|
|
596
612
|
const alternative = { ...plan, selectedOptionId: candidate.id, recommendation: { optionId: candidate.id, reason: 'Continue the highest listed eligible independent alternative.' } };
|
|
597
613
|
const assessed = evaluateAssessedPlan(alternative, managed.autonomyPolicy, managed.approvalScopes, now, { allowLocalTestWrites: searchEnabled(managed), llmDelegated: searchEnabled(managed), allowReversibleExternal: searchEnabled(managed) && managed.resumeChannel === 'codex_app_server' });
|
|
598
614
|
if (assessed.outcome !== 'wait' && !managed.rejectedActionOverflow && !managed.rejectedActionDigests?.includes(digest(JSON.stringify(assessed.action)))) {
|
|
@@ -625,8 +641,7 @@ function recordDecision(managed, plan, now) {
|
|
|
625
641
|
writeResources: option?.writeResources || (decision.action.kind === 'read' ? [] : ['task']),
|
|
626
642
|
requiredCapabilities: option?.requiredCapabilities || [decision.action.kind], requiredPermissions: option?.requiredPermissions || [decision.action.scope],
|
|
627
643
|
status: decision.outcome === 'wait' ? 'waiting' : 'planned', evidenceRefs: [], purposeRevision: ledger.purposeRevision || 1, action: decision.action, createdAt: now });
|
|
628
|
-
|
|
629
|
-
if (ledger.units.length > 192) throw new Error('Work frontier needs reconciliation before more dispatch.');
|
|
644
|
+
compactWorkHistory(ledger);
|
|
630
645
|
}
|
|
631
646
|
managed.plan = plan; managed.decision = decision; managed.proposedAction = decision.action;
|
|
632
647
|
managed.decisionHistory ||= [];
|
|
@@ -752,7 +767,63 @@ export function deliveryAcceptanceContext(run) {
|
|
|
752
767
|
`${run.managed?.deliveryLevel ? ` Frozen delivery level: ${JSON.stringify(run.managed.deliveryLevel)}.` : ''}` +
|
|
753
768
|
` Pending acceptance (last recorded checks): ${JSON.stringify(pendingAcceptance(run))}. Continue only within the original request and permissions; manual checks require the user. SQM is optional and 0 applicable checks do not establish quality acceptance.`;
|
|
754
769
|
}
|
|
755
|
-
|
|
770
|
+
// CLI automation is the default for connected providers with executing hooks.
|
|
771
|
+
// Saved workspace overrides and revoked provider consent take precedence.
|
|
772
|
+
async function automaticPath(root, provider, cwd) {
|
|
773
|
+
if (!['codex', 'claude'].includes(provider)) throw new Error('Choose --provider codex|claude.');
|
|
774
|
+
cwd = await realpath(cwd);
|
|
775
|
+
return { cwd, file: join(root, 'automatic', `${digest(`${provider}\0${cwd}`)}.json`) };
|
|
776
|
+
}
|
|
777
|
+
async function withAutomaticLock(root, provider, cwd, operation) {
|
|
778
|
+
const location = await automaticPath(root, provider, cwd);
|
|
779
|
+
await mkdir(dirname(location.file), { recursive: true, mode: 0o700 });
|
|
780
|
+
const lock = `${location.file}.lock`, token = await waitForStateLock(lock, processIdentity);
|
|
781
|
+
try { return await operation(location); } finally { await releaseStateLock(lock, token); }
|
|
782
|
+
}
|
|
783
|
+
export async function automaticDeliveryStatus({ root = rootDirectory(), provider, cwd = process.cwd() }) {
|
|
784
|
+
const location = await automaticPath(root, provider, cwd);
|
|
785
|
+
const saved = await read(location.file);
|
|
786
|
+
const link = await providerLinkState(root, provider);
|
|
787
|
+
const enabled = link.connected && (saved === null ? !link.requiresConsent
|
|
788
|
+
: saved.schemaVersion === 1 && saved.provider === provider && saved.cwd === location.cwd &&
|
|
789
|
+
saved.enabled === true && saved.providerLinkEpoch === link.epoch);
|
|
790
|
+
return { provider, cwd: location.cwd, enabled: Boolean(enabled), source: saved === null ? 'cli_default' : 'workspace',
|
|
791
|
+
reason: enabled ? 'The next nonempty prompt in a conversation without a loop starts Delivery Loop.' : 'Automatic start is disabled by a saved override or provider consent; it will not be restored by updates.',
|
|
792
|
+
prerequisite: 'SIMY plugin hooks must be enabled in the provider. This setting does not grant hook trust.',
|
|
793
|
+
existingLoops: 'Existing loops, pauses, stops and completed tasks are preserved.' };
|
|
794
|
+
}
|
|
795
|
+
export async function setAutomaticDelivery({ root = rootDirectory(), provider, cwd = process.cwd(), enabled }) {
|
|
796
|
+
if (typeof enabled !== 'boolean') throw new Error('Automatic start requires a boolean choice.');
|
|
797
|
+
return withAutomaticLock(root, provider, cwd, async location => {
|
|
798
|
+
const link = await providerLinkState(root, provider);
|
|
799
|
+
if (enabled && !link.connected) throw new Error('Reconnect this provider to SIMY before enabling automatic start.');
|
|
800
|
+
const temporary = `${location.file}.${randomUUID()}.tmp`;
|
|
801
|
+
try {
|
|
802
|
+
await writeFile(temporary, JSON.stringify({ schemaVersion: 1, provider, cwd: location.cwd, enabled,
|
|
803
|
+
providerLinkEpoch: link.epoch }), { mode: 0o600 });
|
|
804
|
+
await rename(temporary, location.file);
|
|
805
|
+
} finally { await rm(temporary, { force: true }); }
|
|
806
|
+
return automaticDeliveryStatus({ root, provider, cwd: location.cwd });
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
function initialDeliveryRun(objective, nextStep, now, maxFailures) {
|
|
810
|
+
return { id: randomUUID(), objective: text(objective), state: 'active', nextStep: text(nextStep),
|
|
811
|
+
startedAt: now, updatedAt: now, maxFailures: positive(maxFailures, 3, 10),
|
|
812
|
+
maxFailuresSource: maxFailures === undefined ? 'default' : 'explicit', continuations: 0, failures: 0,
|
|
813
|
+
revision: 0, consumedRevision: -1, checkpoint: null, reason: 'started', managed: null };
|
|
814
|
+
}
|
|
815
|
+
function hookDeliveryContext(record, automatic = false) {
|
|
816
|
+
const { provider, session, run } = record;
|
|
817
|
+
const identity = `SIMY connected to this ${provider} conversation: ${session}.`;
|
|
818
|
+
if (!run && automatic && !record.automaticStartBlocked && !record.providerArchived && !record.parentSession && !(record.autoContinue === false && record.autoContinueEpoch)) return `${identity} Automatic Delivery Loop start is enabled for this workspace. The next nonempty user prompt starts checkpoint tracking in this conversation; do not issue a duplicate start. Provider permissions and user stops remain authoritative.`;
|
|
819
|
+
if (!run) return `${identity} Delivery Loop has not started. When the user requests it, use simy delivery start --provider ${provider} --session ${session} with the agreed objective and next step. For future conversations in this workspace, the user can enable automatic start once with simy delivery auto on --provider ${provider}. Never enable, trust, resume, or reset a loop without the user's request.`;
|
|
820
|
+
return `${identity} Existing loop: ${run.state}. ${deliveryAcceptanceContext(run)} ` +
|
|
821
|
+
(['active', 'reported_done', 'completed'].includes(run.state) ? `${correctiveFollowupInstruction} ` : '') +
|
|
822
|
+
(provider === 'codex' && ['completed', 'reported_done'].includes(run.state) ? `Finished run ID: ${run.id}. Its completion does not supervise later user requests. If the user explicitly requests supervision of the current goal, use simy delivery followup --provider codex --session ${session} --previous-run-id ${run.id} --objective CURRENT_GOAL. A different goal archives the old run; the same ordinary reported_done goal retains identity and budgets for read-only reconciliation. A new prompt alone is not consent to run this command. ` : '') +
|
|
823
|
+
(run.state === 'active' && !run.managed ? `Before ending the turn, submit an honest JSON checkpoint with simy delivery checkpoint --provider ${provider} --session ${session} --file -. Use status in_progress with summary, remainingWork and nextStep for remaining work; completed with summary, acceptanceMet:true and evidence for finished work; or blocked with summary and blocker missing_information or authorization. The first completed checkpoint requests an outcome check in this conversation; inspect the actual result and submit a fresh checkpoint. If requested work remains, perform the first authorized step instead of ending with self-evaluation or a plan. Completion is provider-reported, not independently verified. ${informationWaitInstruction} ` : '') +
|
|
824
|
+
`Preserve the current task and its permissions, pauses and stops; never restart or reset it automatically.`;
|
|
825
|
+
}
|
|
826
|
+
export async function startDelivery({ root = rootDirectory(), provider, session, cwd = process.cwd(), executionCwd, upgradeRunId, previousRunId, reconcileRunId, objective, nextStep, maxMinutes, maxContinuations, maxFailures, continuationPolicy, managed, admission,
|
|
756
827
|
liveHookAdmission, autoContinueEpoch, providerLinkEpoch, inspectProcess = processIdentity, now = Date.now() }) {
|
|
757
828
|
const link = await providerLinkState(root, provider);
|
|
758
829
|
if (providerLinkEpoch !== undefined && providerLinkEpoch !== link.epoch) throw Object.assign(new Error("SIMY provider connection changed before enrollment."), { code: "provider_link_changed" });
|
|
@@ -777,12 +848,27 @@ export async function startDelivery({ root = rootDirectory(), provider, session,
|
|
|
777
848
|
}
|
|
778
849
|
if ((!previous?.observedAt && !admitted) || sessionCwd !== cwd && executionCwd !== cwd) throw new Error('No matching verified provider session observed here. Select an exact discovered session or explicitly bind its task execution directory.');
|
|
779
850
|
const upgrading = upgradeRunId !== undefined;
|
|
780
|
-
|
|
781
|
-
|
|
851
|
+
const following = previousRunId !== undefined;
|
|
852
|
+
const reconciling = reconcileRunId !== undefined;
|
|
853
|
+
if (reconciling && (!upgrading || following || previous.run?.id !== reconcileRunId || upgradeRunId !== reconcileRunId || previous.run.managed || previous.run.state !== 'reported_done' || text(objective) !== previous.run.objective)) throw new Error('Reconciliation requires the exact unverified ordinary completion and its unchanged original objective.');
|
|
854
|
+
if (following || reconciling) {
|
|
855
|
+
const prior = previous.run;
|
|
856
|
+
if (following && (upgrading || !prior || prior.id !== previousRunId || !['completed', 'reported_done'].includes(prior.state))) throw new Error('Follow-up requires the exact finished run; active work, stops and exhausted budgets cannot be replaced.');
|
|
857
|
+
if (following && text(objective) === prior.objective) throw new Error('Follow-up requires a new objective; use the original task controls for unfinished work.');
|
|
858
|
+
if (hasPendingUserStop(root, previous) || previous.providerArchived || previous.automaticStartBlocked || previous.autoContinue === false && previous.autoContinueEpoch ||
|
|
859
|
+
prior.managed?.resumeLease || prior.managed?.liveWorkspaceLease || prior.managed?.outcomeAudit?.lease || prior.managed?.executionUncertain || prior.managed?.pendingApproval || prior.managed?.nativeWait ||
|
|
860
|
+
['pending', 'transferred', 'pending_received'].includes(prior.managed?.handoff?.status) || previous.associationTaskId) throw new Error('Resolve the existing stop, opt-out, ownership, approval or uncertain result before enrolling follow-up work.');
|
|
861
|
+
const gateRoot = rootFromDelivery(root), gate = readDispatchGate(gateRoot);
|
|
862
|
+
const fences = Object.values(gate.taskFences?.records || {}).filter(item => item.taskId === (prior.managed?.taskId || prior.id) && item.identity.provider === provider && item.identity.session === session);
|
|
863
|
+
if (fences.some(item => !readTaskAdmission({ root: gateRoot, identity: item.identity, taskId: item.taskId }).canResume)) throw new Error('Existing task admission must be reconciled before enrolling follow-up work.');
|
|
864
|
+
}
|
|
865
|
+
if (upgrading && ((!reconciling && !canUpgradeDelivery(previous.run)) || previous.run.id !== upgradeRunId)) throw new Error('Only the exact nonterminal ordinary loop can be upgraded; resolve its blocker or resume its original task first.');
|
|
866
|
+
if (previous.run && !upgrading && !following) throw new Error('This session already has a Delivery Loop. Resume or stop it; budgets cannot be reset by starting again.');
|
|
782
867
|
if (upgrading && (text(objective) !== previous.run.objective || executionCwd && previous.run.executionCwd && executionCwd !== previous.run.executionCwd)) throw new Error('Upgrade must preserve the original objective and task directory.');
|
|
783
868
|
const managedConfig = managed && { ...(managed === true ? {} : managed),
|
|
784
869
|
...(continuationPolicy === undefined ? {} : { continuationPolicy }) };
|
|
785
870
|
const managedState = managedSpec(managedConfig, session, provider, previous.process, admitted ? admission : null, now);
|
|
871
|
+
if ((following || reconciling) && (provider !== 'codex' || !admitted || !managedState?.autoRecovery || managedState.capability !== 'codex_app_server' || managedState.initialAction.kind !== 'read' || managedState.contextBootstrap)) throw new Error('Follow-up requires exact-session managed supervision, a concrete goal and read-only reconciliation.');
|
|
786
872
|
if (upgrading && (!managedState?.autonomyPolicy || managedState.initialAction.kind !== 'read' || managedState.contextBootstrap)) throw new Error('Upgrade requires read-only reconciliation and frozen acceptance of the original objective.');
|
|
787
873
|
if (!managedState && continuationMode(continuationPolicy) === 'progress') throw new Error('Progress continuation requires frozen managed acceptance criteria.');
|
|
788
874
|
if (autoContinueEpoch && managedState) managedState.autoContinueEpoch = autoContinueEpoch;
|
|
@@ -794,7 +880,7 @@ export async function startDelivery({ root = rootDirectory(), provider, session,
|
|
|
794
880
|
identity: liveHookAdmission.identity, hookStartedAt: liveHookAdmission.hookStartedAt };
|
|
795
881
|
const waiting = managedState?.decision?.outcome === 'wait';
|
|
796
882
|
const queued = managedState?.capability === 'codex_app_server';
|
|
797
|
-
const run = {
|
|
883
|
+
const run = { ...initialDeliveryRun(objective, nextStep, now, maxFailures), state: waiting ? 'blocked' : queued ? 'recovery_required' : 'active',
|
|
798
884
|
...(managedState?.purpose ? { ...(maxMinutes === undefined ? {} : { deadline: now + positive(maxMinutes, 120, 1440) * 60_000 }), ...(maxContinuations === undefined ? {} : { maxContinuations: positive(maxContinuations, 50, 1000) }) } : {}),
|
|
799
885
|
maxFailures: positive(maxFailures, 3, 10), maxFailuresSource: maxFailures === undefined ? 'default' : 'explicit',
|
|
800
886
|
continuations: 0, failures: 0, revision: 0, consumedRevision: -1, checkpoint: null, reason: waiting ? managedState.decision.reason : queued ? 'Exact-session supervision is queued; SIMY will acquire the provider writer lock before dispatch.' : 'started',
|
|
@@ -821,6 +907,15 @@ export async function startDelivery({ root = rootDirectory(), provider, session,
|
|
|
821
907
|
if (waiting) escalate(run, run.reason);
|
|
822
908
|
const latest = await providerLinkState(root, provider);
|
|
823
909
|
if (!latest.connected || latest.epoch !== link.epoch) throw Object.assign(new Error("SIMY connection changed before enrollment."), { code: "provider_link_disconnected" });
|
|
910
|
+
if (following || reconciling) {
|
|
911
|
+
const priorId = previousRunId || reconcileRunId;
|
|
912
|
+
if (following) run.previousRunId = priorId;
|
|
913
|
+
run.previousRunArchive = join('finished-runs', `${digest(`${provider}\0${session}\0${priorId}`)}.json`);
|
|
914
|
+
const archive = { schemaVersion: 1, provider, session, cwd: previous.cwd, run: previous.run };
|
|
915
|
+
const file = join(root, run.previousRunArchive), saved = readState(file);
|
|
916
|
+
if (saved && digest(saved) !== digest(archive)) throw new Error('Finished run archive differs; reconcile its history before enrolling follow-up work.');
|
|
917
|
+
if (!saved) atomicSave(file, archive);
|
|
918
|
+
}
|
|
824
919
|
return { ...previous, run };
|
|
825
920
|
});
|
|
826
921
|
}
|
|
@@ -910,6 +1005,7 @@ export function queuePurposeDiagnosis(run, reason, key, now = Date.now(), recove
|
|
|
910
1005
|
question: run.managed.understandingQuestion?.text || run.managed.userQuestion || null,
|
|
911
1006
|
dependency: run.managed.dependencyWait || null };
|
|
912
1007
|
recordDecision(run.managed, purposeDiagnosisPlan(run.managed, reason), now);
|
|
1008
|
+
purpose.diagnosisDecisionId = run.managed.decision.id;
|
|
913
1009
|
if (run.managed.executionUncertain) prepareRecoveryDiagnosis(run, now);
|
|
914
1010
|
if (run.managed.phase === 'needs_clarification') run.managed.phase = 'understanding';
|
|
915
1011
|
run.managed.userQuestion = null; run.managed.understandingQuestion = null; run.managed.dependencyWait = null;
|
|
@@ -922,6 +1018,9 @@ export function queuePurposeDiagnosis(run, reason, key, now = Date.now(), recove
|
|
|
922
1018
|
return true;
|
|
923
1019
|
}
|
|
924
1020
|
|
|
1021
|
+
export const informationWaitInstruction = 'Use blocked/missing_information only with a nonempty question field containing a concrete question that requires the user\'s answer. Missing verification evidence, zero applicable SQM rules/scenarios, or unfinished checks alone are remaining work: inspect applicability and available evidence, then propose authorized repair or verification. Keep unmet acceptance criteria unverified; do not invent rules or lower acceptance. If investigation finds a real user-only choice or unavailable input, ask that specific question. Preserve native permissions, explicit stops, refusals and budgets.';
|
|
1022
|
+
const informationWaitDiagnosis = 'Read the current task context and available verification evidence to determine whether progress really requires a user answer. Identify an authorized next investigation or verification step, or return blocked/missing_information with the specific user question. Do not repeat prior side effects or claim that incomplete verification passed.';
|
|
1023
|
+
|
|
925
1024
|
export async function checkpointDelivery({ root = rootDirectory(), provider, session, cwd = process.cwd(), checkpoint, expectedLease, toolEvidence = [], sourceContext, controllerEvidence, now = Date.now() }) {
|
|
926
1025
|
if (checkpoint?.kind === 'action_association') return checkpointActionAssociation({ root, provider, session, cwd, checkpoint, expectedLease, now });
|
|
927
1026
|
if (checkpoint?.kind === 'session_understanding') return checkpointUnderstanding({ root, provider, session, cwd, checkpoint, expectedLease, sourceContext, controllerEvidence, now });
|
|
@@ -929,7 +1028,7 @@ export async function checkpointDelivery({ root = rootDirectory(), provider, ses
|
|
|
929
1028
|
const statuses = ['in_progress', 'completed', 'blocked', 'error'];
|
|
930
1029
|
if (!checkpoint || !statuses.includes(checkpoint.status)) throw new Error('Checkpoint status must be in_progress, completed, blocked, or error.');
|
|
931
1030
|
if (!text(checkpoint.summary)) throw new Error('Checkpoint summary is required.');
|
|
932
|
-
if (
|
|
1031
|
+
if (checkpoint.status === 'error' && !text(checkpoint.nextStep)) throw new Error('A concrete nextStep is required.');
|
|
933
1032
|
if (checkpoint.status === 'blocked' && !['authorization', 'missing_information', 'external_dependency'].includes(checkpoint.blocker)) throw new Error('Specify authorization, missing_information, or external_dependency.');
|
|
934
1033
|
const evidence = Array.isArray(checkpoint.evidence) ? checkpoint.evidence.map(v => text(v, 1000)).filter(Boolean).slice(0, 20) : [];
|
|
935
1034
|
const settled = await updateRecord(root, provider, session, async record => {
|
|
@@ -939,10 +1038,34 @@ export async function checkpointDelivery({ root = rootDirectory(), provider, ses
|
|
|
939
1038
|
let repairPlan = null;
|
|
940
1039
|
if (!run || !checkpointDirectoryMatches(record, cwd)) throw new Error('No matching active Delivery Loop.');
|
|
941
1040
|
if (run.managed?.phase === 'understanding' || run.managed?.phase === 'needs_clarification') throw new Error('Session understanding needs its separate structured checkpoint.');
|
|
942
|
-
if (checkpoint.status === 'completed' && !run.managed && (!evidence.length || checkpoint.acceptanceMet !== true)) throw new Error('Completion requires acceptanceMet and concrete verification evidence.');
|
|
943
1041
|
if (expectedLease && (run.state !== 'auto_resuming' || run.managed?.resumeLease?.id !== expectedLease || run.revision !== run.managed.resumeLease.checkpointRevision)) throw new Error('Managed recovery lease changed; checkpoint discarded.');
|
|
944
1042
|
if (TERMINAL.has(run.state) || run.state === 'blocked' || run.state === 'recovery_required') throw new Error('Loop is stopped or awaiting a human resume.');
|
|
945
1043
|
if (['paused', 'verification_pending'].includes(run.state)) throw new Error('Managed task is paused or awaiting acceptance verification.');
|
|
1044
|
+
const remainingWork = text(checkpoint.remainingWork, 4000);
|
|
1045
|
+
const resultDisposition = decideLoop({ completion_claim: checkpoint.status === 'completed',
|
|
1046
|
+
remaining_work: Boolean(remainingWork), acceptance_failed: checkpoint.acceptanceMet === false });
|
|
1047
|
+
const incompleteResult = !run.managed && checkpoint.status === 'completed' && resultDisposition.action === 'repair';
|
|
1048
|
+
const missingNextStep = checkpoint.status === 'in_progress' && !nextStep;
|
|
1049
|
+
if (incompleteResult || (missingNextStep && !run.managed)) {
|
|
1050
|
+
effectiveStatus = 'in_progress';
|
|
1051
|
+
nextStep ||= resultFollowThroughInstruction();
|
|
1052
|
+
}
|
|
1053
|
+
if (missingNextStep && run.managed) throw new Error('A concrete nextStep is required.');
|
|
1054
|
+
if (effectiveStatus === 'completed' && !run.managed && checkpoint.acceptanceMet !== true) throw new Error('Completion requires the requested outcome to be checked (acceptanceMet).');
|
|
1055
|
+
// A worker's unsupported information-wait claim is a recoverable reporting
|
|
1056
|
+
// error, not evidence that a human must act. Reuse the current executor and
|
|
1057
|
+
// failure budget; never resume a previously stopped/blocked task here.
|
|
1058
|
+
const unsupportedWait = checkpoint.status === 'blocked' && checkpoint.blocker === 'missing_information' && !text(checkpoint.question, 1000);
|
|
1059
|
+
if (unsupportedWait) {
|
|
1060
|
+
if (run.blockerSource === 'provider' || run.managed?.pendingApproval || run.managed?.nativeWait || run.managed?.purpose?.nativeBoundary) {
|
|
1061
|
+
throw new Error('Missing information blocker needs a specific user question; existing permission boundaries remain in force.');
|
|
1062
|
+
}
|
|
1063
|
+
effectiveStatus = 'error'; nextStep = informationWaitDiagnosis;
|
|
1064
|
+
if (run.managed) {
|
|
1065
|
+
repairPlan = purposeDiagnosisPlan(run.managed, informationWaitDiagnosis);
|
|
1066
|
+
repairPlan.alternatives[0].action.summary = informationWaitDiagnosis;
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
946
1069
|
if (run.managed) {
|
|
947
1070
|
if (searchEnabled(run.managed) && expectedLease && sourceContext) run.managed.purpose.originalContext ||= sourceContext;
|
|
948
1071
|
if (searchEnabled(run.managed) && expectedLease) retainControllerEvidence(run.managed, controllerEvidence, now);
|
|
@@ -954,7 +1077,7 @@ export async function checkpointDelivery({ root = rootDirectory(), provider, ses
|
|
|
954
1077
|
actionResult = { decisionId: performed.id, status: result.status, summary: text(result.summary, 300), evidence: Array.isArray(result.evidence) ? result.evidence.map(item => text(item, 300)).filter(Boolean).slice(0, 5) : [], source: 'provider_reported' };
|
|
955
1078
|
const capturedAction = expectedLease && ['read','local_edit','local_test'].includes(performed.action.kind) && toolEvidence.some(item => item?.source === 'codex_tool_event' && /^[a-f0-9]{64}$/.test(item.digest || '') && item.kind === 'commandExecution' && item.exitCode === 0 && (!item.status || item.status === 'completed'));
|
|
956
1079
|
if (flexibleReasoning(run.managed) && capturedAction) actionResult.observationSource = 'controller_tool_result';
|
|
957
|
-
|
|
1080
|
+
// Observation prose is optional; actual execution receipts belong to the controller.
|
|
958
1081
|
// Local observations remain the worker's report until independent
|
|
959
1082
|
// inspection. Missing paperwork alone is not an unknown side effect.
|
|
960
1083
|
// For remote effects, keep reconciliation before any repeat operation.
|
|
@@ -980,6 +1103,9 @@ export async function checkpointDelivery({ root = rootDirectory(), provider, ses
|
|
|
980
1103
|
if (searchEnabled(run.managed) && performed && actionResult) {
|
|
981
1104
|
const unit = evidenceLedger(run.managed).units?.find(row => row.id === performed.id);
|
|
982
1105
|
if (unit) { unit.status = actionResult.status === 'failed' ? 'failed' : ['not_run', 'unverified'].includes(actionResult.status) ? 'waiting' : 'observed'; unit.evidenceRefs = (toolEvidence || []).map(row => row.ref || row.digest); unit.updatedAt = now;
|
|
1106
|
+
// A returned local operation releases execution occupancy, not outcome
|
|
1107
|
+
// acceptance. Unknown/external effects still require reconciliation.
|
|
1108
|
+
if (expectedLease && actionResult.status === 'observed' && ['local_edit', 'local_test'].includes(performed.action.kind) && !run.managed.executionUncertain) unit.localExecutionSettledAt = now;
|
|
983
1109
|
if (unit.status === 'observed' && unit.criterionIds.length && unit.criterionIds.every(id=>run.managed.verified[id] && !['semantic','manual'].includes(run.managed.acceptanceCriteria.find(row=>row.id===id)?.check.type))) {unit.status='verified';unit.verificationSource='controller_automatic_criteria';}
|
|
984
1110
|
}
|
|
985
1111
|
}
|
|
@@ -998,6 +1124,11 @@ export async function checkpointDelivery({ root = rootDirectory(), provider, ses
|
|
|
998
1124
|
if (performed) {
|
|
999
1125
|
run.managed.workHistory ||= [];
|
|
1000
1126
|
run.managed.workHistory.push({ decisionId: performed.id, action: performed.action, status: checkpoint.status, summary: text(checkpoint.summary, 300), result: actionResult, verification: `${Math.max(0, Object.keys(run.managed.verified).length - previouslyVerified)} new acceptance criteria verified`, at: now });
|
|
1127
|
+
// A repair check is an observed read, not completion of the original task.
|
|
1128
|
+
if (run.managed.repairRequest?.status === 'running' && performed.action.kind === 'read' && actionResult?.status === 'observed' &&
|
|
1129
|
+
['resolved', 'unresolved', 'needs_human'].includes(checkpoint.repairResult?.status) && text(checkpoint.repairResult?.summary, 600)) {
|
|
1130
|
+
run.managed.workHistory.at(-1).repairResult = { status: checkpoint.repairResult.status, summary: text(checkpoint.repairResult.summary, 600) };
|
|
1131
|
+
}
|
|
1001
1132
|
run.managed.workHistory = run.managed.workHistory.slice(-10);
|
|
1002
1133
|
if (performed.outcome === 'report') run.managed.notification = { key: `report:${run.revision + 1}`, kind: 'action_report', message: `Attempted ${performed.action.summary}: ${text(checkpoint.summary, 300)}. ${run.managed.workHistory.at(-1).verification}.`, at: now };
|
|
1003
1134
|
if (run.managed.approvedAction?.id === performed.id) delete run.managed.approvedAction;
|
|
@@ -1006,15 +1137,15 @@ export async function checkpointDelivery({ root = rootDirectory(), provider, ses
|
|
|
1006
1137
|
if (run.managed.autonomyPolicy) {
|
|
1007
1138
|
const plan = repairPlan || assessedPlan(checkpoint.plan, run.managed);
|
|
1008
1139
|
const selected = plan.alternatives.find(item => item.id === plan.selectedOptionId);
|
|
1009
|
-
if (checkpoint.proposedAction && JSON.stringify(actionDescriptor(checkpoint.proposedAction)) !== JSON.stringify(selected.action)) throw new Error('Proposed action differs from selected assessed option.');
|
|
1140
|
+
if (!unsupportedWait && checkpoint.proposedAction && JSON.stringify(actionDescriptor(checkpoint.proposedAction)) !== JSON.stringify(selected.action)) throw new Error('Proposed action differs from selected assessed option.');
|
|
1010
1141
|
recordDecision(run.managed, plan, now);
|
|
1011
|
-
if (purposeEnabled(run) && !searchEnabled(run.managed) && ['failed', 'not_run'].includes(actionResult.status)) {
|
|
1142
|
+
if (!unsupportedWait && purposeEnabled(run) && !searchEnabled(run.managed) && ['failed', 'not_run'].includes(actionResult.status)) {
|
|
1012
1143
|
recordDecision(run.managed, purposeDiagnosisPlan(run.managed, `Investigate the ${actionResult.status} action before further changes: ${actionResult.summary}`), now);
|
|
1013
|
-
} else if (!searchEnabled(run.managed) && (actionResult.status === 'not_run' || actionResult.status === 'failed' && run.managed.decision.action.kind !== 'read')) {
|
|
1144
|
+
} else if (!unsupportedWait && !searchEnabled(run.managed) && (actionResult.status === 'not_run' || actionResult.status === 'failed' && run.managed.decision.action.kind !== 'read')) {
|
|
1014
1145
|
run.managed.decision.outcome = 'wait'; run.managed.decision.reason = actionResult.status === 'not_run' ? 'Previous selected action was not run.' : 'Failed action requires a read-only diagnosis before another change.';
|
|
1015
1146
|
run.managed.pendingApproval = { id: run.managed.decision.id, ...run.managed.decision.action, reason: run.managed.decision.reason };
|
|
1016
1147
|
}
|
|
1017
|
-
} else run.managed.proposedAction = actionDescriptor(checkpoint.proposedAction);
|
|
1148
|
+
} else run.managed.proposedAction = unsupportedWait ? repairPlan.alternatives[0].action : actionDescriptor(checkpoint.proposedAction);
|
|
1018
1149
|
}
|
|
1019
1150
|
if (checkpoint.status === 'blocked' && checkpoint.blocker === 'authorization') {
|
|
1020
1151
|
const request = checkpoint.approval;
|
|
@@ -1036,8 +1167,7 @@ export async function checkpointDelivery({ root = rootDirectory(), provider, ses
|
|
|
1036
1167
|
run.managed.pendingApproval = { id: digest(`${run.managed.taskId}:${run.revision + 1}:${JSON.stringify(proposed)}`), ...proposed };
|
|
1037
1168
|
}
|
|
1038
1169
|
}
|
|
1039
|
-
if (
|
|
1040
|
-
if (!text(checkpoint.question, 1000)) throw new Error('Missing information blocker needs a specific user question.');
|
|
1170
|
+
if (effectiveStatus === 'blocked' && checkpoint.blocker === 'missing_information') {
|
|
1041
1171
|
run.managed.userQuestion = text(checkpoint.question, 1000);
|
|
1042
1172
|
}
|
|
1043
1173
|
}
|
|
@@ -1046,7 +1176,7 @@ export async function checkpointDelivery({ root = rootDirectory(), provider, ses
|
|
|
1046
1176
|
if (!text(wait?.condition, 1000) || !text(wait?.owner, 200) || !text(wait?.nextCheck, 1000) || !nextStep) throw new Error('External dependency needs waitCondition {condition, owner, nextCheck} and a nextStep.');
|
|
1047
1177
|
run.managed.dependencyWait = { id: randomUUID(), condition: text(wait.condition, 1000), owner: text(wait.owner, 200), nextCheck: text(wait.nextCheck, 1000), createdAt: now };
|
|
1048
1178
|
}
|
|
1049
|
-
if (searchEnabled(run.managed) && checkpoint.status === 'blocked') {
|
|
1179
|
+
if (searchEnabled(run.managed) && checkpoint.status === 'blocked' && !unsupportedWait) {
|
|
1050
1180
|
rememberSearchWait(run.managed, checkpoint.blocker, { reason: checkpoint.summary, question: checkpoint.question,
|
|
1051
1181
|
condition: checkpoint.waitCondition?.condition, owner: checkpoint.waitCondition?.owner, action: checkpoint.approval || null }, now);
|
|
1052
1182
|
if (checkpoint.plan && checkpoint.blocker !== 'authorization' && checkpoint.plan.alternatives?.find(o => o.id === checkpoint.plan.selectedOptionId)?.independent === true) recordDecision(run.managed, assessedPlan(checkpoint.plan, run.managed), now);
|
|
@@ -1074,7 +1204,12 @@ export async function checkpointDelivery({ root = rootDirectory(), provider, ses
|
|
|
1074
1204
|
nextStep = run.nextStep;
|
|
1075
1205
|
}
|
|
1076
1206
|
run.blockerSource = effectiveStatus === 'blocked' ? 'checkpoint' : null;
|
|
1077
|
-
run.checkpoint = { status: effectiveStatus, summary: text(checkpoint.summary), nextStep,
|
|
1207
|
+
run.checkpoint = { status: effectiveStatus, summary: text(checkpoint.summary), nextStep,
|
|
1208
|
+
blocker: unsupportedWait ? undefined : checkpoint.blocker,
|
|
1209
|
+
...(unsupportedWait ? { reportedBlocker: checkpoint.blocker } : checkpoint.blocker === 'missing_information' ? { question: text(checkpoint.question, 1000) } : {}),
|
|
1210
|
+
...(remainingWork ? { remainingWork } : {}), ...(incompleteResult ? { reportedStatus: checkpoint.status } : {}),
|
|
1211
|
+
evidence, acceptanceMet: !unsupportedWait && !incompleteResult && checkpoint.acceptanceMet === true };
|
|
1212
|
+
if (!run.managed && effectiveStatus !== 'completed') delete run.completionCheck;
|
|
1078
1213
|
run.revision += 1; run.updatedAt = now;
|
|
1079
1214
|
recordManagedOutcomeProgress(run, now);
|
|
1080
1215
|
// File predicates were reread above; a prior semantic verdict can belong
|
|
@@ -1093,9 +1228,9 @@ export async function checkpointDelivery({ root = rootDirectory(), provider, ses
|
|
|
1093
1228
|
run.state = 'reported_done'; run.reason = 'Original session reported the goal done; no independent acceptance check was available.';
|
|
1094
1229
|
}
|
|
1095
1230
|
}
|
|
1096
|
-
if (run.managed?.liveWorkspaceLease) {
|
|
1231
|
+
if (run.managed?.liveWorkspaceLease || expectedLease && run.managed?.executionAttempt?.id === expectedLease) {
|
|
1097
1232
|
run.managed.executionAttempt = { ...run.managed.executionAttempt, phase: 'checkpointed', checkpointRevision: run.revision, endedAt: now };
|
|
1098
|
-
run.managed.liveWorkspaceLease.checkpointRevision = run.revision;
|
|
1233
|
+
if (run.managed.liveWorkspaceLease) run.managed.liveWorkspaceLease.checkpointRevision = run.revision;
|
|
1099
1234
|
}
|
|
1100
1235
|
for (const request of run.managed?.consistencyOutbox || []) if (request.decisionId && request.decisionId === checkpoint.actionResult?.decisionId) {
|
|
1101
1236
|
recordHandoffStage(request, run.managed.workHistory?.at(-1)?.result?.status === 'observed' ? 'inspected' : 'inspection_failed', now);
|
|
@@ -1130,7 +1265,10 @@ export function externalReviewIsCurrent(managed,review,now) {
|
|
|
1130
1265
|
if (review.semanticInspection?.source === 'controller_actual_read_set') refs.push(...(review.semanticInspection.observationRefs || []).map(ref => ({ ref })));
|
|
1131
1266
|
const ledger=managed.purpose.evidence;
|
|
1132
1267
|
const observations=[...(ledger?.observations||[]),...(ledger?.reviewObservationActionId===(review.decisionId||managed.decision.id) ? ledger.reviewObservations||[] : [])];
|
|
1133
|
-
|
|
1268
|
+
const inspected = new Set(review.semanticInspection?.source === 'controller_actual_read_set' ? review.semanticInspection.observationRefs || [] : []);
|
|
1269
|
+
return refs.filter(link => inspected.has(link.ref) || observations.some(item => item.id === link.ref))
|
|
1270
|
+
.every(link=>observations.some(item=>item.id===link.ref && item.status==='observed' && currentPurposeObservation(item,now,review.observationWindow)));
|
|
1271
|
+
|
|
1134
1272
|
}
|
|
1135
1273
|
export async function settlePurposeVerification({ root, provider, session, expectedLease, review, verifySnapshot, now = Date.now() }) {
|
|
1136
1274
|
const started = Date.now(), initialNow = now;
|
|
@@ -1145,8 +1283,16 @@ export async function settlePurposeVerification({ root, provider, session, expec
|
|
|
1145
1283
|
managed.purpose.search?.verificationDecisionId !== managed.decision.id || run.revision !== managed.resumeLease.checkpointRevision || now >= run.deadline) throw new Error('Independent review lease changed.');
|
|
1146
1284
|
const state = searchState(managed);
|
|
1147
1285
|
if (review?.source !== 'independent_codex_session' || !review.session || review.session === session || !review.workspaceDigest || typeof review.passed !== 'boolean') throw new Error('Independent review provenance is missing.');
|
|
1286
|
+
const verificationUnit = managed.purpose.evidence?.units?.find(unit => unit.id === managed.decision.id);
|
|
1287
|
+
if (verificationUnit?.action?.kind === 'read' && verificationUnit.status === 'dispatched') {
|
|
1288
|
+
verificationUnit.status = 'observed'; verificationUnit.observedAt = now;
|
|
1289
|
+
}
|
|
1148
1290
|
retainApplicableRules(managed, review.rules || []);
|
|
1149
1291
|
review = assessRuleReview(review, managed);
|
|
1292
|
+
if (review.contractDigest !== completionContractIdentity(managed)) {
|
|
1293
|
+
review = { ...review, passed: false, needsHuman: false,
|
|
1294
|
+
findings: [...review.findings, 'Requested behavior changed; check the current request.'] };
|
|
1295
|
+
}
|
|
1150
1296
|
if (currentDigest !== review.workspaceDigest) {
|
|
1151
1297
|
review = { ...review, passed: false, needsHuman: false, findings: [...review.findings, 'Workspace changed before review settlement; verify the new revision.'] };
|
|
1152
1298
|
}
|
|
@@ -1156,45 +1302,64 @@ export async function settlePurposeVerification({ root, provider, session, expec
|
|
|
1156
1302
|
'Required operation, effect reconciliation or original-purpose contract remains unresolved. Preserve the waiting unit and continue independent authorized work.'] };
|
|
1157
1303
|
}
|
|
1158
1304
|
if(!externalReviewIsCurrent(managed,review,now)) review={...review,passed:false,needsHuman:false,controllerEvidenceFailure:{code:'independent_reference_invalid',targets:['current_external_observations'],instruction:'Controller external observations expired before settlement; reacquire the same authorized observations and independently re-evaluate without replaying writes.'}};
|
|
1159
|
-
// Validate counterevidence against a disposable ledger before mutations.
|
|
1160
|
-
// Verifier contract failures are verifier work, never worker checkpoint errors.
|
|
1161
|
-
if (!review.controllerEvidenceFailure) {
|
|
1162
|
-
try { reconcileFindings(structuredClone(managed), review, now); }
|
|
1163
|
-
catch (error) {
|
|
1164
|
-
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;
|
|
1165
|
-
review = { ...review, passed: false, needsHuman: false, controllerEvidenceFailure: {
|
|
1166
|
-
code: 'independent_finding_evidence_invalid', targets: [...(review.resolutions || []).map(row => `resolution:${row.id}`), ...(review.findings || []).filter(row => row.reopenReason).map(row => `reopen:${row.id}`)],
|
|
1167
|
-
instruction: error.message + ' Repeat isolated verification and reconcile existing findings with primary reads. For reopening use the exact prior closure read scope.',
|
|
1168
|
-
requiredReads: (managed.purpose.evidence?.findings || []).filter(row => row.status !== 'open').map(row => ({ id: row.id, closedReads: row.closedReads || [] }))
|
|
1169
|
-
} };
|
|
1170
|
-
}
|
|
1171
|
-
}
|
|
1172
1305
|
if (review.controllerEvidenceFailure) {
|
|
1173
1306
|
const prior = state.verifierFailures;
|
|
1174
1307
|
const key = digest(JSON.stringify([currentDigest, review.controllerEvidenceFailure.code, review.controllerEvidenceFailure.targets]));
|
|
1175
1308
|
state.verifierFailures = { key, attempts: prior?.key === key ? prior.attempts + 1 : 1, at: now, failure: review.controllerEvidenceFailure };
|
|
1176
1309
|
state.review = review;
|
|
1177
1310
|
run.revision += 1; run.updatedAt = now;
|
|
1178
|
-
|
|
1179
|
-
|
|
1311
|
+
// An invalid review record is not a product defect and must not send
|
|
1312
|
+
// the verifier around a citation-repair loop. The normal planner selects
|
|
1313
|
+
// useful work or another actual outcome check under existing authority.
|
|
1314
|
+
const reason = 'The review record could not establish completion. Preserve existing work and inspect the original requested behavior through another available method. Repair concrete defects or continue remaining authorized work; do not generate citations, evidence files or audit forms. Unknown external effects still require reconciliation before any retry.';
|
|
1315
|
+
const fallbackPlan = purposeDiagnosisPlan(managed, reason);
|
|
1316
|
+
fallbackPlan.alternatives[0].action.summary = 'Inspect the original requested behavior using existing results or another available method; continue concrete work without audit paperwork.';
|
|
1317
|
+
recordDecision(managed, fallbackPlan, now);
|
|
1318
|
+
state.phase = 'repairing';
|
|
1319
|
+
delete state.verificationDecisionId;
|
|
1320
|
+
run.checkpoint = null; run.state = 'recovery_required';
|
|
1321
|
+
run.recoveryCause = 'session_supervision'; run.reason = reason;
|
|
1322
|
+
run.nextStep = managed.decision.action.summary;
|
|
1323
|
+
managed.autoRecovery = true; managed.nextAttemptAt = now;
|
|
1180
1324
|
notifyState(run, now); return record;
|
|
1181
1325
|
}
|
|
1182
1326
|
const openFindings = reconcileFindings(managed, review, now);
|
|
1327
|
+
if ((review.passed || review.needsHuman) && openFindings.length) {
|
|
1328
|
+
review = { ...review, passed: false, needsHuman: false, findings: openFindings };
|
|
1329
|
+
}
|
|
1183
1330
|
const units=evidenceLedger(managed).units;
|
|
1331
|
+
// A bound independent read can establish an earlier effect even when its
|
|
1332
|
+
// result is wrong. Release only its resource reservation, not its pending
|
|
1333
|
+
// acceptance or replay prohibition; a new assessed repair is still needed.
|
|
1334
|
+
for (const unit of units) if (unit.status === 'observed' && unit.criterionIds?.length &&
|
|
1335
|
+
unit.criterionIds.every(id => review.criteria?.some(row =>
|
|
1336
|
+
row.id === id && typeof row.passed === 'boolean' && row.evidenceRefs?.some(link =>
|
|
1337
|
+
review.capturedReads?.some(read => read.target === link.target))))) {
|
|
1338
|
+
unit.effectReconciledRevision = unit.purposeRevision;
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
await verifyAutomaticCriteria(record, now);
|
|
1342
|
+
const criteria = effectiveAcceptanceCriteria(managed);
|
|
1343
|
+
const independentlySatisfied = id => {
|
|
1344
|
+
const criterion = criteria.find(row => row.id === id);
|
|
1345
|
+
if (!criterion) return false;
|
|
1346
|
+
if (criterion.check.type === 'manual') return managed.verified[id]?.source === 'user';
|
|
1347
|
+
if (criterion.check.type !== 'semantic') return Boolean(managed.verified[id]);
|
|
1348
|
+
return review.criteria.some(row => row.id === id && row.passed) || review.passed && review.inspectionPerformed === true;
|
|
1349
|
+
};
|
|
1184
1350
|
for (const result of (review.passed || review.needsHuman) ? review.workUnits || [] : []) {
|
|
1185
1351
|
const unit=units.find(row=>row.id===result.id);
|
|
1186
1352
|
if (!unit || !['verified','superseded'].includes(result.status) || !text(result.result || review.summary) ||
|
|
1187
|
-
unit.criterionIds.some(id=>!
|
|
1188
|
-
if (unit.purposeRevision !== evidenceLedger(managed).purposeRevision)
|
|
1353
|
+
unit.criterionIds.some(id=>!independentlySatisfied(id))) continue; // Incomplete optional bookkeeping cannot settle an operation or reject the review.
|
|
1354
|
+
if (unit.purposeRevision !== evidenceLedger(managed).purposeRevision) continue;
|
|
1189
1355
|
unit.status=result.status;unit.evidenceRefs=result.evidenceRefs;unit.verifiedAt=now;
|
|
1190
1356
|
}
|
|
1191
1357
|
state.review = review; delete state.verificationDecisionId;
|
|
1192
1358
|
run.revision += 1; run.updatedAt = now;
|
|
1193
|
-
await verifyAutomaticCriteria(record, now);
|
|
1194
1359
|
if (review.passed || review.needsHuman) {
|
|
1195
1360
|
for (const criterion of effectiveAcceptanceCriteria(managed)) {
|
|
1196
|
-
if (criterion.check.type === 'semantic' && review.criteria.some(row => row.id === criterion.id && row.passed)) {
|
|
1197
|
-
managed.verified[criterion.id] = { source: 'independent_codex_session', checkedAt: now, workspaceDigest: review.workspaceDigest, session: review.session };
|
|
1361
|
+
if (criterion.check.type === 'semantic' && (review.criteria.some(row => row.id === criterion.id && row.passed) || review.passed && review.inspectionPerformed)) {
|
|
1362
|
+
managed.verified[criterion.id] = { source: 'independent_codex_session', checkedAt: now, workspaceDigest: review.workspaceDigest, session: review.session, assessment: review.criteria.some(row => row.id === criterion.id) ? 'criterion' : 'whole_outcome' };
|
|
1198
1363
|
}
|
|
1199
1364
|
}
|
|
1200
1365
|
for (const unit of units) if (unit.id.startsWith('criterion:') && unit.criterionIds.every(id=>managed.verified[id])) {unit.status='verified';unit.evidenceRefs=review.criteria.filter(row=>unit.criterionIds.includes(row.id)).flatMap(row=>row.evidenceRefs || []);}
|
|
@@ -1224,18 +1389,11 @@ export async function settlePurposeVerification({ root, provider, session, expec
|
|
|
1224
1389
|
notifyState(run, now); return record;
|
|
1225
1390
|
}
|
|
1226
1391
|
}
|
|
1227
|
-
if (!review.passed && !review.needsHuman && openFindings.length && openFindings.every(item => item.owner === 'controller')) {
|
|
1228
|
-
const ledger = evidenceLedger(managed);
|
|
1229
|
-
const lookup = createHash('sha256').update(JSON.stringify({ findings: openFindings.map(item => item.id).sort(), receipts: ledger.receipts.map(item => item.id), baseline: ledger.baseline?.digest, workspace: review.workspaceDigest })).digest('hex');
|
|
1230
|
-
if (state.controllerLookup !== lookup && (ledger.baseline || ledger.receipts.length)) {
|
|
1231
|
-
state.controllerLookup = lookup;
|
|
1232
|
-
queuePurposeVerification(run, 'Controller retrieved existing baseline and tool receipts; reconcile the missing evidence against the supplied ledger before requesting worker repairs.', now);
|
|
1233
|
-
notifyState(run, now); return record;
|
|
1234
|
-
}
|
|
1235
|
-
}
|
|
1236
1392
|
if (!review.passed && !review.needsHuman) {
|
|
1237
1393
|
const reason = `Outcome review found remaining work: ${[review.summary, ...openFindings.map(findingText)].join(' ').slice(0, 1800)}`;
|
|
1238
|
-
|
|
1394
|
+
const fallbackPlan = purposeDiagnosisPlan(managed, reason);
|
|
1395
|
+
fallbackPlan.alternatives[0].action.summary = 'Inspect the original requested behavior using existing results or another available method; continue concrete work without audit paperwork.';
|
|
1396
|
+
recordDecision(managed, fallbackPlan, now);
|
|
1239
1397
|
state.phase = 'repairing'; managed.purpose.lastRecovery = { reason, at: now };
|
|
1240
1398
|
run.checkpoint = null; run.state = 'recovery_required'; run.recoveryCause = 'session_supervision';
|
|
1241
1399
|
run.nextStep = managed.decision.action.summary; run.reason = reason; managed.autoRecovery = true;
|
|
@@ -1290,6 +1448,13 @@ async function checkpointUnderstanding({ root, provider, session, cwd, checkpoin
|
|
|
1290
1448
|
validatePurposeReview(checkpoint.purposeReview, plan, run.managed.purpose.ruleIds, searchEnabled(run.managed) ? run.managed.purpose.lastReview?.rules || [] : [], run.managed), now);
|
|
1291
1449
|
if (searchEnabled(run.managed) && expectedLease && sourceContext) run.managed.purpose.originalContext ||= sourceContext;
|
|
1292
1450
|
if (searchEnabled(run.managed) && expectedLease) retainControllerEvidence(run.managed, controllerEvidence, now);
|
|
1451
|
+
// This read has returned under the current lease. Release its resource
|
|
1452
|
+
// occupancy before selecting the next action; observation is not acceptance.
|
|
1453
|
+
const understandingUnit = run.managed.purpose?.evidence?.units?.find(unit => unit.id === decision.id);
|
|
1454
|
+
if (understandingUnit && decision.action.kind === 'read') {
|
|
1455
|
+
understandingUnit.status = result.status === 'observed' ? 'observed' : 'failed';
|
|
1456
|
+
understandingUnit.updatedAt = now;
|
|
1457
|
+
}
|
|
1293
1458
|
run.managed.sessionUnderstanding = understanding;
|
|
1294
1459
|
run.managed.understandingRevision = (run.managed.understandingRevision || 0) + 1;
|
|
1295
1460
|
run.managed.understandingAnswer = null;
|
|
@@ -1448,7 +1613,7 @@ export async function verifyDelivery({ root = rootDirectory(), provider, session
|
|
|
1448
1613
|
run.updatedAt = now; return record;
|
|
1449
1614
|
});
|
|
1450
1615
|
}
|
|
1451
|
-
function prepareWaitingDiagnosis(run, reason, now) {
|
|
1616
|
+
export function prepareWaitingDiagnosis(run, reason, now) {
|
|
1452
1617
|
const managed = run.managed, scope = managed.approvalScopes[0];
|
|
1453
1618
|
if (!scope) throw new Error('Waiting recovery needs an enrolled scope.');
|
|
1454
1619
|
const action = { kind: 'read', scope, summary: 'Inspect the resolved wait and current result before choosing new work; do not replay the prior action' };
|
|
@@ -1577,7 +1742,10 @@ export function decideStop(run, now = Date.now()) {
|
|
|
1577
1742
|
// A fresh, explicitly evidenced completion may finish at the budget boundary.
|
|
1578
1743
|
const fresh = next.checkpoint && next.revision > next.consumedRevision;
|
|
1579
1744
|
const cp = fresh ? next.checkpoint : null;
|
|
1580
|
-
|
|
1745
|
+
const completionChecked = Number.isSafeInteger(next.completionCheck?.revision) &&
|
|
1746
|
+
next.revision > next.completionCheck.revision && next.consumedRevision >= next.completionCheck.revision;
|
|
1747
|
+
const inspectCompletion = !next.managed && cp?.status === 'completed' && !completionChecked;
|
|
1748
|
+
if (cp?.status === 'completed' && !inspectCompletion) {
|
|
1581
1749
|
const pending = pendingAcceptance(next);
|
|
1582
1750
|
if (pending.length) return finish('verification_pending', acceptancePendingReason(next));
|
|
1583
1751
|
if (!next.managed || !next.managed.acceptanceCriteria.length) return finish('reported_done',
|
|
@@ -1629,7 +1797,7 @@ export function decideStop(run, now = Date.now()) {
|
|
|
1629
1797
|
}
|
|
1630
1798
|
const reportedStep = next.managed.associationCheckpointRevision !== next.revision && next.managed.contextBootstrap && next.managed.phase === 'active' && !next.managed.acceptanceCriteria.length &&
|
|
1631
1799
|
!['session_understanding', 'action_association'].includes(cp?.kind) && cp?.status === 'in_progress' &&
|
|
1632
|
-
next.managed.workHistory?.at(-1)?.result?.status === 'observed' && next.managed.workHistory.at(-1).result.evidence?.length;
|
|
1800
|
+
next.managed.workHistory?.at(-1)?.result?.status === 'observed' && (next.managed.workHistory.at(-1).result.evidence?.length || next.managed.workHistory.at(-1).result.observationSource === 'controller_tool_result');
|
|
1633
1801
|
if (next.managed.continuationPolicy === 'progress') {
|
|
1634
1802
|
if (next.managed.associationCheckpointRevision !== next.revision && !initialHook && !replanHook && !understandingHook && !approvedHook && !legacyApprovedHook && !['session_understanding', 'action_association'].includes(cp?.kind) && (!fresh || next.managed.progressCheckpointRevision !== next.revision)) recordManagedOutcomeProgress(next, now, Boolean(fresh));
|
|
1635
1803
|
} else if (next.managed.verifiedRevision > next.managed.consumedVerifiedRevision) next.managed.noProgress = 0;
|
|
@@ -1672,8 +1840,15 @@ export function decideStop(run, now = Date.now()) {
|
|
|
1672
1840
|
if (legacyApprovedHook) delete next.managed.approvedAction;
|
|
1673
1841
|
}
|
|
1674
1842
|
next.reason = fresh ? 'continue_pending_work' : 'checkpoint_missing';
|
|
1843
|
+
// A first standard completion is a candidate. One live continuation checks
|
|
1844
|
+
// the actual endpoint; only a subsequent checkpoint can report it done.
|
|
1845
|
+
// Missing/duplicate Stops cannot count as the result of that continuation.
|
|
1846
|
+
if (inspectCompletion) {
|
|
1847
|
+
next.completionCheck = { revision: next.revision, requestedAt: now };
|
|
1848
|
+
next.reason = 'check_reported_outcome';
|
|
1849
|
+
}
|
|
1675
1850
|
const selected = next.managed?.autonomyPolicy ? next.managed.decision : null;
|
|
1676
|
-
const instruction = selected && next.managed.phase === 'action_association' ? `${associationContract(next.managed.actionAssociation.job)} Submit the JSON checkpoint through standard input using simy delivery checkpoint --provider ${next.managed.thread.provider} --session ${next.managed.thread.id} --file - before stopping.` : selected && next.managed.phase === 'understanding' ? `Understand the current user request in this ORIGINAL conversation. ${next.managed.understandingAnswer ? `The user answered question ${next.managed.understandingAnswer.questionId}: ${next.managed.understandingAnswer.text}. ` : ''}${understandingCheckpointContract(selected)} Submit the JSON checkpoint through standard input using simy delivery checkpoint --provider ${next.managed.thread.provider} --session ${next.managed.thread.id} --file - before stopping. A shell heredoc is suitable. This updates SIMY metadata only; do not create a checkpoint file in the task workspace.` : selected ? `${initialHook ? `Managed objective: ${next.objective}. Frozen acceptance criteria: ${JSON.stringify(next.managed.acceptanceCriteria)}. ` : ''}${replanHook ? `Replan context: ${next.managed.plan.diagnosis}. ` : ''}Execute only selected ${selected.action.kind} action in scope ${selected.action.scope}: ${selected.action.summary}. Expected verification: ${selected.expectedVerification}. Rollback: ${selected.rollback || 'none needed'}. ${assessedCheckpointContract(selected, next.managed)} Submit that JSON checkpoint through standard input using simy delivery checkpoint --provider ${next.managed.thread.provider} --session ${next.managed.thread.id} --file - before stopping. A shell heredoc is suitable; do not create a task-workspace checkpoint file.` : fresh ? `${minimalSufficientChangeInstruction()}\n${next.nextStep}` : `${minimalSufficientChangeInstruction()}\
|
|
1851
|
+
const instruction = inspectCompletion ? resultFollowThroughInstruction({ completion: true }) : selected && next.managed.phase === 'action_association' ? `${associationContract(next.managed.actionAssociation.job)} Submit the JSON checkpoint through standard input using simy delivery checkpoint --provider ${next.managed.thread.provider} --session ${next.managed.thread.id} --file - before stopping.` : selected && next.managed.phase === 'understanding' ? `Understand the current user request in this ORIGINAL conversation. ${next.managed.understandingAnswer ? `The user answered question ${next.managed.understandingAnswer.questionId}: ${next.managed.understandingAnswer.text}. ` : ''}${understandingCheckpointContract(selected)} Submit the JSON checkpoint through standard input using simy delivery checkpoint --provider ${next.managed.thread.provider} --session ${next.managed.thread.id} --file - before stopping. A shell heredoc is suitable. This updates SIMY metadata only; do not create a checkpoint file in the task workspace.` : selected ? `${initialHook ? `Managed objective: ${next.objective}. Frozen acceptance criteria: ${JSON.stringify(next.managed.acceptanceCriteria)}. ` : ''}${replanHook ? `Replan context: ${next.managed.plan.diagnosis}. ` : ''}Execute only selected ${selected.action.kind} action in scope ${selected.action.scope}: ${selected.action.summary}. Expected verification: ${selected.expectedVerification}. Rollback: ${selected.rollback || 'none needed'}. ${assessedCheckpointContract(selected, next.managed)} Submit that JSON checkpoint through standard input using simy delivery checkpoint --provider ${next.managed.thread.provider} --session ${next.managed.thread.id} --file - before stopping. A shell heredoc is suitable; do not create a task-workspace checkpoint file.` : fresh ? `${minimalSufficientChangeInstruction()}\n${informationWaitInstruction}\n${next.nextStep}` : `${minimalSufficientChangeInstruction()}\n${informationWaitInstruction}\n${resultFollowThroughInstruction()}`;
|
|
1677
1852
|
return { run: next, output: { decision: 'block', reason: `SIMY Delivery Loop (${next.continuations} continuations). ${next.executionCwd ? `Use task execution directory ${JSON.stringify(next.executionCwd)} for task work and acceptance. ` : ''}${deliveryAcceptanceContext(next)} ${instruction}\n${purposeContract(next.managed)}\nStay within the original request and permissions. If one action lacks authorization or information, preserve that dependency and select independent authorized work. Resume this same task; do not start a replacement loop.` } };
|
|
1678
1853
|
}
|
|
1679
1854
|
// A process signature protects against PID reuse. Never treat a stale timer alone
|
|
@@ -1786,7 +1961,7 @@ async function handleHookState({ input, provider, root = rootDirectory(), now =
|
|
|
1786
1961
|
const currentLink = await providerLinkState(root, provider);
|
|
1787
1962
|
if (!currentLink.connected || currentLink.epoch !== link.epoch) return old;
|
|
1788
1963
|
if (old && old.cwd !== cwd) return old;
|
|
1789
|
-
const record = old || { schemaVersion: 1, provider, session: input.session_id, cwd, run: null };
|
|
1964
|
+
const record = old || await applyArchiveFence(root, { schemaVersion: 1, provider, session: input.session_id, cwd, run: null });
|
|
1790
1965
|
const stopMarker = readState(stopMarkerPath(root, provider, input.session_id));
|
|
1791
1966
|
if (record.run && typeof stopMarker?.runId === 'string' && stopMarker.runId === record.run.id && stopMarker.id !== record.run.handledStopMarkerId) { record.run.state = 'stopped'; record.run.reason = 'User interrupted the provider.'; }
|
|
1792
1967
|
// Inherited provider identity links an AI-launched conversation to its
|
|
@@ -1847,9 +2022,28 @@ async function handleHookState({ input, provider, root = rootDirectory(), now =
|
|
|
1847
2022
|
if (identity) { record.hookStartedAt = now; record.hookSessionProcess = { ...identity }; }
|
|
1848
2023
|
else { delete record.hookStartedAt; delete record.hookSessionProcess; delete record.process; }
|
|
1849
2024
|
const approved = record.run?.managed?.approvedAction;
|
|
1850
|
-
output = { hookSpecificOutput: { hookEventName:
|
|
2025
|
+
output = { hookSpecificOutput: { hookEventName: event, additionalContext: hookDeliveryContext(record, !env.SIMY_PARENT_SESSION && !(provider === 'claude' && env.CODEX_THREAD_ID) && (await automaticDeliveryStatus({ root, provider, cwd })).enabled) +
|
|
2026
|
+
(approved ? ` User approved exactly one ${approved.action.kind} action in scope ${approved.action.scope}: ${approved.action.summary}. Next step: ${approved.nextStep}. ${approved.dispatchedAt ? 'That approval was already dispatched; any repeated action needs a new approval.' : 'Resume the original conversation to dispatch this one action.'}` : '') } };
|
|
1851
2027
|
}
|
|
1852
2028
|
if (event === 'SessionEnd') record.hookEndedAt = now;
|
|
2029
|
+
if (!record.run && (['Interrupt', 'PermissionRequest'].includes(event) ||
|
|
2030
|
+
event === 'Notification' && ['permission_prompt', 'elicitation_dialog'].includes(input.notification_type))) record.automaticStartBlocked = event;
|
|
2031
|
+
if (event === 'UserPromptSubmit') {
|
|
2032
|
+
const automatic = await automaticDeliveryStatus({ root, provider, cwd });
|
|
2033
|
+
if (!record.run && !record.providerArchived && !record.automaticStartBlocked && !record.parentSession && !(record.autoContinue === false && record.autoContinueEpoch) &&
|
|
2034
|
+
!env.SIMY_PARENT_SESSION && !(provider === 'claude' && env.CODEX_THREAD_ID) && automatic.enabled && text(input.prompt)) {
|
|
2035
|
+
record.run = { ...initialDeliveryRun(input.prompt, 'Address the user request in this conversation and verify the result.', now),
|
|
2036
|
+
enrollmentSource: automatic.source === 'cli_default' ? 'cli_automatic' : 'workspace_automatic' };
|
|
2037
|
+
}
|
|
2038
|
+
// A completion claim made before a new user turn cannot answer that turn's
|
|
2039
|
+
// corrections. Keep its evidence and the original goal, but require a
|
|
2040
|
+
// fresh result. This does not classify the prompt or reopen finished work.
|
|
2041
|
+
if (record.run?.state === 'active' && !record.run.managed && text(input.prompt)) {
|
|
2042
|
+
delete record.run.completionCheck;
|
|
2043
|
+
if (record.run.checkpoint?.status === 'completed') record.run.consumedRevision = record.run.revision;
|
|
2044
|
+
}
|
|
2045
|
+
output = { hookSpecificOutput: { hookEventName: event, additionalContext: hookDeliveryContext(record, automatic.enabled && !env.SIMY_PARENT_SESSION && !(provider === 'claude' && env.CODEX_THREAD_ID)) } };
|
|
2046
|
+
}
|
|
1853
2047
|
const run = record.run;
|
|
1854
2048
|
if(run && event==='Stop') {
|
|
1855
2049
|
// Recheck the actual retained task under its record lock. Lifecycle
|
|
@@ -1886,7 +2080,6 @@ async function handleHookState({ input, provider, root = rootDirectory(), now =
|
|
|
1886
2080
|
}
|
|
1887
2081
|
}
|
|
1888
2082
|
if (!run || TERMINAL.has(run.state)) return record;
|
|
1889
|
-
if (event === 'UserPromptSubmit') output = { hookSpecificOutput: { hookEventName: event, additionalContext: deliveryAcceptanceContext(run) } };
|
|
1890
2083
|
if (event === 'Interrupt') { run.state = 'stopped'; run.reason = 'User interrupted the provider.'; }
|
|
1891
2084
|
else if (run.state === 'auto_resuming' && ['Stop', 'SessionEnd', 'StopFailure'].includes(event)) {
|
|
1892
2085
|
// The supervisor owns this exact attempt and consumes its structured
|
|
@@ -1978,6 +2171,7 @@ async function handleHookState({ input, provider, root = rootDirectory(), now =
|
|
|
1978
2171
|
});
|
|
1979
2172
|
try {
|
|
1980
2173
|
if (event === 'SessionStart') observed = await withSessionParentLock(root, applyObservation, inspectProcess);
|
|
2174
|
+
else if (event === 'UserPromptSubmit') observed = await withAutomaticLock(root, provider, cwd, applyObservation);
|
|
1981
2175
|
else observed = await applyObservation();
|
|
1982
2176
|
} catch (error) {
|
|
1983
2177
|
if (claimedWorkspace?.release) await claimedWorkspace.release();
|
|
@@ -2244,7 +2438,7 @@ export async function controlDelivery({ root = rootDirectory(), provider, sessio
|
|
|
2244
2438
|
const selectedKind = run.managed?.decision?.action?.kind;
|
|
2245
2439
|
const approvedWait = run.managed?.decision?.outcome === 'wait' && run.managed.approvedAction?.id === run.managed.decision.id && !run.managed.approvedAction.dispatchedAt;
|
|
2246
2440
|
const queuedAppServer = run.managed?.resumeChannel === 'codex_app_server' &&
|
|
2247
|
-
['read', 'local_edit', 'local_test', ...(searchEnabled(run.managed) ? ['external'] : [])].includes(selectedKind) &&
|
|
2441
|
+
['read', 'local_edit', 'local_test', ...(searchEnabled(run.managed) && run.managed.autonomyPolicy === 'autonomous' && run.managed.decision?.assessment?.reversibility === 'reversible' ? ['external'] : [])].includes(selectedKind) &&
|
|
2248
2442
|
(run.managed.decision.outcome !== 'wait' || approvedWait);
|
|
2249
2443
|
if (queuedAppServer) run.managed.autoRecovery = true;
|
|
2250
2444
|
run.state = queuedAppServer ? 'recovery_required' : 'active';
|