@awak-app/simy-cli 0.7.15 → 0.7.17
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 +225 -98
- package/package.json +5 -3
- package/src/agent.js +129 -7
- package/src/backend-executable.js +7 -6
- 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 +302 -78
- 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 +16 -5
- package/src/codex-plugins/simy/hooks/purpose-completion-contract.mjs +10 -0
- package/src/codex-plugins/simy/hooks/purpose-cycle.mjs +12 -11
- package/src/codex-plugins/simy/hooks/purpose-evidence.mjs +28 -38
- package/src/codex-plugins/simy/hooks/purpose-rule-policy.mjs +1 -2
- package/src/codex-plugins/simy/hooks/purpose-rule-review.mjs +5 -63
- package/src/codex-plugins/simy/hooks/purpose-search.mjs +21 -4
- package/src/codex-plugins/simy/hooks/result-cycle.mjs +166 -0
- package/src/codex-plugins/simy/hooks/scene-reasoning.mjs +98 -12
- package/src/codex-plugins/simy/hooks/user-experience.mjs +26 -0
- 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 +76 -10
- package/src/delivery-hooks/consistency-monitor.js +20 -4
- 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 +81 -1
- package/src/delivery-hooks/local-supervisor.js +13 -4
- package/src/delivery-hooks/repair-continuation.js +80 -0
- package/src/delivery-hooks/repair.js +164 -0
- package/src/delivery-hooks/supervisor-handover.js +84 -0
- package/src/delivery-hooks/supervisor-owner.js +3 -2
- package/src/delivery-hooks/supervisor-service.js +38 -6
- package/src/delivery-hooks/supervisor-watchdog.js +15 -4
- package/src/delivery-hooks/supervisor.js +151 -18
- package/src/desktop-access.js +2 -1
- package/src/desktop-host.js +35 -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/audit.js +24 -323
- package/src/orchestrator/budget.js +22 -11
- package/src/orchestrator/charter-validation.js +3 -41
- package/src/orchestrator/checklist-continuation.js +2 -14
- package/src/orchestrator/completion-contract.js +4 -49
- package/src/orchestrator/contract.js +5 -9
- package/src/orchestrator/delivery/WORKFLOW.md +30 -710
- package/src/orchestrator/delivery/references/acceptance-evidence.md +4 -216
- package/src/orchestrator/delivery/references/change-boundary-matrix.md +4 -103
- package/src/orchestrator/delivery/references/chrome-user-verification.md +5 -133
- package/src/orchestrator/delivery/references/cross-repository-control.md +4 -74
- package/src/orchestrator/delivery/references/delivery-levels.md +3 -117
- package/src/orchestrator/delivery/references/local-sqm-evidence.md +1 -1
- package/src/orchestrator/delivery/references/minimum-completion.md +4 -144
- package/src/orchestrator/delivery/references/pr-closure.md +4 -200
- package/src/orchestrator/delivery/references/shared-system-specifications.md +4 -161
- package/src/orchestrator/delivery/references/state-and-receipts.md +4 -245
- package/src/orchestrator/delivery/references/state-model.md +4 -185
- package/src/orchestrator/delivery/references/ux-specification.md +4 -95
- package/src/orchestrator/delivery-level.js +2 -10
- package/src/orchestrator/evidence.js +2 -3
- package/src/orchestrator/independent-audit.js +15 -164
- package/src/orchestrator/instruction.js +19 -254
- package/src/orchestrator/loop.js +35 -95
- package/src/orchestrator/outcome-monitor.js +21 -0
- package/src/orchestrator/pr-closure.js +2 -16
- package/src/orchestrator/recovery.js +32 -2
- package/src/orchestrator/ux-specification.js +3 -12
- package/src/provider-delegation.js +336 -0
- package/src/runner.js +5 -2
- package/src/session-supervision/acceptance-reads.js +34 -0
- package/src/session-supervision/artifact-observer.js +138 -0
- package/src/session-supervision/browser-cdp.js +59 -0
- package/src/session-supervision/browser-recovery.js +97 -0
- package/src/session-supervision/codex-app-server.js +61 -22
- 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 +47 -28
- package/src/session-supervision/final-response-recovery.js +5 -0
- package/src/session-supervision/index.js +22 -5
- package/src/session-supervision/interaction-proxy.js +250 -0
- package/src/session-supervision/interaction-task.js +189 -0
- package/src/session-supervision/native-interaction.js +102 -0
- package/src/session-supervision/provider-failure.js +40 -0
- package/src/session-supervision/purpose-instruction.js +14 -14
- package/src/session-supervision/purpose-recovery.js +32 -12
- package/src/session-supervision/purpose-verification.js +50 -216
- package/src/session-supervision/recovery-frontier.js +55 -0
- 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 +6 -0
- package/src/sqm/local-tests.js +2 -1
- 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/startup-preflight.js +2 -20
- package/src/team-status-fence-store.js +32 -0
- package/src/team-threads.js +457 -0
|
@@ -1,6 +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';
|
|
6
|
+
import { continueResultCycle, resultEvent, resultFollowThroughInstruction, correctiveFollowupInstruction } from './result-cycle.mjs';
|
|
4
7
|
import { currentRecoveryIncident, recordRecoveryIncident, recordRecoveryStage, settleRecoveryIncidents, resolveRecoveryReceipt } from './recovery-incident.mjs';
|
|
5
8
|
import { rememberPerspectives, perspectiveScope, recallPerspectives, perspectiveInstruction } from './perspective-memory.mjs';
|
|
6
9
|
import { recordHypothesisSheet } from './hypothesis-sheet.mjs';
|
|
@@ -9,7 +12,7 @@ import { recordHandoffStage } from './consistency-handoff.mjs';
|
|
|
9
12
|
import { retainApplicableRules } from './purpose-rule-policy.mjs';
|
|
10
13
|
import { reconcileRequirements, retainControllerEvidence, reconcileFindings, findingText, evidenceLedger, currentPurposeObservation } from './purpose-evidence.mjs';
|
|
11
14
|
import { workspaceIdentity } from './purpose-workspace.mjs';
|
|
12
|
-
import { searchEnabled, failureLimitReached, searchState, recordSearchExperiment, rememberSearchWait, searchNeedsPivot, markSearchPivot, repeatedSearchAction } from './purpose-search.mjs';
|
|
15
|
+
import { actionReversibilityInstruction, searchEnabled, failureLimitReached, searchState, recordSearchExperiment, rememberSearchWait, searchNeedsPivot, markSearchPivot, repeatedSearchAction } from './purpose-search.mjs';
|
|
13
16
|
// Standalone by design: this exact module is shipped to both provider plugins.
|
|
14
17
|
import { createHash, randomUUID } from 'node:crypto';
|
|
15
18
|
import { mkdir, open, readFile, readdir, realpath, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
@@ -275,7 +278,16 @@ export async function updateRecord(root, provider, session, mutate, { inspectPro
|
|
|
275
278
|
if (serialized === previousBytes) return next;
|
|
276
279
|
temporary = `${file}.${randomUUID()}.tmp`;
|
|
277
280
|
await writeFile(temporary, serialized, { mode: 0o600 });
|
|
278
|
-
|
|
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
|
+
}
|
|
279
291
|
}
|
|
280
292
|
return next;
|
|
281
293
|
} finally { if (temporary) await rm(temporary, { force: true }); await releaseStateLock(lock, lockToken); }
|
|
@@ -416,7 +428,7 @@ function researchWaitDiagnosis(managed) {
|
|
|
416
428
|
}
|
|
417
429
|
// Enrollment repair keeps its original local-only gate. Later research waits may
|
|
418
430
|
// diagnose recorded uncertainty, but never dispatch or approve the waiting action.
|
|
419
|
-
function canResearchReplan(run, provider, now) {
|
|
431
|
+
export function canResearchReplan(run, provider, now) {
|
|
420
432
|
const m = run?.managed, d = m?.decision;
|
|
421
433
|
const channel = m?.resumeChannel === 'codex_app_server' && provider === 'codex' ||
|
|
422
434
|
m?.capability === 'live_hook' && ['codex', 'claude'].includes(provider);
|
|
@@ -463,7 +475,7 @@ export function evaluateAssessedPlan(plan, policy, scopes, now = Date.now(), { a
|
|
|
463
475
|
const localWrite = action.kind === 'local_edit' || allowLocalTestWrites && action.kind === 'local_test' && facts.externalEffect === 'local';
|
|
464
476
|
const autonomous = policy === 'autonomous' && llmDelegated;
|
|
465
477
|
const external = autonomous && allowReversibleExternal && action.kind === 'external' && facts.externalEffect === 'external';
|
|
466
|
-
let reason = null, waitKind = 'research';
|
|
478
|
+
let reason = null, waitKind = autonomous ? 'research' : 'human';
|
|
467
479
|
if (!scopes.includes(action.scope)) { reason = 'Action scope is outside the task enrollment.'; waitKind = 'authority'; }
|
|
468
480
|
else if (facts.inObjective !== true) reason = 'Establish objective fit or choose work within the original task.';
|
|
469
481
|
else if (facts.reversibility === 'irreversible') { reason = 'Irreversible consequences require an exact user decision.'; waitKind = 'human'; }
|
|
@@ -494,11 +506,12 @@ export function canExecuteAutonomousExternal(managed) {
|
|
|
494
506
|
// Shared by live Stop hooks and cold exact-session recovery. Keep the result
|
|
495
507
|
// contract identical across transports; the host still verifies acceptance.
|
|
496
508
|
export function assessedCheckpointContract(decision, managed = null) {
|
|
497
|
-
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.' : '
|
|
498
|
-
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} 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
|
+
|
|
499
512
|
}
|
|
500
513
|
export function understandingCheckpointContract(decision) {
|
|
501
|
-
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.`;
|
|
502
515
|
}
|
|
503
516
|
function boundedContextList(value, maxItems = 8, maxLength = 300) {
|
|
504
517
|
if (!Array.isArray(value) || value.length > maxItems) throw new Error('Session understanding has too many items.');
|
|
@@ -515,7 +528,9 @@ function boundedMatchingContext(value) {
|
|
|
515
528
|
}
|
|
516
529
|
function sessionUnderstanding(value) {
|
|
517
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.');
|
|
518
|
-
|
|
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 => {
|
|
519
534
|
if (!['user', 'assistant', 'artifact'].includes(item?.source)) throw new Error('Understanding basis needs a source.');
|
|
520
535
|
const turnRef = contextText(item.turnRef, 100) || null;
|
|
521
536
|
const snippet = contextText(item.snippet, 240) || null;
|
|
@@ -587,11 +602,13 @@ function recordPurposeProgress(run, now, countAttempt = true) {
|
|
|
587
602
|
}
|
|
588
603
|
function recordDecision(managed, plan, now) {
|
|
589
604
|
let decision = evaluateAssessedPlan(plan, managed.autonomyPolicy, managed.approvalScopes, now, { allowLocalTestWrites: searchEnabled(managed), llmDelegated: searchEnabled(managed), allowReversibleExternal: searchEnabled(managed) && managed.resumeChannel === 'codex_app_server' });
|
|
590
|
-
|
|
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)))) {
|
|
608
|
+
rememberSearchWait(managed, decision.waitKind === 'research' ? 'research' : 'authorization', { action: decision.action, reason: decision.reason }, now);
|
|
592
609
|
const priorities = new Map((managed.purpose.lastReview?.priorities || []).map(row => [row.optionId, row.rank]));
|
|
593
610
|
for (const candidate of [...plan.alternatives].sort((a, b) => (priorities.get(a.id) ?? 99) - (priorities.get(b.id) ?? 99))) {
|
|
594
|
-
if (candidate.id === plan.selectedOptionId || !candidate.independent) continue;
|
|
611
|
+
if (candidate.id === plan.selectedOptionId || !candidate.independent || dependencyPending(candidate)) continue;
|
|
595
612
|
const alternative = { ...plan, selectedOptionId: candidate.id, recommendation: { optionId: candidate.id, reason: 'Continue the highest listed eligible independent alternative.' } };
|
|
596
613
|
const assessed = evaluateAssessedPlan(alternative, managed.autonomyPolicy, managed.approvalScopes, now, { allowLocalTestWrites: searchEnabled(managed), llmDelegated: searchEnabled(managed), allowReversibleExternal: searchEnabled(managed) && managed.resumeChannel === 'codex_app_server' });
|
|
597
614
|
if (assessed.outcome !== 'wait' && !managed.rejectedActionOverflow && !managed.rejectedActionDigests?.includes(digest(JSON.stringify(assessed.action)))) {
|
|
@@ -624,8 +641,7 @@ function recordDecision(managed, plan, now) {
|
|
|
624
641
|
writeResources: option?.writeResources || (decision.action.kind === 'read' ? [] : ['task']),
|
|
625
642
|
requiredCapabilities: option?.requiredCapabilities || [decision.action.kind], requiredPermissions: option?.requiredPermissions || [decision.action.scope],
|
|
626
643
|
status: decision.outcome === 'wait' ? 'waiting' : 'planned', evidenceRefs: [], purposeRevision: ledger.purposeRevision || 1, action: decision.action, createdAt: now });
|
|
627
|
-
|
|
628
|
-
if (ledger.units.length > 192) throw new Error('Work frontier needs reconciliation before more dispatch.');
|
|
644
|
+
compactWorkHistory(ledger);
|
|
629
645
|
}
|
|
630
646
|
managed.plan = plan; managed.decision = decision; managed.proposedAction = decision.action;
|
|
631
647
|
managed.decisionHistory ||= [];
|
|
@@ -699,10 +715,11 @@ function managedSpec(spec, session, provider, observedProcess, admission, now) {
|
|
|
699
715
|
const value = config.deliveryLevel;
|
|
700
716
|
if (!Number.isInteger(value?.requested) || value.requested < 1 || value.requested > 5 ||
|
|
701
717
|
!Number.isInteger(value.effective) || value.effective < value.requested || value.effective > 5 ||
|
|
702
|
-
value.numberingVersion !== 'assurance-ascending-v2'
|
|
718
|
+
value.numberingVersion !== 'assurance-ascending-v2') throw new Error('Delivery level needs requested/effective levels 1-5 and assurance-ascending-v2 numbering.');
|
|
703
719
|
deliveryLevel = { requested: value.requested, effective: value.effective, numberingVersion: value.numberingVersion, selectionRef: text(value.selectionRef, 200) };
|
|
704
720
|
}
|
|
705
721
|
const managed = { ...(config.purpose ? { purpose: structuredClone(config.purpose) } : {}), taskId: randomUUID(), deliveryLevel, thread: { provider, id: session },
|
|
722
|
+
independentReviewRequired: config.independentReviewRequired === true,
|
|
706
723
|
actionId: text(config.actionId, 200) || null, actionDueAt, acceptanceCriteria: criteria, verified: {}, remainingWork: text(config.remainingWork, 4000) || null,
|
|
707
724
|
initialAction, proposedAction: initialAction, approvalPolicy, approvalScopes: [...new Set(scopes)].slice(0, 20),
|
|
708
725
|
grants: Object.fromEntries(scopes.map(scope => [scope, { decision: approvalPolicy }])), maxNoProgress: positive(config.maxNoProgress, 3, 20),
|
|
@@ -750,7 +767,63 @@ export function deliveryAcceptanceContext(run) {
|
|
|
750
767
|
`${run.managed?.deliveryLevel ? ` Frozen delivery level: ${JSON.stringify(run.managed.deliveryLevel)}.` : ''}` +
|
|
751
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.`;
|
|
752
769
|
}
|
|
753
|
-
|
|
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,
|
|
754
827
|
liveHookAdmission, autoContinueEpoch, providerLinkEpoch, inspectProcess = processIdentity, now = Date.now() }) {
|
|
755
828
|
const link = await providerLinkState(root, provider);
|
|
756
829
|
if (providerLinkEpoch !== undefined && providerLinkEpoch !== link.epoch) throw Object.assign(new Error("SIMY provider connection changed before enrollment."), { code: "provider_link_changed" });
|
|
@@ -775,12 +848,27 @@ export async function startDelivery({ root = rootDirectory(), provider, session,
|
|
|
775
848
|
}
|
|
776
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.');
|
|
777
850
|
const upgrading = upgradeRunId !== undefined;
|
|
778
|
-
|
|
779
|
-
|
|
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.');
|
|
780
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.');
|
|
781
868
|
const managedConfig = managed && { ...(managed === true ? {} : managed),
|
|
782
869
|
...(continuationPolicy === undefined ? {} : { continuationPolicy }) };
|
|
783
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.');
|
|
784
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.');
|
|
785
873
|
if (!managedState && continuationMode(continuationPolicy) === 'progress') throw new Error('Progress continuation requires frozen managed acceptance criteria.');
|
|
786
874
|
if (autoContinueEpoch && managedState) managedState.autoContinueEpoch = autoContinueEpoch;
|
|
@@ -792,7 +880,7 @@ export async function startDelivery({ root = rootDirectory(), provider, session,
|
|
|
792
880
|
identity: liveHookAdmission.identity, hookStartedAt: liveHookAdmission.hookStartedAt };
|
|
793
881
|
const waiting = managedState?.decision?.outcome === 'wait';
|
|
794
882
|
const queued = managedState?.capability === 'codex_app_server';
|
|
795
|
-
const run = {
|
|
883
|
+
const run = { ...initialDeliveryRun(objective, nextStep, now, maxFailures), state: waiting ? 'blocked' : queued ? 'recovery_required' : 'active',
|
|
796
884
|
...(managedState?.purpose ? { ...(maxMinutes === undefined ? {} : { deadline: now + positive(maxMinutes, 120, 1440) * 60_000 }), ...(maxContinuations === undefined ? {} : { maxContinuations: positive(maxContinuations, 50, 1000) }) } : {}),
|
|
797
885
|
maxFailures: positive(maxFailures, 3, 10), maxFailuresSource: maxFailures === undefined ? 'default' : 'explicit',
|
|
798
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',
|
|
@@ -819,6 +907,15 @@ export async function startDelivery({ root = rootDirectory(), provider, session,
|
|
|
819
907
|
if (waiting) escalate(run, run.reason);
|
|
820
908
|
const latest = await providerLinkState(root, provider);
|
|
821
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
|
+
}
|
|
822
919
|
return { ...previous, run };
|
|
823
920
|
});
|
|
824
921
|
}
|
|
@@ -866,6 +963,17 @@ export function queuePurposeDiagnosis(run, reason, key, now = Date.now(), recove
|
|
|
866
963
|
(!searchEnabled(run.managed) && run.managed.pendingApproval?.denied) || run.managed.providerWarning && !run.managed.nativeWait || run.managed.usageBlocked ||
|
|
867
964
|
['pending', 'transferred', 'pending_received'].includes(run.managed.handoff?.status)) return false;
|
|
868
965
|
const purpose = run.managed.purpose;
|
|
966
|
+
// Older decisions classified an AI investigation as an authorization unit.
|
|
967
|
+
// Repair only the exact still-selected research wait, never a real approval.
|
|
968
|
+
if (recoveryCode === 'research_wait' && canResearchReplan(run, 'codex', now)) {
|
|
969
|
+
const decision = run.managed.decision;
|
|
970
|
+
for (const wait of searchState(run.managed).waits) {
|
|
971
|
+
if (wait.type !== 'authorization' || wait.condition !== decision.reason || JSON.stringify(wait.action) !== JSON.stringify(decision.action)) continue;
|
|
972
|
+
wait.type = 'research';
|
|
973
|
+
const ledger = evidenceLedger(run.managed);
|
|
974
|
+
for (const unit of ledger.units) if (unit.id === `wait:${wait.id}:revision:${ledger.purposeRevision}` && unit.status === 'waiting' && JSON.stringify(unit.action) === JSON.stringify(decision.action)) unit.status = 'superseded';
|
|
975
|
+
}
|
|
976
|
+
}
|
|
869
977
|
purpose.recoveries ||= [];
|
|
870
978
|
if (searchEnabled(run.managed)) {
|
|
871
979
|
// Back off the same obstacle; a newly observed obstacle or independently
|
|
@@ -897,6 +1005,7 @@ export function queuePurposeDiagnosis(run, reason, key, now = Date.now(), recove
|
|
|
897
1005
|
question: run.managed.understandingQuestion?.text || run.managed.userQuestion || null,
|
|
898
1006
|
dependency: run.managed.dependencyWait || null };
|
|
899
1007
|
recordDecision(run.managed, purposeDiagnosisPlan(run.managed, reason), now);
|
|
1008
|
+
purpose.diagnosisDecisionId = run.managed.decision.id;
|
|
900
1009
|
if (run.managed.executionUncertain) prepareRecoveryDiagnosis(run, now);
|
|
901
1010
|
if (run.managed.phase === 'needs_clarification') run.managed.phase = 'understanding';
|
|
902
1011
|
run.managed.userQuestion = null; run.managed.understandingQuestion = null; run.managed.dependencyWait = null;
|
|
@@ -909,6 +1018,9 @@ export function queuePurposeDiagnosis(run, reason, key, now = Date.now(), recove
|
|
|
909
1018
|
return true;
|
|
910
1019
|
}
|
|
911
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
|
+
|
|
912
1024
|
export async function checkpointDelivery({ root = rootDirectory(), provider, session, cwd = process.cwd(), checkpoint, expectedLease, toolEvidence = [], sourceContext, controllerEvidence, now = Date.now() }) {
|
|
913
1025
|
if (checkpoint?.kind === 'action_association') return checkpointActionAssociation({ root, provider, session, cwd, checkpoint, expectedLease, now });
|
|
914
1026
|
if (checkpoint?.kind === 'session_understanding') return checkpointUnderstanding({ root, provider, session, cwd, checkpoint, expectedLease, sourceContext, controllerEvidence, now });
|
|
@@ -916,7 +1028,7 @@ export async function checkpointDelivery({ root = rootDirectory(), provider, ses
|
|
|
916
1028
|
const statuses = ['in_progress', 'completed', 'blocked', 'error'];
|
|
917
1029
|
if (!checkpoint || !statuses.includes(checkpoint.status)) throw new Error('Checkpoint status must be in_progress, completed, blocked, or error.');
|
|
918
1030
|
if (!text(checkpoint.summary)) throw new Error('Checkpoint summary is required.');
|
|
919
|
-
if (
|
|
1031
|
+
if (checkpoint.status === 'error' && !text(checkpoint.nextStep)) throw new Error('A concrete nextStep is required.');
|
|
920
1032
|
if (checkpoint.status === 'blocked' && !['authorization', 'missing_information', 'external_dependency'].includes(checkpoint.blocker)) throw new Error('Specify authorization, missing_information, or external_dependency.');
|
|
921
1033
|
const evidence = Array.isArray(checkpoint.evidence) ? checkpoint.evidence.map(v => text(v, 1000)).filter(Boolean).slice(0, 20) : [];
|
|
922
1034
|
const settled = await updateRecord(root, provider, session, async record => {
|
|
@@ -926,10 +1038,34 @@ export async function checkpointDelivery({ root = rootDirectory(), provider, ses
|
|
|
926
1038
|
let repairPlan = null;
|
|
927
1039
|
if (!run || !checkpointDirectoryMatches(record, cwd)) throw new Error('No matching active Delivery Loop.');
|
|
928
1040
|
if (run.managed?.phase === 'understanding' || run.managed?.phase === 'needs_clarification') throw new Error('Session understanding needs its separate structured checkpoint.');
|
|
929
|
-
if (checkpoint.status === 'completed' && !run.managed && (!evidence.length || checkpoint.acceptanceMet !== true)) throw new Error('Completion requires acceptanceMet and concrete verification evidence.');
|
|
930
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.');
|
|
931
1042
|
if (TERMINAL.has(run.state) || run.state === 'blocked' || run.state === 'recovery_required') throw new Error('Loop is stopped or awaiting a human resume.');
|
|
932
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
|
+
}
|
|
933
1069
|
if (run.managed) {
|
|
934
1070
|
if (searchEnabled(run.managed) && expectedLease && sourceContext) run.managed.purpose.originalContext ||= sourceContext;
|
|
935
1071
|
if (searchEnabled(run.managed) && expectedLease) retainControllerEvidence(run.managed, controllerEvidence, now);
|
|
@@ -941,7 +1077,7 @@ export async function checkpointDelivery({ root = rootDirectory(), provider, ses
|
|
|
941
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' };
|
|
942
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'));
|
|
943
1079
|
if (flexibleReasoning(run.managed) && capturedAction) actionResult.observationSource = 'controller_tool_result';
|
|
944
|
-
|
|
1080
|
+
// Observation prose is optional; actual execution receipts belong to the controller.
|
|
945
1081
|
// Local observations remain the worker's report until independent
|
|
946
1082
|
// inspection. Missing paperwork alone is not an unknown side effect.
|
|
947
1083
|
// For remote effects, keep reconciliation before any repeat operation.
|
|
@@ -958,11 +1094,7 @@ export async function checkpointDelivery({ root = rootDirectory(), provider, ses
|
|
|
958
1094
|
const workerPlan = checkpoint.plan ? assessedPlan(checkpoint.plan, run.managed) : null;
|
|
959
1095
|
const review = validatePurposeReview(checkpoint.purposeReview, workerPlan, run.managed.purpose.ruleIds, searchEnabled(run.managed) ? run.managed.purpose.lastReview?.rules || [] : [], run.managed);
|
|
960
1096
|
recordPurposeReview(run.managed, review, now);
|
|
961
|
-
|
|
962
|
-
effectiveStatus = 'in_progress';
|
|
963
|
-
repairPlan = purposeDiagnosisPlan(run.managed, 'Completion is unproven: resolve failed/unverified workflow rules against the original purpose.');
|
|
964
|
-
nextStep = repairPlan.alternatives[0].action.summary;
|
|
965
|
-
}
|
|
1097
|
+
|
|
966
1098
|
}
|
|
967
1099
|
const previouslyVerified = Object.keys(run.managed.verified).length;
|
|
968
1100
|
run.managed.remainingWork = text(checkpoint.remainingWork, 4000) || run.managed.remainingWork;
|
|
@@ -971,6 +1103,9 @@ export async function checkpointDelivery({ root = rootDirectory(), provider, ses
|
|
|
971
1103
|
if (searchEnabled(run.managed) && performed && actionResult) {
|
|
972
1104
|
const unit = evidenceLedger(run.managed).units?.find(row => row.id === performed.id);
|
|
973
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;
|
|
974
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';}
|
|
975
1110
|
}
|
|
976
1111
|
}
|
|
@@ -989,6 +1124,11 @@ export async function checkpointDelivery({ root = rootDirectory(), provider, ses
|
|
|
989
1124
|
if (performed) {
|
|
990
1125
|
run.managed.workHistory ||= [];
|
|
991
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
|
+
}
|
|
992
1132
|
run.managed.workHistory = run.managed.workHistory.slice(-10);
|
|
993
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 };
|
|
994
1134
|
if (run.managed.approvedAction?.id === performed.id) delete run.managed.approvedAction;
|
|
@@ -997,15 +1137,15 @@ export async function checkpointDelivery({ root = rootDirectory(), provider, ses
|
|
|
997
1137
|
if (run.managed.autonomyPolicy) {
|
|
998
1138
|
const plan = repairPlan || assessedPlan(checkpoint.plan, run.managed);
|
|
999
1139
|
const selected = plan.alternatives.find(item => item.id === plan.selectedOptionId);
|
|
1000
|
-
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.');
|
|
1001
1141
|
recordDecision(run.managed, plan, now);
|
|
1002
|
-
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)) {
|
|
1003
1143
|
recordDecision(run.managed, purposeDiagnosisPlan(run.managed, `Investigate the ${actionResult.status} action before further changes: ${actionResult.summary}`), now);
|
|
1004
|
-
} 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')) {
|
|
1005
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.';
|
|
1006
1146
|
run.managed.pendingApproval = { id: run.managed.decision.id, ...run.managed.decision.action, reason: run.managed.decision.reason };
|
|
1007
1147
|
}
|
|
1008
|
-
} else run.managed.proposedAction = actionDescriptor(checkpoint.proposedAction);
|
|
1148
|
+
} else run.managed.proposedAction = unsupportedWait ? repairPlan.alternatives[0].action : actionDescriptor(checkpoint.proposedAction);
|
|
1009
1149
|
}
|
|
1010
1150
|
if (checkpoint.status === 'blocked' && checkpoint.blocker === 'authorization') {
|
|
1011
1151
|
const request = checkpoint.approval;
|
|
@@ -1027,8 +1167,7 @@ export async function checkpointDelivery({ root = rootDirectory(), provider, ses
|
|
|
1027
1167
|
run.managed.pendingApproval = { id: digest(`${run.managed.taskId}:${run.revision + 1}:${JSON.stringify(proposed)}`), ...proposed };
|
|
1028
1168
|
}
|
|
1029
1169
|
}
|
|
1030
|
-
if (
|
|
1031
|
-
if (!text(checkpoint.question, 1000)) throw new Error('Missing information blocker needs a specific user question.');
|
|
1170
|
+
if (effectiveStatus === 'blocked' && checkpoint.blocker === 'missing_information') {
|
|
1032
1171
|
run.managed.userQuestion = text(checkpoint.question, 1000);
|
|
1033
1172
|
}
|
|
1034
1173
|
}
|
|
@@ -1037,7 +1176,7 @@ export async function checkpointDelivery({ root = rootDirectory(), provider, ses
|
|
|
1037
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.');
|
|
1038
1177
|
run.managed.dependencyWait = { id: randomUUID(), condition: text(wait.condition, 1000), owner: text(wait.owner, 200), nextCheck: text(wait.nextCheck, 1000), createdAt: now };
|
|
1039
1178
|
}
|
|
1040
|
-
if (searchEnabled(run.managed) && checkpoint.status === 'blocked') {
|
|
1179
|
+
if (searchEnabled(run.managed) && checkpoint.status === 'blocked' && !unsupportedWait) {
|
|
1041
1180
|
rememberSearchWait(run.managed, checkpoint.blocker, { reason: checkpoint.summary, question: checkpoint.question,
|
|
1042
1181
|
condition: checkpoint.waitCondition?.condition, owner: checkpoint.waitCondition?.owner, action: checkpoint.approval || null }, now);
|
|
1043
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);
|
|
@@ -1065,26 +1204,41 @@ export async function checkpointDelivery({ root = rootDirectory(), provider, ses
|
|
|
1065
1204
|
nextStep = run.nextStep;
|
|
1066
1205
|
}
|
|
1067
1206
|
run.blockerSource = effectiveStatus === 'blocked' ? 'checkpoint' : null;
|
|
1068
|
-
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;
|
|
1069
1213
|
run.revision += 1; run.updatedAt = now;
|
|
1070
1214
|
recordManagedOutcomeProgress(run, now);
|
|
1071
|
-
|
|
1215
|
+
// File predicates were reread above; a prior semantic verdict can belong
|
|
1216
|
+
// to the workspace before the latest repair, so inspect the current result.
|
|
1217
|
+
const semanticReviewRequired = effectiveAcceptanceCriteria(run.managed).some(c => c.check.type === 'semantic');
|
|
1218
|
+
if (searchEnabled(run.managed) && effectiveStatus === 'completed' && (run.managed.independentReviewRequired || semanticReviewRequired || pendingAcceptance(run).length || purposeCompletionBarrier(run))) {
|
|
1072
1219
|
queuePurposeVerification(run, checkpoint.summary, now);
|
|
1073
1220
|
} else if (run.managed && effectiveStatus === 'completed') {
|
|
1074
1221
|
const pending = pendingAcceptance(run);
|
|
1075
1222
|
if (pending.length) { run.state = 'verification_pending'; run.reason = acceptancePendingReason(run); }
|
|
1223
|
+
else if (searchEnabled(run.managed) && run.managed.acceptanceCriteria.length && !purposeCompletionBarrier(run)) {
|
|
1224
|
+
run.state = 'completed'; run.reason = checkpoint.summary;
|
|
1225
|
+
bindControllerCompletion(run, now);
|
|
1226
|
+
}
|
|
1076
1227
|
else if (run.managed.contextBootstrap && !run.managed.acceptanceCriteria.length) {
|
|
1077
1228
|
run.state = 'reported_done'; run.reason = 'Original session reported the goal done; no independent acceptance check was available.';
|
|
1078
1229
|
}
|
|
1079
1230
|
}
|
|
1080
|
-
if (run.managed?.liveWorkspaceLease) {
|
|
1231
|
+
if (run.managed?.liveWorkspaceLease || expectedLease && run.managed?.executionAttempt?.id === expectedLease) {
|
|
1081
1232
|
run.managed.executionAttempt = { ...run.managed.executionAttempt, phase: 'checkpointed', checkpointRevision: run.revision, endedAt: now };
|
|
1082
|
-
run.managed.liveWorkspaceLease.checkpointRevision = run.revision;
|
|
1233
|
+
if (run.managed.liveWorkspaceLease) run.managed.liveWorkspaceLease.checkpointRevision = run.revision;
|
|
1083
1234
|
}
|
|
1084
1235
|
for (const request of run.managed?.consistencyOutbox || []) if (request.decisionId && request.decisionId === checkpoint.actionResult?.decisionId) {
|
|
1085
1236
|
recordHandoffStage(request, run.managed.workHistory?.at(-1)?.result?.status === 'observed' ? 'inspected' : 'inspection_failed', now);
|
|
1086
1237
|
request.observedAt = now; request.result = run.managed.workHistory?.at(-1)?.result?.status || checkpoint.actionResult.status;
|
|
1087
1238
|
}
|
|
1239
|
+
await continueResultCycle(record, resultEvent(record, { kind: 'checkpoint', origin: 'checkpointDelivery',
|
|
1240
|
+
sourceId: expectedLease || `checkpoint:${run.revision}`, attemptId: expectedLease,
|
|
1241
|
+
resultRef: `checkpoint:${run.revision}`, now }));
|
|
1088
1242
|
notifyState(run, now);
|
|
1089
1243
|
return record;
|
|
1090
1244
|
});
|
|
@@ -1111,7 +1265,10 @@ export function externalReviewIsCurrent(managed,review,now) {
|
|
|
1111
1265
|
if (review.semanticInspection?.source === 'controller_actual_read_set') refs.push(...(review.semanticInspection.observationRefs || []).map(ref => ({ ref })));
|
|
1112
1266
|
const ledger=managed.purpose.evidence;
|
|
1113
1267
|
const observations=[...(ledger?.observations||[]),...(ledger?.reviewObservationActionId===(review.decisionId||managed.decision.id) ? ledger.reviewObservations||[] : [])];
|
|
1114
|
-
|
|
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
|
+
|
|
1115
1272
|
}
|
|
1116
1273
|
export async function settlePurposeVerification({ root, provider, session, expectedLease, review, verifySnapshot, now = Date.now() }) {
|
|
1117
1274
|
const started = Date.now(), initialNow = now;
|
|
@@ -1126,8 +1283,16 @@ export async function settlePurposeVerification({ root, provider, session, expec
|
|
|
1126
1283
|
managed.purpose.search?.verificationDecisionId !== managed.decision.id || run.revision !== managed.resumeLease.checkpointRevision || now >= run.deadline) throw new Error('Independent review lease changed.');
|
|
1127
1284
|
const state = searchState(managed);
|
|
1128
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
|
+
}
|
|
1129
1290
|
retainApplicableRules(managed, review.rules || []);
|
|
1130
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
|
+
}
|
|
1131
1296
|
if (currentDigest !== review.workspaceDigest) {
|
|
1132
1297
|
review = { ...review, passed: false, needsHuman: false, findings: [...review.findings, 'Workspace changed before review settlement; verify the new revision.'] };
|
|
1133
1298
|
}
|
|
@@ -1137,45 +1302,64 @@ export async function settlePurposeVerification({ root, provider, session, expec
|
|
|
1137
1302
|
'Required operation, effect reconciliation or original-purpose contract remains unresolved. Preserve the waiting unit and continue independent authorized work.'] };
|
|
1138
1303
|
}
|
|
1139
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.'}};
|
|
1140
|
-
// Validate counterevidence against a disposable ledger before mutations.
|
|
1141
|
-
// Verifier contract failures are verifier work, never worker checkpoint errors.
|
|
1142
|
-
if (!review.controllerEvidenceFailure) {
|
|
1143
|
-
try { reconcileFindings(structuredClone(managed), review, now); }
|
|
1144
|
-
catch (error) {
|
|
1145
|
-
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;
|
|
1146
|
-
review = { ...review, passed: false, needsHuman: false, controllerEvidenceFailure: {
|
|
1147
|
-
code: 'independent_finding_evidence_invalid', targets: [...(review.resolutions || []).map(row => `resolution:${row.id}`), ...(review.findings || []).filter(row => row.reopenReason).map(row => `reopen:${row.id}`)],
|
|
1148
|
-
instruction: error.message + ' Repeat isolated verification and reconcile existing findings with primary reads. For reopening use the exact prior closure read scope.',
|
|
1149
|
-
requiredReads: (managed.purpose.evidence?.findings || []).filter(row => row.status !== 'open').map(row => ({ id: row.id, closedReads: row.closedReads || [] }))
|
|
1150
|
-
} };
|
|
1151
|
-
}
|
|
1152
|
-
}
|
|
1153
1305
|
if (review.controllerEvidenceFailure) {
|
|
1154
1306
|
const prior = state.verifierFailures;
|
|
1155
1307
|
const key = digest(JSON.stringify([currentDigest, review.controllerEvidenceFailure.code, review.controllerEvidenceFailure.targets]));
|
|
1156
1308
|
state.verifierFailures = { key, attempts: prior?.key === key ? prior.attempts + 1 : 1, at: now, failure: review.controllerEvidenceFailure };
|
|
1157
1309
|
state.review = review;
|
|
1158
1310
|
run.revision += 1; run.updatedAt = now;
|
|
1159
|
-
|
|
1160
|
-
|
|
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;
|
|
1161
1324
|
notifyState(run, now); return record;
|
|
1162
1325
|
}
|
|
1163
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
|
+
}
|
|
1164
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
|
+
};
|
|
1165
1350
|
for (const result of (review.passed || review.needsHuman) ? review.workUnits || [] : []) {
|
|
1166
1351
|
const unit=units.find(row=>row.id===result.id);
|
|
1167
|
-
if (!unit || !['verified','superseded'].includes(result.status) || !result.
|
|
1168
|
-
unit.criterionIds.some(id=>!
|
|
1169
|
-
if (unit.purposeRevision !== evidenceLedger(managed).purposeRevision)
|
|
1352
|
+
if (!unit || !['verified','superseded'].includes(result.status) || !text(result.result || review.summary) ||
|
|
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;
|
|
1170
1355
|
unit.status=result.status;unit.evidenceRefs=result.evidenceRefs;unit.verifiedAt=now;
|
|
1171
1356
|
}
|
|
1172
1357
|
state.review = review; delete state.verificationDecisionId;
|
|
1173
1358
|
run.revision += 1; run.updatedAt = now;
|
|
1174
|
-
await verifyAutomaticCriteria(record, now);
|
|
1175
1359
|
if (review.passed || review.needsHuman) {
|
|
1176
1360
|
for (const criterion of effectiveAcceptanceCriteria(managed)) {
|
|
1177
|
-
if (criterion.check.type === 'semantic' && review.criteria.some(row => row.id === criterion.id && row.passed)) {
|
|
1178
|
-
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' };
|
|
1179
1363
|
}
|
|
1180
1364
|
}
|
|
1181
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 || []);}
|
|
@@ -1205,18 +1389,11 @@ export async function settlePurposeVerification({ root, provider, session, expec
|
|
|
1205
1389
|
notifyState(run, now); return record;
|
|
1206
1390
|
}
|
|
1207
1391
|
}
|
|
1208
|
-
if (!review.passed && !review.needsHuman && openFindings.length && openFindings.every(item => item.owner === 'controller')) {
|
|
1209
|
-
const ledger = evidenceLedger(managed);
|
|
1210
|
-
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');
|
|
1211
|
-
if (state.controllerLookup !== lookup && (ledger.baseline || ledger.receipts.length)) {
|
|
1212
|
-
state.controllerLookup = lookup;
|
|
1213
|
-
queuePurposeVerification(run, 'Controller retrieved existing baseline and tool receipts; reconcile the missing evidence against the supplied ledger before requesting worker repairs.', now);
|
|
1214
|
-
notifyState(run, now); return record;
|
|
1215
|
-
}
|
|
1216
|
-
}
|
|
1217
1392
|
if (!review.passed && !review.needsHuman) {
|
|
1218
1393
|
const reason = `Outcome review found remaining work: ${[review.summary, ...openFindings.map(findingText)].join(' ').slice(0, 1800)}`;
|
|
1219
|
-
|
|
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);
|
|
1220
1397
|
state.phase = 'repairing'; managed.purpose.lastRecovery = { reason, at: now };
|
|
1221
1398
|
run.checkpoint = null; run.state = 'recovery_required'; run.recoveryCause = 'session_supervision';
|
|
1222
1399
|
run.nextStep = managed.decision.action.summary; run.reason = reason; managed.autoRecovery = true;
|
|
@@ -1271,6 +1448,13 @@ async function checkpointUnderstanding({ root, provider, session, cwd, checkpoin
|
|
|
1271
1448
|
validatePurposeReview(checkpoint.purposeReview, plan, run.managed.purpose.ruleIds, searchEnabled(run.managed) ? run.managed.purpose.lastReview?.rules || [] : [], run.managed), now);
|
|
1272
1449
|
if (searchEnabled(run.managed) && expectedLease && sourceContext) run.managed.purpose.originalContext ||= sourceContext;
|
|
1273
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
|
+
}
|
|
1274
1458
|
run.managed.sessionUnderstanding = understanding;
|
|
1275
1459
|
run.managed.understandingRevision = (run.managed.understandingRevision || 0) + 1;
|
|
1276
1460
|
run.managed.understandingAnswer = null;
|
|
@@ -1429,7 +1613,7 @@ export async function verifyDelivery({ root = rootDirectory(), provider, session
|
|
|
1429
1613
|
run.updatedAt = now; return record;
|
|
1430
1614
|
});
|
|
1431
1615
|
}
|
|
1432
|
-
function prepareWaitingDiagnosis(run, reason, now) {
|
|
1616
|
+
export function prepareWaitingDiagnosis(run, reason, now) {
|
|
1433
1617
|
const managed = run.managed, scope = managed.approvalScopes[0];
|
|
1434
1618
|
if (!scope) throw new Error('Waiting recovery needs an enrolled scope.');
|
|
1435
1619
|
const action = { kind: 'read', scope, summary: 'Inspect the resolved wait and current result before choosing new work; do not replay the prior action' };
|
|
@@ -1558,7 +1742,10 @@ export function decideStop(run, now = Date.now()) {
|
|
|
1558
1742
|
// A fresh, explicitly evidenced completion may finish at the budget boundary.
|
|
1559
1743
|
const fresh = next.checkpoint && next.revision > next.consumedRevision;
|
|
1560
1744
|
const cp = fresh ? next.checkpoint : null;
|
|
1561
|
-
|
|
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) {
|
|
1562
1749
|
const pending = pendingAcceptance(next);
|
|
1563
1750
|
if (pending.length) return finish('verification_pending', acceptancePendingReason(next));
|
|
1564
1751
|
if (!next.managed || !next.managed.acceptanceCriteria.length) return finish('reported_done',
|
|
@@ -1610,7 +1797,7 @@ export function decideStop(run, now = Date.now()) {
|
|
|
1610
1797
|
}
|
|
1611
1798
|
const reportedStep = next.managed.associationCheckpointRevision !== next.revision && next.managed.contextBootstrap && next.managed.phase === 'active' && !next.managed.acceptanceCriteria.length &&
|
|
1612
1799
|
!['session_understanding', 'action_association'].includes(cp?.kind) && cp?.status === 'in_progress' &&
|
|
1613
|
-
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');
|
|
1614
1801
|
if (next.managed.continuationPolicy === 'progress') {
|
|
1615
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));
|
|
1616
1803
|
} else if (next.managed.verifiedRevision > next.managed.consumedVerifiedRevision) next.managed.noProgress = 0;
|
|
@@ -1653,8 +1840,15 @@ export function decideStop(run, now = Date.now()) {
|
|
|
1653
1840
|
if (legacyApprovedHook) delete next.managed.approvedAction;
|
|
1654
1841
|
}
|
|
1655
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
|
+
}
|
|
1656
1850
|
const selected = next.managed?.autonomyPolicy ? next.managed.decision : null;
|
|
1657
|
-
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()}`;
|
|
1658
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.` } };
|
|
1659
1853
|
}
|
|
1660
1854
|
// A process signature protects against PID reuse. Never treat a stale timer alone
|
|
@@ -1767,7 +1961,7 @@ async function handleHookState({ input, provider, root = rootDirectory(), now =
|
|
|
1767
1961
|
const currentLink = await providerLinkState(root, provider);
|
|
1768
1962
|
if (!currentLink.connected || currentLink.epoch !== link.epoch) return old;
|
|
1769
1963
|
if (old && old.cwd !== cwd) return old;
|
|
1770
|
-
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 });
|
|
1771
1965
|
const stopMarker = readState(stopMarkerPath(root, provider, input.session_id));
|
|
1772
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.'; }
|
|
1773
1967
|
// Inherited provider identity links an AI-launched conversation to its
|
|
@@ -1828,9 +2022,28 @@ async function handleHookState({ input, provider, root = rootDirectory(), now =
|
|
|
1828
2022
|
if (identity) { record.hookStartedAt = now; record.hookSessionProcess = { ...identity }; }
|
|
1829
2023
|
else { delete record.hookStartedAt; delete record.hookSessionProcess; delete record.process; }
|
|
1830
2024
|
const approved = record.run?.managed?.approvedAction;
|
|
1831
|
-
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.'}` : '') } };
|
|
1832
2027
|
}
|
|
1833
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
|
+
}
|
|
1834
2047
|
const run = record.run;
|
|
1835
2048
|
if(run && event==='Stop') {
|
|
1836
2049
|
// Recheck the actual retained task under its record lock. Lifecycle
|
|
@@ -1867,7 +2080,6 @@ async function handleHookState({ input, provider, root = rootDirectory(), now =
|
|
|
1867
2080
|
}
|
|
1868
2081
|
}
|
|
1869
2082
|
if (!run || TERMINAL.has(run.state)) return record;
|
|
1870
|
-
if (event === 'UserPromptSubmit') output = { hookSpecificOutput: { hookEventName: event, additionalContext: deliveryAcceptanceContext(run) } };
|
|
1871
2083
|
if (event === 'Interrupt') { run.state = 'stopped'; run.reason = 'User interrupted the provider.'; }
|
|
1872
2084
|
else if (run.state === 'auto_resuming' && ['Stop', 'SessionEnd', 'StopFailure'].includes(event)) {
|
|
1873
2085
|
// The supervisor owns this exact attempt and consumes its structured
|
|
@@ -1908,7 +2120,13 @@ async function handleHookState({ input, provider, root = rootDirectory(), now =
|
|
|
1908
2120
|
run.checkpoint = null; run.consumedRevision = run.revision;
|
|
1909
2121
|
run.managed.awaitingReplanHook = true;
|
|
1910
2122
|
}
|
|
1911
|
-
|
|
2123
|
+
let decision;
|
|
2124
|
+
await continueResultCycle(record, resultEvent(record, { kind: 'stop', origin: 'Stop',
|
|
2125
|
+
sourceId: key || `stop:${now}:${run.revision}`, now }), {
|
|
2126
|
+
selectNext: () => { decision = decideStop(run, now); }, now });
|
|
2127
|
+
// Existing permission/wait/ownership gates remain authoritative even when
|
|
2128
|
+
// the shared cycle correctly declines to select another action.
|
|
2129
|
+
decision ||= decideStop(run, now);
|
|
1912
2130
|
const selected = decision.run.managed?.decision;
|
|
1913
2131
|
if (decision.output.decision === 'block' && provider === 'claude' && owner && run.managed.capability === 'live_hook' && ['local_edit', 'local_test'].includes(selected?.action.kind)) {
|
|
1914
2132
|
const unresolved = (await listRecords(root)).find(item => sharesTaskWorkspace(item, record) && item.run?.managed?.executionUncertain);
|
|
@@ -1942,6 +2160,7 @@ async function handleHookState({ input, provider, root = rootDirectory(), now =
|
|
|
1942
2160
|
decision.run.managed.liveWorkspaceLease = { ownerId: claimedWorkspace.owner.id, cwds: claimedWorkspace.owner.cwds, decisionId: selected.id, attemptId };
|
|
1943
2161
|
decision.run.managed.executionAttempt = { id: attemptId, decisionId: selected.id, phase: 'dispatching', dispatchedAt: now, lastActivityAt: now };
|
|
1944
2162
|
}
|
|
2163
|
+
decision.run.resultCycle = run.resultCycle;
|
|
1945
2164
|
record.run = decision.run; output = decision.output;
|
|
1946
2165
|
if (record.runtime && output.decision === 'block') record.runtime.isRunning = true;
|
|
1947
2166
|
record.run.lastStopKey = key;
|
|
@@ -1952,6 +2171,7 @@ async function handleHookState({ input, provider, root = rootDirectory(), now =
|
|
|
1952
2171
|
});
|
|
1953
2172
|
try {
|
|
1954
2173
|
if (event === 'SessionStart') observed = await withSessionParentLock(root, applyObservation, inspectProcess);
|
|
2174
|
+
else if (event === 'UserPromptSubmit') observed = await withAutomaticLock(root, provider, cwd, applyObservation);
|
|
1955
2175
|
else observed = await applyObservation();
|
|
1956
2176
|
} catch (error) {
|
|
1957
2177
|
if (claimedWorkspace?.release) await claimedWorkspace.release();
|
|
@@ -2218,7 +2438,7 @@ export async function controlDelivery({ root = rootDirectory(), provider, sessio
|
|
|
2218
2438
|
const selectedKind = run.managed?.decision?.action?.kind;
|
|
2219
2439
|
const approvedWait = run.managed?.decision?.outcome === 'wait' && run.managed.approvedAction?.id === run.managed.decision.id && !run.managed.approvedAction.dispatchedAt;
|
|
2220
2440
|
const queuedAppServer = run.managed?.resumeChannel === 'codex_app_server' &&
|
|
2221
|
-
['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) &&
|
|
2222
2442
|
(run.managed.decision.outcome !== 'wait' || approvedWait);
|
|
2223
2443
|
if (queuedAppServer) run.managed.autoRecovery = true;
|
|
2224
2444
|
run.state = queuedAppServer ? 'recovery_required' : 'active';
|
|
@@ -2245,6 +2465,10 @@ export async function controlDelivery({ root = rootDirectory(), provider, sessio
|
|
|
2245
2465
|
rememberRejectedAction(run.managed, pending);
|
|
2246
2466
|
}
|
|
2247
2467
|
} else throw new Error('Unsupported control.');
|
|
2468
|
+
if (['approve', 'resume', 'session_answer'].includes(action)) {
|
|
2469
|
+
await continueResultCycle(record, resultEvent(record, { kind: 'condition_changed', origin: 'task_control',
|
|
2470
|
+
sourceId: `control:${action}:${run.revision}:${now}`, now }));
|
|
2471
|
+
}
|
|
2248
2472
|
run.updatedAt = now; notifyState(run, now); return commitControl();
|
|
2249
2473
|
});
|
|
2250
2474
|
}
|