@awak-app/simy-cli 0.6.2 → 0.6.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -178,7 +178,12 @@ marketplace still points to SIMY’s generated local source. Plugin versions inc
178
178
  receive a fresh cache. Use a new Codex task to pick up updated skills; running
179
179
  tasks are not restarted. Missing or disabled plugins are not enabled automatically.
180
180
  Failed refreshes retry on the next check without blocking daemon startup.
181
- The desktop Sessions page discovers existing provider conversations and lets users
181
+ The desktop Sessions page loads a recent window of up to 200 conversations,
182
+ ranked by last activity. Explicitly tracked, monitored, auto-continue and unsettled
183
+ supervision records remain visible even outside that window. Claude discovery
184
+ ranks file metadata before opening selected transcripts. A conflicting saved
185
+ workspace leaves only that conversation's Action association unavailable; it
186
+ does not stop discovery or change the saved identity. The page lets users
182
187
  choose monitoring or supported session supervision with a goal, acceptance
183
188
  criteria and an autonomy policy. Local time, token and continuation counters do not stop work. Codex enrollment uses an exact
184
189
  app-server session and checks writer ownership before each continuation. Claude
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@awak-app/simy-cli",
3
- "version": "0.6.2",
3
+ "version": "0.6.5",
4
4
  "description": "Local SIMY executor for non-coding Local Tasks and Agentic Loop runs.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -38,12 +38,12 @@
38
38
  "test:e2e:console:real-provider": "node ./scripts/real-provider-console-e2e.js",
39
39
  "test:e2e:console:fullstack": "node ./scripts/fullstack-cli-smoke.js",
40
40
  "check": "npm run check:auto-update && node --check ./src/desktop-executor.js && node --check ./src/local-task.js && node --check ./scripts/desktop-executor-e2e.js && node --check ./scripts/pre-guardrail-e2e.js && node --check ./scripts/fixtures/fake-desktop-provider.js && npm run check:syntax && npm run check:session-supervision-syntax && npm run check:real-provider-syntax && npm test && npm run check:package",
41
- "check:session-supervision-syntax": "node --check ./src/session-supervision/index.js && node --check ./src/session-supervision/codex-app-server.js && node --check ./src/session-supervision/claude-sessions.js && node --check ./src/session-supervision/original-session-target.js && node --check ./src/session-supervision/action-association.js && node --check ./src/session-supervision/action-association-host.js && node --check ./src/session-supervision/action-association-api.js && node --check ./src/session-supervision/association-store.js && node --check ./src/session-supervision/action-deadline.js && node --check ./src/session-supervision/action-deadline-store.js && node --check ./src/codex-plugins/simy/hooks/action-association.mjs",
41
+ "check:session-supervision-syntax": "node --check ./src/session-supervision/index.js && node --check ./src/session-supervision/discovery-window.js && node --check ./src/session-supervision/codex-app-server.js && node --check ./src/session-supervision/claude-sessions.js && node --check ./src/session-supervision/original-session-target.js && node --check ./src/session-supervision/action-association.js && node --check ./src/session-supervision/action-association-host.js && node --check ./src/session-supervision/action-association-api.js && node --check ./src/session-supervision/association-store.js && node --check ./src/session-supervision/action-deadline.js && node --check ./src/session-supervision/action-deadline-store.js && node --check ./src/codex-plugins/simy/hooks/action-association.mjs",
42
42
  "check:auto-update": "node --check ./src/auto-update.js && node --check ./scripts/auto-update-e2e.js && npm run check:sqm",
43
43
  "check:workflow-observation-sender": "node --check ./src/workflow-observation-sender.js",
44
44
  "check:sqm": "node --check ./src/sqm/canonical.js && node --check ./src/sqm/validation.js && node --check ./src/sqm/reliability.js && node --check ./src/sqm/repository.js && node --check ./src/sqm/checkers.js && node --check ./src/sqm/bundle-store.js && node --check ./src/sqm/proof.js && node --check ./src/sqm/evidence-client.js && node --check ./src/sqm/session.js && node --check ./src/sqm/index.js && node --check ./src/sqm/command.js && node --check ./src/sqm/agent-skill.js && node --check ./src/sqm/workflow-onboarding.js && node --check ./src/sqm/workflow-update.js && node --check ./src/sqm/history-store.js && node --check ./src/sqm/review-worker.js && node --check ./scripts/sqm-complete-loop-e2e.js && node --check ./scripts/sqm-local-review-e2e.js",
45
45
  "check:real-provider-syntax": "node --check ./scripts/real-codex-probe.js && node --check ./scripts/real-claude-probe.js && node --check ./scripts/real-provider-console-fixture.js && node --check ./scripts/real-provider-console-e2e.js && node --check ./scripts/claude-parity-runtime-e2e.js && node --check ./scripts/claude-plugin-runtime-e2e.js",
46
- "check:syntax": "node --check ./src/purpose-local-proof.js && node --check ./src/device-signing-identity.js && node --check ./src/index.js && node --check ./src/install-update.js && node --check ./src/agent.js && node --check ./src/connection-discovery.js && node --check ./src/codex-plugins/simy/hooks/connection-reminder.mjs && node --check ./src/codex-plugins/simy/hooks/dev-work.mjs && node --check ./src/dev-work-events.js && node --check ./src/browser.js && node --check ./src/web-api.js && node --check ./src/durable-local-task-worker.js && node --check ./src/durable-local-task-steps-contract.js && node --check ./src/bounded-local-task-subtasks-contract.js && node --check ./src/bounded-local-task-subtask-pool.js && node --check ./src/local-task-artifact-contract.js && node --check ./src/execution-capability-contract.js && node --check ./src/shutdown.js && node --check ./src/run-registry.js && node --check ./src/direct-executor.js && node --check ./src/execution-guardrail.js && node --check ./src/backend-executable.js && node --check ./src/login-shell-path.js && node --check ./src/hook-runtime.js && node --check ./src/provider-stream.js && node --check ./src/console/index.js && node --check ./src/console/app.js && node --check ./src/console/commands.js && node --check ./src/local-attachments.js && node --check ./src/repository-inventory.js && node --check ./src/session-store.js && node --check ./src/web-origin.js && node --check ./src/workspace-context.js && node --check ./src/orchestrator/index.js && node --check ./src/orchestrator/shared.js && node --check ./src/orchestrator/recovery.js && node --check ./src/orchestrator/risk.js && node --check ./src/orchestrator/contract.js && node --check ./src/orchestrator/completion-contract.js && node --check ./src/orchestrator/pr-preparation.js && node --check ./src/orchestrator/minimum-completion.js && node --check ./src/orchestrator/delivery/scripts/check-minimum-completion.mjs && node --check ./src/orchestrator/instruction.js && node --check ./src/orchestrator/problem-solving.js && node --check ./src/orchestrator/budget.js && node --check ./src/orchestrator/execution-io.js && node --check ./src/orchestrator/presentation.js && node --check ./src/orchestrator/result.js && node --check ./src/orchestrator/retry.js && node --check ./src/orchestrator/evidence.js && node --check ./src/orchestrator/independent-audit.js && node --check ./src/orchestrator/audit.js && node --check ./src/orchestrator/loop.js && node --check ./src/runner.js && node --check ./src/coding-run-store.js && node --check ./scripts/fixtures/completion-contract-fixture.js && node --check ./scripts/fixtures/fake-coding-backend.js && node --check ./scripts/e2e-terminal-evidence.js && node --check ./scripts/audit-gate-e2e.js && node --check ./scripts/completion-contract-e2e.js && node --check ./scripts/retry-circuit-e2e.js && node --check ./scripts/follow-up-charter-e2e.js && node --check ./scripts/task-routing-e2e.js && node --check ./scripts/executor-version-preflight-e2e.js && node --check ./scripts/blocked-recovery-e2e.js && node --check ./scripts/process-localization-e2e.js && node --check ./scripts/hypothesis-retries-e2e.js && node --check ./scripts/budget-enforcement-e2e.js && node --check ./scripts/local-task-default-workspace-e2e.js && node --check ./scripts/local-task-idempotency-e2e.js && node --check ./scripts/console-e2e-fixture.js && node --check ./scripts/console-e2e.js && node --check ./scripts/console-recovery-e2e-fixture.js && node --check ./scripts/console-recovery-e2e.js && node --check ./scripts/real-provider-console-fixture.js && node --check ./scripts/real-provider-console-e2e.js && node --check ./scripts/fullstack-cli-smoke.js && node --check ./scripts/check-package-contents.js && node --check ./src/codex-plugins/simy/hooks/delivery-loop.mjs && node --check ./src/delivery-hooks/command.js && node --check ./src/delivery-hooks/desktop.js && node --check ./src/delivery-hooks/supervisor.js && node --check ./src/session-supervision/automatic-continuation.js",
46
+ "check:syntax": "node --check ./src/purpose-local-proof.js && node --check ./src/device-signing-identity.js && node --check ./src/index.js && node --check ./src/install-update.js && node --check ./src/agent.js && node --check ./src/connection-discovery.js && node --check ./src/codex-plugins/simy/hooks/connection-reminder.mjs && node --check ./src/codex-plugins/simy/hooks/dev-work.mjs && node --check ./src/dev-work-events.js && node --check ./src/browser.js && node --check ./src/web-api.js && node --check ./src/durable-local-task-worker.js && node --check ./src/durable-local-task-steps-contract.js && node --check ./src/bounded-local-task-subtasks-contract.js && node --check ./src/bounded-local-task-subtask-pool.js && node --check ./src/local-task-artifact-contract.js && node --check ./src/execution-capability-contract.js && node --check ./src/shutdown.js && node --check ./src/run-registry.js && node --check ./src/direct-executor.js && node --check ./src/execution-guardrail.js && node --check ./src/backend-executable.js && node --check ./src/login-shell-path.js && node --check ./src/hook-runtime.js && node --check ./src/provider-stream.js && node --check ./src/console/index.js && node --check ./src/console/app.js && node --check ./src/console/commands.js && node --check ./src/local-attachments.js && node --check ./src/repository-inventory.js && node --check ./src/session-store.js && node --check ./src/web-origin.js && node --check ./src/workspace-context.js && node --check ./src/orchestrator/index.js && node --check ./src/orchestrator/shared.js && node --check ./src/orchestrator/recovery.js && node --check ./src/orchestrator/risk.js && node --check ./src/orchestrator/contract.js && node --check ./src/orchestrator/completion-contract.js && node --check ./src/orchestrator/pr-preparation.js && node --check ./src/orchestrator/minimum-completion.js && node --check ./src/orchestrator/delivery/scripts/check-minimum-completion.mjs && node --check ./src/orchestrator/instruction.js && node --check ./src/orchestrator/problem-solving.js && node --check ./src/orchestrator/budget.js && node --check ./src/orchestrator/execution-io.js && node --check ./src/orchestrator/presentation.js && node --check ./src/orchestrator/result.js && node --check ./src/orchestrator/retry.js && node --check ./src/orchestrator/evidence.js && node --check ./src/orchestrator/independent-audit.js && node --check ./src/orchestrator/audit.js && node --check ./src/orchestrator/loop.js && node --check ./src/runner.js && node --check ./src/coding-run-store.js && node --check ./scripts/fixtures/completion-contract-fixture.js && node --check ./scripts/fixtures/fake-coding-backend.js && node --check ./scripts/e2e-terminal-evidence.js && node --check ./scripts/audit-gate-e2e.js && node --check ./scripts/completion-contract-e2e.js && node --check ./scripts/retry-circuit-e2e.js && node --check ./scripts/follow-up-charter-e2e.js && node --check ./scripts/task-routing-e2e.js && node --check ./scripts/executor-version-preflight-e2e.js && node --check ./scripts/blocked-recovery-e2e.js && node --check ./scripts/process-localization-e2e.js && node --check ./scripts/hypothesis-retries-e2e.js && node --check ./scripts/budget-enforcement-e2e.js && node --check ./scripts/local-task-default-workspace-e2e.js && node --check ./scripts/local-task-idempotency-e2e.js && node --check ./scripts/console-e2e-fixture.js && node --check ./scripts/console-e2e.js && node --check ./scripts/console-recovery-e2e-fixture.js && node --check ./scripts/console-recovery-e2e.js && node --check ./scripts/real-provider-console-fixture.js && node --check ./scripts/real-provider-console-e2e.js && node --check ./scripts/fullstack-cli-smoke.js && node --check ./scripts/check-package-contents.js && node --check ./src/codex-plugins/simy/hooks/delivery-loop.mjs && node --check ./src/delivery-hooks/command.js && node --check ./src/delivery-hooks/desktop.js && node --check ./src/delivery-hooks/supervisor.js && node --check ./src/session-supervision/automatic-continuation.js && node --check ./src/session-supervision/stop-notifications.js",
47
47
  "check:package": "node ./scripts/check-package-contents.js",
48
48
  "build:macos-installer:dev": "bash packaging/build-macos-arm64.sh --unsigned-dev",
49
49
  "build:macos-installer": "bash packaging/build-macos-arm64.sh --production",
@@ -13,7 +13,7 @@ import {
13
13
  } from "./durable-local-task-steps-contract.js";
14
14
  import { buildExecutionCapabilityContract } from "./execution-capability-contract.js";
15
15
 
16
- export const CLI_VERSION = "0.6.2";
16
+ export const CLI_VERSION = "0.6.5";
17
17
  export const CLI_API_CONTRACT_VERSION = 10;
18
18
 
19
19
  export function withCliContract(
@@ -22,7 +22,7 @@ scans ports or reads stored session tokens.
22
22
  Claude distributions. SessionStart supplies the exact session identity. It does
23
23
  not start a loop. After an explicit `simy delivery start`, Stop checks that
24
24
  session's checkpoint and returns bounded next-step instructions. Approval/input
25
- blocks, interruption, completion, failures and budgets stop automatic continuation.
25
+ blocks, interruption, completion, failures and missing progress stop automatic continuation.
26
26
  Provider errors/exits are surfaced to SIMY's desktop; a dead provider is not
27
27
  silently restarted. No hook writes trust, permission settings, or SQM evidence.
28
28
 
@@ -31,27 +31,21 @@ keyed by provider and session ID and bound to the canonical working directory.
31
31
  Historical hook observations do not prove hooks are still enabled. See the
32
32
  bundled delivery-loop skill for checkpoint commands and explicit recovery.
33
33
 
34
- Managed tasks may explicitly select `--continuation-policy progress`. Existing
35
- tasks without that choice keep their bounded contract. Progress mode treats the
36
- ordinary time, continuation and token thresholds as warnings, with independent
37
- hard ceilings of 24 hours from the original start, 50 continuations and, on the
38
- host-observed Codex channel, 15 million provider tokens. Claude live hooks expose
39
- no trustworthy usage stream; their status explicitly reports that token limits
40
- are unsupported. Required approvals, missing observed usage, manual stops and
41
- unreconciled effects continue to block dispatch.
42
-
43
- The host records passing, failed and unevaluated frozen acceptance checks.
44
- Provider summaries, test counts, renamed evidence and restored old criteria
45
- cannot manufacture progress. Three attempts without new verified criteria, or
46
- 30 minutes without verified progress at a dispatch/checkpoint boundary, pause
47
- the original run. Active tools are allowed to reach their checkpoint within the
48
- hard ceiling; activity alone does not prove progress.
49
-
50
- `delivery extend` raises explicitly selected ceilings within those global caps.
51
- It records the old and new bounds, preserves run identity, usage, checkpoints,
52
- criteria and approvals, and does not start a new provider turn. A separate resume
53
- is required after an exhausted run becomes paused. `delivery continuation_policy`
54
- requires an explicit choice and never infers progress consent from an extension.
34
+ SIMY does not limit a task's elapsed time, number of turns or provider tokens.
35
+ A task stops after its failure limit (three by default). Missing progress is
36
+ evaluated after three steps without a newly verified acceptance criterion, or
37
+ 30 minutes without verified progress at a dispatch/checkpoint boundary. The
38
+ policy-specific pause behavior is described below. Provider summaries, test counts, renamed evidence and restored old
39
+ criteria cannot manufacture progress, and activity alone does not prove it.
40
+ Required approvals, manual stops and unreconciled effects still block dispatch.
41
+
42
+ Context-first tasks without host-checkable acceptance pause rather than fail at
43
+ that limit, so Auto-continue stays available after review. An explicit resume,
44
+ answer or approval resets both no-progress attempt counters and the progress clock,
45
+ even before a threshold was reached. Failure counts and verified criterion history
46
+ remain; reusing old evidence cannot count as fresh progress.
47
+ `delivery continuation_policy` switches between the `bounded` and `progress`
48
+ policies only after an explicit choice; `progress` also pauses rather than fails.
55
49
 
56
50
  ## Project 実装状況 (dev-work) hook
57
51
 
@@ -8,8 +8,8 @@ import { execFile } from 'node:child_process';
8
8
  import { promisify } from 'node:util';
9
9
  import { acquireWorkspaceLease, releaseWorkspaceLease } from './workspace-lease.mjs';
10
10
  import { associationContract, validateAssociationResult } from './action-association.mjs';
11
- import { continuationMode, MAX_COMPLETION_TOKENS } from './continuation-policy.mjs';
12
- import { evaluateOutcomeProgress } from './outcome-progress.mjs';
11
+ import { continuationMode } from './continuation-policy.mjs';
12
+ import { evaluateOutcomeProgress, restartOutcomeProgressWindow } from './outcome-progress.mjs';
13
13
 
14
14
  export const TERMINAL = new Set(['completed', 'reported_done', 'stopped', 'failed', 'budget_exhausted']);
15
15
  const EVENTS = new Set(['SessionStart', 'UserPromptSubmit', 'Stop', 'StopFailure', 'SessionEnd', 'Interrupt', 'PermissionRequest', 'Notification']);
@@ -374,7 +374,7 @@ function escalate(run, cause) {
374
374
  managed.escalation = { cause, tried: history.map(item => ({ action: item.action?.summary, result: item.status, reason: item.summary })), results: history.map(item => ({ action: item.action?.summary, result: item.status, reason: item.verification })), remainingWork: text(managed.remainingWork || run.nextStep, 1000), userQuestion: managed.userQuestion || managed.plan?.userQuestion || 'Which action should be authorized or revised?', options: (managed.plan?.alternatives || []).map(option => ({ id: option.id, action: { kind: option.action.kind, scope: option.action.scope, summary: text(option.action.summary, 200) } })), recommendation: managed.plan?.recommendation || null };
375
375
  }
376
376
  export function notifyState(run, now) {
377
- if (!run?.managed || !['blocked', 'verification_pending', 'recovery_required', 'failed', 'budget_exhausted', 'completed', 'reported_done'].includes(run.state)) return;
377
+ if (!run?.managed || !['blocked', 'verification_pending', 'recovery_required', 'paused', 'failed', 'budget_exhausted', 'completed', 'reported_done'].includes(run.state)) return;
378
378
  const key = `${run.state}:${run.revision}:${run.reason}`;
379
379
  if (run.managed.notificationKey === key) return;
380
380
  run.managed.notificationKey = key;
@@ -420,11 +420,8 @@ function managedSpec(spec, session, provider, observedProcess, admission, now) {
420
420
  initialAction, proposedAction: initialAction, approvalPolicy, approvalScopes: [...new Set(scopes)].slice(0, 20),
421
421
  grants: Object.fromEntries(scopes.map(scope => [scope, { decision: approvalPolicy }])), maxNoProgress: positive(config.maxNoProgress, 3, 20),
422
422
  maxNoProgressMinutes: positive(config.maxNoProgressMinutes, 30, 1440), lastVerifiedProgressAt: now,
423
- maxTokens: config.maxTokens === undefined ? null : positive(config.maxTokens, null, Number.MAX_SAFE_INTEGER), tokensUsed: 0, usageAvailable: null,
424
423
  noProgress: 0, verifiedRevision: 0, consumedVerifiedRevision: 0, notification: null, notificationKey: null,
425
424
  continuationPolicy: continuationMode(config.continuationPolicy),
426
- providerTokenCeiling: appServerChannel ? positive(config.providerTokenCeiling, MAX_COMPLETION_TOKENS, MAX_COMPLETION_TOKENS) : null,
427
- budgetCapability: { time: false, continuations: false, providerTokens: appServerChannel, localExecutionLimits: false },
428
425
  autoRecovery, resumeModel, autoRecoveryUnavailableReason: config.autoRecovery === true && !autoRecovery ? 'Exact Codex process, session UUID, and resumeModel are required.' : null,
429
426
  autoRecoverySandbox: config.autoRecoverySandbox || 'read-only', resumeChannel: config.resumeChannel || null,
430
427
  capability: appServerChannel && autoRecovery ? 'codex_app_server' : autoRecovery ? 'codex_exec_resume' : 'live_hook',
@@ -434,7 +431,7 @@ function managedSpec(spec, session, provider, observedProcess, admission, now) {
434
431
  if (initialPlan) recordDecision(managed, initialPlan, Date.now());
435
432
  return managed;
436
433
  }
437
- export async function startDelivery({ root = rootDirectory(), provider, session, cwd = process.cwd(), objective, nextStep, maxContinuations, maxFailures, maxMinutes, continuationPolicy, hardMaxMinutes, hardMaxContinuations, providerTokenCeiling, managed, admission,
434
+ export async function startDelivery({ root = rootDirectory(), provider, session, cwd = process.cwd(), objective, nextStep, maxFailures, continuationPolicy, managed, admission,
438
435
  liveHookAdmission, autoContinueEpoch, inspectProcess = processIdentity, now = Date.now() }) {
439
436
  cwd = await realpath(cwd);
440
437
  if (!text(objective) || !text(nextStep)) throw new Error('Objective and concrete next step are required.');
@@ -451,8 +448,7 @@ export async function startDelivery({ root = rootDirectory(), provider, session,
451
448
  if ((!previous?.observedAt && !admitted) || previous.cwd !== cwd) throw new Error('No matching verified provider session observed here. Select an exact discovered session or enable SIMY hooks.');
452
449
  if (previous.run) throw new Error('This session already has a Delivery Loop. Resume or stop it; budgets cannot be reset by starting again.');
453
450
  const managedConfig = managed && { ...(managed === true ? {} : managed),
454
- ...(continuationPolicy === undefined ? {} : { continuationPolicy }),
455
- ...(providerTokenCeiling === undefined ? {} : { providerTokenCeiling }) };
451
+ ...(continuationPolicy === undefined ? {} : { continuationPolicy }) };
456
452
  const managedState = managedSpec(managedConfig, session, provider, previous.process, admitted ? admission : null, now);
457
453
  if (!managedState && continuationMode(continuationPolicy) === 'progress') throw new Error('Progress continuation requires frozen managed acceptance criteria.');
458
454
  if (autoContinueEpoch && managedState) managedState.autoContinueEpoch = autoContinueEpoch;
@@ -465,12 +461,7 @@ export async function startDelivery({ root = rootDirectory(), provider, session,
465
461
  const waiting = managedState?.decision?.outcome === 'wait';
466
462
  const queued = managedState?.capability === 'codex_app_server';
467
463
  const run = { id: randomUUID(), objective: text(objective), state: waiting ? 'blocked' : queued ? 'recovery_required' : 'active', nextStep: text(nextStep), startedAt: now, updatedAt: now,
468
- deadline: now + positive(maxMinutes, 30, 1440) * 60_000, maxContinuations: positive(maxContinuations, 6, 50), maxFailures: positive(maxFailures, 3, 10),
469
- ...(managedState?.continuationPolicy === 'progress' ? {
470
- hardDeadline: now + positive(hardMaxMinutes ?? managedConfig.hardMaxMinutes, 1440, 1440) * 60_000,
471
- hardMaxContinuations: positive(hardMaxContinuations ?? managedConfig.hardMaxContinuations, 50, 50),
472
- budgetWarnings: [], budgetExtensionHistory: [],
473
- } : {}),
464
+ maxFailures: positive(maxFailures, 3, 10),
474
465
  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',
475
466
  ...(queued ? { recoveryCause: 'session_supervision' } : {}), managed: managedState };
476
467
  if (waiting) escalate(run, run.reason);
@@ -774,37 +765,54 @@ export function prepareRecoveryDiagnosis(run, now = Date.now()) {
774
765
  run.nextStep = decision.action.summary;
775
766
  return decision;
776
767
  }
777
- export function deliveryBudgetState(run, now = Date.now(), { checkProgress = true } = {}) {
768
+ // Local time, turn and token caps were removed. A managed task stops only after
769
+ // repeated failures or when no progress is verified; both reasons are shown to
770
+ // people, so keep them plain and actionable. Codes drive state transitions.
771
+ export const STOP_CODES = Object.freeze({ failureLimit: 'failure_limit', noProgressTime: 'no_progress_time', noProgressAttempts: 'no_progress_attempts' });
772
+ export function deliveryStop(run, now = Date.now(), { checkProgress = true } = {}) {
778
773
  const managed = run.managed;
779
- const progress = managed?.continuationPolicy === 'progress';
780
- // Historical policies/counters remain immutable, but local elapsed time,
781
- // turn count and token usage no longer limit either new or restored tasks.
782
- const deadline = Infinity;
783
- const maxContinuations = Infinity;
784
- const tokenCeiling = null;
785
- const warnings = [];
786
- const checkpointDetail = run.checkpoint ? '' : ' No progress checkpoint was recorded.';
787
- if (progress) {
788
- if (now >= run.deadline) warnings.push('soft_time_limit');
789
- if (run.continuations >= run.maxContinuations) warnings.push('soft_continuation_limit');
790
- if (managed.maxTokens && managed.tokensUsed >= managed.maxTokens) warnings.push('soft_token_limit');
791
- }
792
- let reason = null;
793
- if (run.failures >= run.maxFailures) {
794
- reason = `Delivery failure limit exhausted (${run.failures}/${run.maxFailures}).${checkpointDetail}`;
795
- } else if (checkProgress && managed && now - (managed.lastVerifiedProgressAt ?? run.startedAt) >= (managed.maxNoProgressMinutes ?? 30) * 60_000) {
796
- reason = 'No verified acceptance progress within the configured time limit.';
797
- } else if (checkProgress && managed && managed.noProgress >= managed.maxNoProgress) {
798
- reason = 'No verified acceptance progress within the configured continuation limit.';
799
- }
800
- return { continuationPolicy: progress ? 'progress' : 'bounded', localLimitsRemoved: true, deadline, maxContinuations, tokenCeiling, warnings, reason };
774
+ if (run.failures >= run.maxFailures) return { code: STOP_CODES.failureLimit,
775
+ reason: `Stopped after ${run.failures} failed attempts. Open the original conversation to see what went wrong.` };
776
+ if (!checkProgress || !managed) return null;
777
+ const minutes = managed.maxNoProgressMinutes ?? 30;
778
+ if (now - (managed.lastVerifiedProgressAt ?? run.startedAt) >= minutes * 60_000) return { code: STOP_CODES.noProgressTime,
779
+ reason: `Paused because no progress was confirmed in ${minutes} minutes. Review the original conversation, then resume.` };
780
+ if (managed.noProgress >= managed.maxNoProgress) return { code: STOP_CODES.noProgressAttempts,
781
+ reason: `Paused after ${managed.noProgress} steps without confirmed progress. Review the original conversation, then resume.` };
782
+ return null;
783
+ }
784
+ // What a provider turn is told about its remaining room to work.
785
+ export function deliveryProgressContext(run) {
786
+ return { continuations: run.continuations, failures: run.failures, maxFailures: run.maxFailures,
787
+ stepsWithoutConfirmedProgress: run.managed?.noProgress ?? 0, maxStepsWithoutConfirmedProgress: run.managed?.maxNoProgress ?? null,
788
+ minutesWithoutConfirmedProgress: run.managed?.maxNoProgressMinutes ?? null };
801
789
  }
802
790
  export function managedBudgetReason(run, now = Date.now(), options) {
803
- return deliveryBudgetState(run, now, options).reason;
791
+ return deliveryStop(run, now, options)?.reason ?? null;
792
+ }
793
+ // Only repeated failures make a task unrecoverable. A missing-progress pause is a
794
+ // review point: an explicit resume starts a fresh progress window.
795
+ export function deliveryStopBlocksResume(run, now = Date.now()) {
796
+ return deliveryStop(run, now)?.code === STOP_CODES.failureLimit;
804
797
  }
805
- export function managedDispatchDeadline(run) {
806
- return deliveryBudgetState(run).deadline;
798
+ // Time spent waiting for a person is not missing work progress. An explicit
799
+ // resume, answer or approval starts a new progress window; failures remain.
800
+ function continueAfterHumanDecision(run, now) {
801
+ const stop = deliveryStop(run, now);
802
+ if (stop?.code === STOP_CODES.failureLimit) throw new Error(stop.reason);
803
+ if (!run.managed) return;
804
+ run.managed.progressWindowResets = [...(run.managed.progressWindowResets || []),
805
+ { at: now, code: stop?.code ?? 'human_decision', noProgress: run.managed.noProgress }].slice(-10);
806
+ run.managed.noProgress = 0;
807
+ run.managed.outcomeProgress = restartOutcomeProgressWindow(run.managed.outcomeProgress);
808
+ run.managed.lastVerifiedProgressAt = now;
807
809
  }
810
+ const stoppedState = stop => stop.code === STOP_CODES.failureLimit ? 'budget_exhausted' : 'paused';
811
+ // Context-first tasks usually have no host-checkable acceptance, so worker
812
+ // reports can never reset noProgress. Reaching that limit pauses for review
813
+ // instead of failing permanently and ending Auto-continue for the conversation.
814
+ const pausesWithoutProgress = managed => managed.continuationPolicy === 'progress' ||
815
+ managed.contextBootstrap === true && !managed.acceptanceCriteria.some(criterion => criterion.check?.type !== 'manual');
808
816
 
809
817
  function recordManagedOutcomeProgress(run, now, assessmentAvailable = true) {
810
818
  const managed = run.managed;
@@ -842,7 +850,6 @@ export function automaticContinuationAllowed(record) {
842
850
  export function decideStop(run, now = Date.now()) {
843
851
  if (TERMINAL.has(run.state) || ['blocked', 'recovery_required', 'paused', 'verification_pending'].includes(run.state)) return { run, output: {} };
844
852
  const next = structuredClone(run);
845
- next.budgetWarnings = deliveryBudgetState(next, now).warnings;
846
853
  const finish = (state, reason) => { next.state = state; next.reason = reason; next.updatedAt = now; return { run: next, output: {} }; };
847
854
  // A fresh, explicitly evidenced completion may finish at the budget boundary.
848
855
  const fresh = next.checkpoint && next.revision > next.consumedRevision;
@@ -859,8 +866,8 @@ export function decideStop(run, now = Date.now()) {
859
866
  next.nextStep = text(cp.nextStep) || next.nextStep;
860
867
  } else { escalate(next, cp.summary); return finish('blocked', cp.summary); }
861
868
  }
862
- const budgetReason = managedBudgetReason(next, now);
863
- if (budgetReason) return finish(budgetReason.startsWith('No verified') ? 'paused' : 'budget_exhausted', budgetReason);
869
+ const stop = deliveryStop(next, now);
870
+ if (stop) return finish(stoppedState(stop), stop.reason);
864
871
  const initialHook = next.managed?.awaitingInitialHook && next.revision === 0 && !next.checkpoint;
865
872
  const replanHook = next.managed?.capability === 'live_hook' && next.managed.awaitingReplanHook === true;
866
873
  const understandingHook = next.managed?.capability === 'live_hook' && (next.managed.awaitingUnderstandingHook === true || next.managed.awaitingAssociationHook === true);
@@ -901,7 +908,11 @@ export function decideStop(run, now = Date.now()) {
901
908
  else if (next.managed.associationCheckpointRevision !== next.revision && !initialHook && !replanHook && !understandingHook && !approvedHook && !legacyApprovedHook && !['session_understanding', 'action_association'].includes(cp?.kind)) next.managed.noProgress += 1;
902
909
  if (reportedStep) next.managed.reportedProgress = (next.managed.reportedProgress || 0) + 1;
903
910
  next.managed.consumedVerifiedRevision = next.managed.verifiedRevision;
904
- if (next.managed.noProgress >= next.managed.maxNoProgress) { escalate(next, 'No verified acceptance progress within the configured continuation limit.'); return finish(next.managed.continuationPolicy === 'progress' ? 'paused' : 'failed', 'No verified acceptance progress within the configured continuation limit.'); }
911
+ if (next.managed.noProgress >= next.managed.maxNoProgress) {
912
+ const stop = deliveryStop(next, now);
913
+ escalate(next, stop.reason);
914
+ return finish(pausesWithoutProgress(next.managed) ? 'paused' : 'failed', stop.reason);
915
+ }
905
916
  }
906
917
  next.consumedRevision = next.revision;
907
918
  if (initialHook) next.managed.awaitingInitialHook = false;
@@ -1122,7 +1133,7 @@ export function canReplanDelivery(run, provider, now = Date.now()) {
1122
1133
  managed?.capability === 'live_hook' && provider === 'claude' && Boolean(managed.liveHookOwner?.identity);
1123
1134
  return Boolean(supported && managed.autonomyPolicy && managed.approvalScopes?.length && !managed.rejectedActionOverflow &&
1124
1135
  run.state === 'blocked' && managed.pendingApproval?.denied === true &&
1125
- !managedBudgetReason(run, now));
1136
+ !deliveryStopBlocksResume(run, now));
1126
1137
  }
1127
1138
  export function canChangeDeliveryContinuation(run) {
1128
1139
  return Boolean(run?.managed && ['paused', 'blocked', 'failed', 'budget_exhausted', 'verification_pending', 'recovery_required'].includes(run.state) &&
@@ -1130,7 +1141,8 @@ export function canChangeDeliveryContinuation(run) {
1130
1141
  }
1131
1142
  function reopenLegacyLimitStop(run, now) {
1132
1143
  if (run?.state !== 'budget_exhausted' ||
1133
- managedBudgetReason(run, now, { checkProgress: false }) ||
1144
+ deliveryStop(run, now)?.code === STOP_CODES.failureLimit ||
1145
+ // Reasons persisted by older versions for stops that were not obsolete caps.
1134
1146
  /No verified|failure limit/i.test(run.reason || '') ||
1135
1147
  run.managed?.noProgress >= run.managed?.maxNoProgress) return;
1136
1148
  if (run.managed) {
@@ -1138,9 +1150,9 @@ function reopenLegacyLimitStop(run, now) {
1138
1150
  // Time spent stopped at an obsolete cap is not failed execution progress.
1139
1151
  run.managed.lastVerifiedProgressAt = now;
1140
1152
  }
1141
- run.state = 'paused'; run.reason = 'Local limits removed; resume this same task.';
1153
+ run.state = 'paused'; run.reason = 'SIMY no longer limits time or turns. Resume this task to continue.';
1142
1154
  }
1143
- export async function controlDelivery({ root = rootDirectory(), provider, session, taskId, approvalId, autonomyPolicy, continuationPolicy, limits, questionId, waitId, answer, action, now = Date.now() }) {
1155
+ export async function controlDelivery({ root = rootDirectory(), provider, session, taskId, approvalId, autonomyPolicy, continuationPolicy, questionId, waitId, answer, action, now = Date.now() }) {
1144
1156
  for (let retry = 0; ; retry += 1) {
1145
1157
  try { return await updateRecord(root, provider, session, record => {
1146
1158
  const run = record?.run;
@@ -1149,49 +1161,12 @@ export async function controlDelivery({ root = rootDirectory(), provider, sessio
1149
1161
  if (['pending', 'pending_received'].includes(run.managed?.handoff?.status)) throw new Error('Task handoff is locked; use delivery handoff_cancel or retry the same target to complete the transfer.');
1150
1162
  if (action !== 'stop' && run.managed?.handoff?.status === 'transferred') throw new Error('Task handoff is locked; continue in the received target session.');
1151
1163
  reopenLegacyLimitStop(run, now);
1152
- if (action === 'extend' || action === 'continuation_policy') {
1153
- if (!canChangeDeliveryContinuation(run)) throw new Error('Pause a managed task before changing its continuation bounds.');
1154
- if (action === 'continuation_policy') {
1155
- if (!['bounded', 'progress'].includes(continuationPolicy)) throw new Error('Select an explicit continuation policy.');
1156
- run.managed.continuationPolicy = continuationMode(continuationPolicy);
1157
- if (continuationPolicy === 'progress') {
1158
- run.hardDeadline ??= run.startedAt + 1440 * 60_000;
1159
- run.hardMaxContinuations ??= 50;
1160
- if (run.managed.resumeChannel === 'codex_app_server') run.managed.providerTokenCeiling ??= MAX_COMPLETION_TOKENS;
1161
- }
1162
- run.continuationPolicyHistory ||= [];
1163
- run.continuationPolicyHistory.push({ policy: continuationPolicy, at: now, source: 'explicit_controller_selection' });
1164
- } else {
1165
- if (!limits || typeof limits !== 'object' || Array.isArray(limits) || !Object.keys(limits).length ||
1166
- Object.keys(limits).some(key => !['hardMaxMinutes', 'hardMaxContinuations', 'providerTokenCeiling'].includes(key))) throw new Error('Supply explicit continuation ceiling increases.');
1167
- const progress = run.managed.continuationPolicy === 'progress';
1168
- const before = { deadline: progress ? run.hardDeadline : run.deadline,
1169
- maxContinuations: progress ? run.hardMaxContinuations : run.maxContinuations,
1170
- tokens: progress ? run.managed.providerTokenCeiling : run.managed.maxTokens };
1171
- const after = { ...before };
1172
- if (limits.hardMaxMinutes !== undefined) {
1173
- after.deadline = run.startedAt + positive(limits.hardMaxMinutes, null, 1440) * 60_000;
1174
- if (after.deadline <= before.deadline) throw new Error('Extension must increase the original deadline within 24 hours of its start.');
1175
- }
1176
- if (limits.hardMaxContinuations !== undefined) {
1177
- after.maxContinuations = positive(limits.hardMaxContinuations, null, 50);
1178
- if (after.maxContinuations <= before.maxContinuations) throw new Error('Extension must increase the continuation ceiling within 50 turns.');
1179
- }
1180
- if (limits.providerTokenCeiling !== undefined) {
1181
- if (run.managed.resumeChannel !== 'codex_app_server') throw new Error('Provider token ceilings require host-observed Codex usage.');
1182
- after.tokens = positive(limits.providerTokenCeiling, null, MAX_COMPLETION_TOKENS);
1183
- if (after.tokens <= (before.tokens || 0)) throw new Error('Extension must increase the provider token ceiling.');
1184
- }
1185
- if (after.deadline <= now || after.maxContinuations <= run.continuations || after.tokens && after.tokens <= run.managed.tokensUsed) throw new Error('Extended ceilings must exceed already consumed time, turns, and tokens.');
1186
- if (progress) { run.hardDeadline = after.deadline; run.hardMaxContinuations = after.maxContinuations; run.managed.providerTokenCeiling = after.tokens; }
1187
- else { run.deadline = after.deadline; run.maxContinuations = after.maxContinuations; run.managed.maxTokens = after.tokens; }
1188
- run.budgetExtensionHistory ||= [];
1189
- run.budgetExtensionHistory.push({ before, after, at: now, source: 'explicit_controller_extension' });
1190
- }
1191
- run.budgetWarnings = deliveryBudgetState(run, now).warnings;
1192
- if (run.state === 'budget_exhausted' && !managedBudgetReason(run, now)) {
1193
- run.state = 'paused'; run.reason = 'Continuation bounds changed; explicitly resume this same task.';
1194
- }
1164
+ if (action === 'continuation_policy') {
1165
+ if (!canChangeDeliveryContinuation(run)) throw new Error('Pause a managed task before changing its continuation policy.');
1166
+ if (!['bounded', 'progress'].includes(continuationPolicy)) throw new Error('Select an explicit continuation policy.');
1167
+ run.managed.continuationPolicy = continuationMode(continuationPolicy);
1168
+ run.continuationPolicyHistory ||= [];
1169
+ run.continuationPolicyHistory.push({ policy: continuationPolicy, at: now, source: 'explicit_controller_selection' });
1195
1170
  run.updatedAt = now;
1196
1171
  return record;
1197
1172
  }
@@ -1209,7 +1184,7 @@ export async function controlDelivery({ root = rootDirectory(), provider, sessio
1209
1184
  const question = run.managed?.understandingQuestion;
1210
1185
  if (run.managed?.phase !== 'needs_clarification' || run.state !== 'blocked' || !question || question.id !== questionId ||
1211
1186
  !text(answer, 1000) || typeof answer !== 'string' || answer.length > 1000) throw new Error('Session question changed or answer is empty; refresh its status.');
1212
- if (managedBudgetReason(run, now)) throw new Error('Original budget exhausted. Session cannot continue.');
1187
+ continueAfterHumanDecision(run, now);
1213
1188
  const read = { kind: 'read', scope: 'task', summary: 'Re-examine the original conversation using the new user answer without changing files' };
1214
1189
  const plan = assessedPlan({ diagnosis: `Resolve the recorded question before any work: ${question.text}`,
1215
1190
  alternatives: [{ id: 'understand_answer', action: read,
@@ -1250,7 +1225,8 @@ export async function controlDelivery({ root = rootDirectory(), provider, sessio
1250
1225
  run.state = 'paused'; run.reason = 'Paused by user.';
1251
1226
  }
1252
1227
  else if (action === 'replan') {
1253
- if (!canReplanDelivery(run, provider, now)) throw new Error('Replanning needs a rejected assessed approval, a supported original-session channel, and remaining budget.');
1228
+ if (!canReplanDelivery(run, provider, now)) throw new Error('Replanning needs a rejected assessed approval, a supported original-session channel, and fewer failures than the limit.');
1229
+ continueAfterHumanDecision(run, now);
1254
1230
  const rejected = run.managed.pendingApproval;
1255
1231
  // Also cover denied records written by earlier versions before rejection
1256
1232
  // fingerprints existed; never re-authorize that exact action on upgrade.
@@ -1285,7 +1261,7 @@ export async function controlDelivery({ root = rootDirectory(), provider, sessio
1285
1261
  if (run.managed?.executionUncertain) { prepareRecoveryDiagnosis(run, now); run.blocker = null; }
1286
1262
  if (run.managed?.pendingApproval) throw new Error('Resolve the exact pending approval before resuming.');
1287
1263
  if (run.managed?.autonomyPolicy && run.managed.decision?.outcome === 'wait' && (run.managed.approvedAction?.id !== run.managed.decision.id || run.managed.approvedAction.dispatchedAt)) throw new Error('Selected action requires a fresh scoped user decision.');
1288
- if (managedBudgetReason(run, now)) throw new Error('Original budget exhausted. Loop cannot resume.');
1264
+ continueAfterHumanDecision(run, now);
1289
1265
  if (run.managed?.approvedAction && run.recoveryCause === 'approved_action' && !run.managed.approvedAction.dispatchedAt && run.managed.capability !== 'live_hook' && run.managed.resumeChannel !== 'codex_app_server') {
1290
1266
  run.managed.approvedAction.dispatchedAt = now;
1291
1267
  run.managed.dispatches ||= [];
@@ -1308,7 +1284,7 @@ export async function controlDelivery({ root = rootDirectory(), provider, sessio
1308
1284
  if (!pending || pending.id !== approvalId || pending.denied || run.state !== 'blocked') throw new Error('Pending approval changed; refresh its status.');
1309
1285
  run.managed.notification = { key: `approval:${pending.id}`, kind: 'approval', message: `${action === 'approve' ? 'Approved' : 'Rejected'} ${pending.scope}.`, at: now };
1310
1286
  if (action === 'approve') {
1311
- if (managedBudgetReason(run, now)) throw new Error('Original budget exhausted. Task cannot resume.');
1287
+ continueAfterHumanDecision(run, now);
1312
1288
  run.managed.pendingApproval = null;
1313
1289
  run.nextStep = run.checkpoint?.nextStep || run.nextStep;
1314
1290
  run.state = 'recovery_required'; run.recoveryCause = 'approved_action';
@@ -1351,11 +1327,10 @@ export async function reconcileRecords({ root = rootDirectory(), now = Date.now(
1351
1327
  }
1352
1328
  const activeTurn = run.managed &&
1353
1329
  (run.state === 'auto_resuming' || current.runtime?.isRunning === true || run.managed.executionAttempt?.phase === 'dispatching');
1354
- const budgetState = deliveryBudgetState(run, now, { checkProgress: !activeTurn });
1355
- run.budgetWarnings = budgetState.warnings;
1356
- if (budgetState.reason && !TERMINAL.has(run.state) && !['verification_pending', 'paused'].includes(run.state)) {
1357
- run.state = budgetState.reason.startsWith('No verified') ? 'paused' : 'budget_exhausted';
1358
- run.reason = budgetState.reason; run.updatedAt = now;
1330
+ const stop = deliveryStop(run, now, { checkProgress: !activeTurn });
1331
+ if (stop && !TERMINAL.has(run.state) && !['verification_pending', 'paused'].includes(run.state)) {
1332
+ run.state = stoppedState(stop);
1333
+ run.reason = stop.reason; run.updatedAt = now;
1359
1334
  }
1360
1335
  else if (run.state === 'active' && observed !== null && observed !== current.process?.identity && current.process?.identity === record.process?.identity) {
1361
1336
  run.state = 'recovery_required'; run.recoveryCause = 'process_exit'; run.reason = run.managed?.autoRecovery ? 'Observed provider process exited. Managed recovery is eligible for a bounded exact-session attempt.' : 'Observed provider process exited. Reopen the existing conversation and explicitly resume.'; run.updatedAt = now;
@@ -27,6 +27,16 @@ function classifyBlockers(findings) {
27
27
  return blockers;
28
28
  }
29
29
 
30
+ // Human review grants a new attempt window, not new proof. Keep the comparison
31
+ // history so previously verified criteria cannot be presented as fresh progress.
32
+ export function restartOutcomeProgressWindow(previous) {
33
+ if (!previous) return previous;
34
+ const nextActions = (previous.next_actions || []).filter(action => action !== "reassess_approach");
35
+ return { ...previous, no_progress_attempts: 0, stalled: false, stall_reason: null,
36
+ has_progress: false, newly_verified_ids: [], next_actions: nextActions,
37
+ next_action: nextActions[0] || "repair_or_verify" };
38
+ }
39
+
30
40
  /**
31
41
  * Compare controller-owned independent observations against one frozen objective.
32
42
  * Evidence references identify proof; changing their paths/hashes is not progress.
@@ -8,7 +8,7 @@ description: Continue an explicitly requested SIMY Delivery Loop in the current
8
8
  Use only when the user explicitly requests this skill or SIMY Delivery Loop for the current task. Installing this plugin does not opt a task in. Do not run SQM unless separately requested.
9
9
 
10
10
  1. Require the SIMY SessionStart hook context with the exact provider and session ID. If absent, explain how to install the SIMY plugin, review and enable its hooks with `/hooks`, and start a new conversation. Codex settings can be opened at `codex://settings`; a hooks subpage deep link is not guaranteed. Never change hook trust or bypass permissions yourself.
11
- 2. For a regular Delivery Loop, use `simy delivery start --provider <provider> --session <exact-session-id> --objective '<user outcome and acceptance criteria>' --next-step '<concrete next action>'`. When the user explicitly asks for a managed task with durable acceptance tracking, add `--managed-file <task.json>` as described below. Default limits: 6 automatic continuations, 3 consecutive failures/missing checkpoints, 30 minutes. Set different finite limits only if the user requests them. Use the current working directory. Never invent a session ID, start another loop to reset limits, or resume a stopped loop without a user request.
11
+ 2. For a regular Delivery Loop, use `simy delivery start --provider <provider> --session <exact-session-id> --objective '<user outcome and acceptance criteria>' --next-step '<concrete next action>'`. When the user explicitly asks for a managed task with durable acceptance tracking, add `--managed-file <task.json>` as described below. The loop stops after 3 failures/missing checkpoints and pauses for review when no progress is verified; it does not limit elapsed time or turns. Set a different failure limit only if the user requests it. Use the current working directory. Never invent a session ID, start another loop to reset limits, or resume a stopped loop without a user request.
12
12
  3. Work in the current conversation with its normal permissions. Before ending each turn, submit a JSON checkpoint through standard input with `simy delivery checkpoint --provider <provider> --session <id> --file -` (for example, a shell heredoc). This updates SIMY state; it does not require writing a checkpoint file to the user's workspace. A file path remains supported when authorized.
13
13
 
14
14
  Checkpoint shapes:
@@ -21,15 +21,14 @@ export async function runDeliveryCommand(argv, { root = rootDirectory(), cwd = p
21
21
  if (action === 'activity') return runActivityCommand(args, { root, output });
22
22
  if (!action || action === '--help') {
23
23
  output('simy delivery activity [--watch] [--json] [--provider codex|claude] [--session ID] — local observations every 60 seconds; not productive time.');
24
- output('Progress: --continuation-policy progress [--hard-max-minutes 1440] [--hard-max-continuations 50] [--provider-token-ceiling 15000000 (Codex only)]. Soft thresholds warn; approvals, missing usage and no verified progress still stop.');
24
+ output('A task stops after repeated failures, and pauses for review when no progress is confirmed. Resume starts a new progress window.');
25
25
  output('simy delivery continuation_policy --provider PROVIDER --session ID --task-id ID --continuation-policy bounded|progress');
26
- output('simy delivery extend --provider PROVIDER --session ID --task-id ID --hard-max-minutes N|--hard-max-continuations N|--provider-token-ceiling N');
27
- output('simy delivery continue --provider codex|claude --session ID [--max-no-progress-minutes 30] [--max-tokens N (Codex only)]\nsimy delivery dependency_ready --provider PROVIDER --session ID --task-id ID --wait-id ID --answer TEXT\nsimy delivery handoff|handoff_cancel --provider codex --session ID --task-id ID --target-session ID');
28
- output('simy delivery start --provider codex|claude --session ID --objective TEXT --next-step TEXT [--managed-file task.json] [--max-continuations 6] [--max-failures 3] [--max-minutes 30]\nsimy delivery checkpoint --provider codex|claude --session ID --file checkpoint.json|-\nsimy delivery status\nsimy delivery stop|pause|resume --provider codex|claude --session ID [--task-id ID]\nsimy delivery replan --provider PROVIDER --session ID --task-id ID\nsimy delivery approve|reject --provider PROVIDER --session ID --task-id ID --approval-id ID\nsimy delivery verify --provider PROVIDER --session ID --task-id ID --criterion-id ID\nsimy delivery set_policy --provider PROVIDER --session ID --task-id ID --autonomy-policy strict|balanced|autonomous\nEnable and review SIMY hooks in your provider first. Manual managed start requires an acceptance specification. Desktop Continue infers the original conversation goal first. Resume preserves original task identity, usage history and approvals; local time, turn and token counters do not stop work.');
26
+ output('simy delivery continue --provider codex|claude --session ID [--max-no-progress-minutes 30] [--max-failures 3]\nsimy delivery dependency_ready --provider PROVIDER --session ID --task-id ID --wait-id ID --answer TEXT\nsimy delivery handoff|handoff_cancel --provider codex --session ID --task-id ID --target-session ID');
27
+ output('simy delivery start --provider codex|claude --session ID --objective TEXT --next-step TEXT [--managed-file task.json] [--max-failures 3]\nsimy delivery checkpoint --provider codex|claude --session ID --file checkpoint.json|-\nsimy delivery status\nsimy delivery stop|pause|resume --provider codex|claude --session ID [--task-id ID]\nsimy delivery replan --provider PROVIDER --session ID --task-id ID\nsimy delivery approve|reject --provider PROVIDER --session ID --task-id ID --approval-id ID\nsimy delivery verify --provider PROVIDER --session ID --task-id ID --criterion-id ID\nsimy delivery set_policy --provider PROVIDER --session ID --task-id ID --autonomy-policy strict|balanced|autonomous\nEnable and review SIMY hooks in your provider first. Manual managed start requires an acceptance specification. Desktop Continue infers the original conversation goal first. Resume preserves original task identity, history and approvals.');
29
28
  return 0;
30
29
  }
31
30
  const options = {};
32
- const allowed = new Set(['provider', 'session', 'objective', 'next-step', 'max-continuations', 'max-failures', 'max-minutes', 'max-no-progress-minutes', 'max-tokens', 'continuation-policy', 'hard-max-minutes', 'hard-max-continuations', 'provider-token-ceiling', 'file', 'managed-file', 'task-id', 'approval-id', 'criterion-id', 'autonomy-policy', 'wait-id', 'answer', 'target-session']);
31
+ const allowed = new Set(['provider', 'session', 'objective', 'next-step', 'max-failures', 'max-no-progress-minutes', 'continuation-policy', 'file', 'managed-file', 'task-id', 'approval-id', 'criterion-id', 'autonomy-policy', 'wait-id', 'answer', 'target-session']);
33
32
  for (let i = 0; i < args.length; i += 2) {
34
33
  const key = args[i]?.replace(/^--/, '');
35
34
  if (!args[i]?.startsWith('--') || !allowed.has(key) || args[i + 1] === undefined || Object.hasOwn(options, key)) throw new Error('Invalid delivery option. Use simy delivery --help.');
@@ -39,9 +38,8 @@ export async function runDeliveryCommand(argv, { root = rootDirectory(), cwd = p
39
38
  let result;
40
39
  if (action === 'status') result = (await listRecords(root)).filter(r => (!options.provider || r.provider === options.provider) && (!options.session || r.session === options.session));
41
40
  else if (action === 'continue') result = await continueSession({ ...base, autonomyPolicy: options['autonomy-policy'], continuationPolicy: options['continuation-policy'],
42
- ...Object.fromEntries([['max-continuations', 'maxContinuations'], ['max-failures', 'maxFailures'], ['max-minutes', 'maxMinutes'],
43
- ['hard-max-minutes', 'hardMaxMinutes'], ['hard-max-continuations', 'hardMaxContinuations'], ['provider-token-ceiling', 'providerTokenCeiling'],
44
- ['max-no-progress-minutes', 'maxNoProgressMinutes'], ['max-tokens', 'maxTokens']].filter(([key]) => options[key] !== undefined).map(([key, field]) => [field, Number(options[key])])) });
41
+ ...Object.fromEntries([['max-failures', 'maxFailures'], ['max-no-progress-minutes', 'maxNoProgressMinutes']]
42
+ .filter(([key]) => options[key] !== undefined).map(([key, field]) => [field, Number(options[key])])) });
45
43
  else if (action === 'handoff') result = await handoffSession({ ...base, taskId: options['task-id'], targetSession: options['target-session'] });
46
44
  else if (action === 'handoff_cancel') result = await cancelHandoffSession({ ...base, taskId: options['task-id'], targetSession: options['target-session'] });
47
45
  else if (action === 'start') {
@@ -51,22 +49,20 @@ export async function runDeliveryCommand(argv, { root = rootDirectory(), cwd = p
51
49
  if (Buffer.byteLength(data) > 64 * 1024) throw new Error('Managed task specification exceeds 64 KiB.');
52
50
  managed = JSON.parse(data);
53
51
  }
54
- if (options['max-tokens'] !== undefined || options['max-no-progress-minutes'] !== undefined) {
55
- if (!managed || typeof managed !== 'object') throw new Error('Managed budget flags require --managed-file; use delivery continue for context-first supervision.');
56
- managed = { ...managed, ...Object.fromEntries([['max-tokens', 'maxTokens'], ['max-no-progress-minutes', 'maxNoProgressMinutes']]
57
- .filter(([key]) => options[key] !== undefined).map(([key, field]) => [field, Number(options[key])])) };
52
+ if (options['max-no-progress-minutes'] !== undefined) {
53
+ if (!managed || typeof managed !== 'object') throw new Error('--max-no-progress-minutes requires --managed-file; use delivery continue for context-first supervision.');
54
+ managed = { ...managed, maxNoProgressMinutes: Number(options['max-no-progress-minutes']) };
58
55
  }
59
56
  result = await startDelivery({ ...base, objective: options.objective, nextStep: options['next-step'], managed, continuationPolicy: options['continuation-policy'],
60
- ...Object.fromEntries([['max-continuations', 'maxContinuations'], ['max-failures', 'maxFailures'], ['max-minutes', 'maxMinutes'],
61
- ['hard-max-minutes', 'hardMaxMinutes'], ['hard-max-continuations', 'hardMaxContinuations'], ['provider-token-ceiling', 'providerTokenCeiling']].filter(([key]) => options[key] !== undefined).map(([key, field]) => [field, Number(options[key])])) });
57
+ ...(options['max-failures'] === undefined ? {} : { maxFailures: Number(options['max-failures']) }) });
62
58
  }
63
59
  else if (action === 'checkpoint') {
64
60
  if (!options.file) throw new Error('--file is required.');
65
61
  const data = options.file === '-' ? await readCheckpointInput(input) : await readFile(options.file, 'utf8');
66
62
  if (Buffer.byteLength(data) > 64 * 1024) throw new Error('Checkpoint exceeds 64 KiB.');
67
63
  result = await checkpointDelivery({ ...base, checkpoint: JSON.parse(data) });
68
- } else if (['stop', 'pause', 'resume', 'replan', 'approve', 'reject', 'set_policy', 'dependency_ready', 'extend', 'continuation_policy'].includes(action)) result = await controlDelivery({ ...base, taskId: options['task-id'], approvalId: options['approval-id'], autonomyPolicy: options['autonomy-policy'], continuationPolicy: options['continuation-policy'],
69
- limits: Object.fromEntries([['hard-max-minutes', 'hardMaxMinutes'], ['hard-max-continuations', 'hardMaxContinuations'], ['provider-token-ceiling', 'providerTokenCeiling']].filter(([key]) => options[key] !== undefined).map(([key, field]) => [field, Number(options[key])])), waitId: options['wait-id'], answer: options.answer, action });
64
+ } else if (['stop', 'pause', 'resume', 'replan', 'approve', 'reject', 'set_policy', 'dependency_ready', 'continuation_policy'].includes(action)) result = await controlDelivery({ ...base, taskId: options['task-id'], approvalId: options['approval-id'], autonomyPolicy: options['autonomy-policy'], continuationPolicy: options['continuation-policy'],
65
+ waitId: options['wait-id'], answer: options.answer, action });
70
66
  else if (action === 'verify') result = await verifyDelivery({ ...base, taskId: options['task-id'], criterionId: options['criterion-id'] });
71
67
  else throw new Error('Unknown delivery command.');
72
68
  output(JSON.stringify(result, null, 2));