@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.
Files changed (121) hide show
  1. package/README.md +225 -98
  2. package/package.json +5 -3
  3. package/src/agent.js +129 -7
  4. package/src/backend-executable.js +7 -6
  5. package/src/bounded-local-task-subtask-pool.js +32 -6
  6. package/src/bounded-local-task-subtasks-contract.js +1 -1
  7. package/src/cli-contract.js +1 -1
  8. package/src/codex-plugins/simy/.codex-plugin/plugin.json +2 -2
  9. package/src/codex-plugins/simy/hooks/README.md +8 -3
  10. package/src/codex-plugins/simy/hooks/delivery-loop.mjs +302 -78
  11. package/src/codex-plugins/simy/hooks/hooks.json +1 -1
  12. package/src/codex-plugins/simy/hooks/loop-kernel/conformance.json +136 -0
  13. package/src/codex-plugins/simy/hooks/loop-kernel/index.mjs +42 -0
  14. package/src/codex-plugins/simy/hooks/loop-kernel/outcome-review.mjs +31 -0
  15. package/src/codex-plugins/simy/hooks/loop-kernel/pin.json +6 -0
  16. package/src/codex-plugins/simy/hooks/loop-kernel/policy.json +141 -0
  17. package/src/codex-plugins/simy/hooks/loop-kernel/task-pool.mjs +34 -0
  18. package/src/codex-plugins/simy/hooks/loop-kernel/work-frontier.mjs +132 -0
  19. package/src/codex-plugins/simy/hooks/loop-kernel.mjs +6 -0
  20. package/src/codex-plugins/simy/hooks/outcome-progress.mjs +16 -5
  21. package/src/codex-plugins/simy/hooks/purpose-completion-contract.mjs +10 -0
  22. package/src/codex-plugins/simy/hooks/purpose-cycle.mjs +12 -11
  23. package/src/codex-plugins/simy/hooks/purpose-evidence.mjs +28 -38
  24. package/src/codex-plugins/simy/hooks/purpose-rule-policy.mjs +1 -2
  25. package/src/codex-plugins/simy/hooks/purpose-rule-review.mjs +5 -63
  26. package/src/codex-plugins/simy/hooks/purpose-search.mjs +21 -4
  27. package/src/codex-plugins/simy/hooks/result-cycle.mjs +166 -0
  28. package/src/codex-plugins/simy/hooks/scene-reasoning.mjs +98 -12
  29. package/src/codex-plugins/simy/hooks/user-experience.mjs +26 -0
  30. package/src/codex-plugins/simy/hooks/work-frontier.mjs +2 -0
  31. package/src/codex-plugins/simy/skills/diagnose/SKILL.md +10 -4
  32. package/src/coding-run-recovery.js +4 -2
  33. package/src/delivery-hooks/claude-workflow.md +10 -7
  34. package/src/delivery-hooks/command.js +76 -10
  35. package/src/delivery-hooks/consistency-monitor.js +20 -4
  36. package/src/delivery-hooks/consult.js +60 -0
  37. package/src/delivery-hooks/diagnosis.js +2 -2
  38. package/src/delivery-hooks/legacy-hook-routing.js +46 -8
  39. package/src/delivery-hooks/local-runtime.js +81 -1
  40. package/src/delivery-hooks/local-supervisor.js +13 -4
  41. package/src/delivery-hooks/repair-continuation.js +80 -0
  42. package/src/delivery-hooks/repair.js +164 -0
  43. package/src/delivery-hooks/supervisor-handover.js +84 -0
  44. package/src/delivery-hooks/supervisor-owner.js +3 -2
  45. package/src/delivery-hooks/supervisor-service.js +38 -6
  46. package/src/delivery-hooks/supervisor-watchdog.js +15 -4
  47. package/src/delivery-hooks/supervisor.js +151 -18
  48. package/src/desktop-access.js +2 -1
  49. package/src/desktop-host.js +35 -2
  50. package/src/durable-local-task-worker.js +372 -58
  51. package/src/execution-capability-contract.js +15 -1
  52. package/src/index.js +3 -3
  53. package/src/local-photos-read.swift +33 -0
  54. package/src/local-photos.js +47 -0
  55. package/src/local-task-attachment-store.js +31 -0
  56. package/src/local-task-destination.js +174 -0
  57. package/src/local-task.js +119 -7
  58. package/src/native-delegation-contract.js +68 -0
  59. package/src/native-provider-identity.js +60 -0
  60. package/src/orchestrator/audit.js +24 -323
  61. package/src/orchestrator/budget.js +22 -11
  62. package/src/orchestrator/charter-validation.js +3 -41
  63. package/src/orchestrator/checklist-continuation.js +2 -14
  64. package/src/orchestrator/completion-contract.js +4 -49
  65. package/src/orchestrator/contract.js +5 -9
  66. package/src/orchestrator/delivery/WORKFLOW.md +30 -710
  67. package/src/orchestrator/delivery/references/acceptance-evidence.md +4 -216
  68. package/src/orchestrator/delivery/references/change-boundary-matrix.md +4 -103
  69. package/src/orchestrator/delivery/references/chrome-user-verification.md +5 -133
  70. package/src/orchestrator/delivery/references/cross-repository-control.md +4 -74
  71. package/src/orchestrator/delivery/references/delivery-levels.md +3 -117
  72. package/src/orchestrator/delivery/references/local-sqm-evidence.md +1 -1
  73. package/src/orchestrator/delivery/references/minimum-completion.md +4 -144
  74. package/src/orchestrator/delivery/references/pr-closure.md +4 -200
  75. package/src/orchestrator/delivery/references/shared-system-specifications.md +4 -161
  76. package/src/orchestrator/delivery/references/state-and-receipts.md +4 -245
  77. package/src/orchestrator/delivery/references/state-model.md +4 -185
  78. package/src/orchestrator/delivery/references/ux-specification.md +4 -95
  79. package/src/orchestrator/delivery-level.js +2 -10
  80. package/src/orchestrator/evidence.js +2 -3
  81. package/src/orchestrator/independent-audit.js +15 -164
  82. package/src/orchestrator/instruction.js +19 -254
  83. package/src/orchestrator/loop.js +35 -95
  84. package/src/orchestrator/outcome-monitor.js +21 -0
  85. package/src/orchestrator/pr-closure.js +2 -16
  86. package/src/orchestrator/recovery.js +32 -2
  87. package/src/orchestrator/ux-specification.js +3 -12
  88. package/src/provider-delegation.js +336 -0
  89. package/src/runner.js +5 -2
  90. package/src/session-supervision/acceptance-reads.js +34 -0
  91. package/src/session-supervision/artifact-observer.js +138 -0
  92. package/src/session-supervision/browser-cdp.js +59 -0
  93. package/src/session-supervision/browser-recovery.js +97 -0
  94. package/src/session-supervision/codex-app-server.js +61 -22
  95. package/src/session-supervision/completion-artifacts.js +44 -0
  96. package/src/session-supervision/consistency-auditor.js +8 -11
  97. package/src/session-supervision/continuation-analysis.js +47 -28
  98. package/src/session-supervision/final-response-recovery.js +5 -0
  99. package/src/session-supervision/index.js +22 -5
  100. package/src/session-supervision/interaction-proxy.js +250 -0
  101. package/src/session-supervision/interaction-task.js +189 -0
  102. package/src/session-supervision/native-interaction.js +102 -0
  103. package/src/session-supervision/provider-failure.js +40 -0
  104. package/src/session-supervision/purpose-instruction.js +14 -14
  105. package/src/session-supervision/purpose-recovery.js +32 -12
  106. package/src/session-supervision/purpose-verification.js +50 -216
  107. package/src/session-supervision/recovery-frontier.js +55 -0
  108. package/src/session-supervision/stop-notifications.js +8 -8
  109. package/src/sqm/agent-skill.js +1 -1
  110. package/src/sqm/checkers.js +12 -6
  111. package/src/sqm/claude-plugin-update.js +22 -8
  112. package/src/sqm/claude-plugin.js +6 -0
  113. package/src/sqm/local-tests.js +2 -1
  114. package/src/sqm/plugin-cache.js +67 -0
  115. package/src/sqm/proof.js +25 -0
  116. package/src/sqm/report.js +1 -0
  117. package/src/sqm/workflow-onboarding.js +3 -39
  118. package/src/sqm/workflow-update.js +17 -6
  119. package/src/startup-preflight.js +2 -20
  120. package/src/team-status-fence-store.js +32 -0
  121. 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
- await rename(temporary, file);
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.' : 'Include evidence:["concrete observation"] in actionResult.';
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. missing_information requires a specific question. external_dependency requires waitCondition:{condition,owner,nextCheck} with the observable resumption condition, dependency owner, and bounded recheck instruction, plus nextStep.`;
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
- const basis = Array.isArray(value.basis) && value.basis.length <= 6 ? value.basis.map(item => {
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
- if (searchEnabled(managed) && decision.outcome === 'wait') {
591
- rememberSearchWait(managed, 'authorization', { action: decision.action, reason: decision.reason }, now);
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
- if (ledger.units.length > 128) { ledger.unitsOmitted = true; ledger.units = ledger.units.filter(row => ['waiting','planned','unknown'].includes(row.status)).concat(ledger.units.filter(row => !['waiting','planned','unknown'].includes(row.status)).slice(-64)); }
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' || !text(value.selectionRef, 200)) throw new Error('Delivery level needs the user selection reference, requested/effective levels 1-5, and assurance-ascending-v2 numbering.');
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
- export async function startDelivery({ root = rootDirectory(), provider, session, cwd = process.cwd(), executionCwd, upgradeRunId, objective, nextStep, maxMinutes, maxContinuations, maxFailures, continuationPolicy, managed, admission,
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
- if (upgrading && (!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.');
779
- if (previous.run && !upgrading) throw new Error('This session already has a Delivery Loop. Resume or stop it; budgets cannot be reset by starting again.');
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 = { id: randomUUID(), objective: text(objective), state: waiting ? 'blocked' : queued ? 'recovery_required' : 'active', nextStep: text(nextStep), startedAt: now, updatedAt: now,
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 (['in_progress', 'error'].includes(checkpoint.status) && !text(checkpoint.nextStep)) throw new Error('A concrete nextStep is required.');
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
- if (result.status === 'observed' && !actionResult.evidence.length && !flexibleReasoning(run.managed)) throw new Error('Observed action result needs concrete evidence.');
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
- if (!searchEnabled(run.managed) && checkpoint.status === 'completed' && review.rules.some(rule => ['failed', 'unverified'].includes(rule.status))) {
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 (run.managed.autonomyPolicy && checkpoint.status === 'blocked' && checkpoint.blocker === 'missing_information') {
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, blocker: checkpoint.blocker, evidence, acceptanceMet: checkpoint.acceptanceMet === true };
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
- if (searchEnabled(run.managed) && effectiveStatus === 'completed') {
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
- return refs.every(link=>observations.some(item=>item.id===link.ref && item.status==='observed' && currentPurposeObservation(item,now,review.observationWindow)));
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
- queuePurposeVerification(run, `${review.controllerEvidenceFailure.instruction} Invalid targets: ${(review.controllerEvidenceFailure.targets || []).join(', ')}. Use a narrow direct read containing the cited excerpt within 2000 characters.`, now);
1160
- managed.nextAttemptAt = now + Math.min(60_000, 1000 * 2 ** Math.min(state.verifierFailures.attempts - 1, 6));
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.evidenceRefs?.length ||
1168
- unit.criterionIds.some(id=>!review.criteria.some(row=>row.id===id && row.passed))) throw new Error('Required unit resolution needs independent criterion-linked evidence.');
1169
- if (unit.purposeRevision !== evidenceLedger(managed).purposeRevision) throw new Error('Unit belongs to an old purpose revision.');
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
- recordDecision(managed, purposeDiagnosisPlan(managed, reason), now);
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
- if (cp?.status === 'completed') {
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()}\nCheck the actual delivery status first. Record an honest checkpoint with simy delivery checkpoint before stopping; do not claim completion without verification. Pending step: ${next.nextStep}`;
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: 'SessionStart', additionalContext: `SIMY hooks observed this ${provider} session: ${input.session_id}. Only if the user requests SIMY Delivery Loop, use simy delivery start --provider ${provider} --session ${input.session_id} with the agreed objective and next step. Existing loop: ${record.run?.state || 'not started'}.${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.'}` : ''} Never enable, trust, resume, or reset a loop on the user's behalf without their request.` } };
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
- const decision = decideStop(run, now);
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
  }