@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 +6 -1
- package/package.json +3 -3
- package/src/cli-contract.js +1 -1
- package/src/codex-plugins/simy/hooks/README.md +16 -22
- package/src/codex-plugins/simy/hooks/delivery-loop.mjs +76 -101
- package/src/codex-plugins/simy/hooks/outcome-progress.mjs +10 -0
- package/src/codex-plugins/simy/skills/delivery-loop/SKILL.md +1 -1
- package/src/delivery-hooks/command.js +12 -16
- package/src/delivery-hooks/desktop.js +6 -14
- package/src/delivery-hooks/supervisor.js +7 -31
- package/src/desktop-host.js +15 -14
- package/src/session-supervision/action-association-host.js +7 -3
- package/src/session-supervision/action-association.js +2 -2
- package/src/session-supervision/association-store.js +12 -3
- package/src/session-supervision/claude-sessions.js +36 -20
- package/src/session-supervision/codex-app-server.js +8 -22
- package/src/session-supervision/discovery-window.js +15 -0
- package/src/session-supervision/handoff.js +2 -1
- package/src/session-supervision/index.js +25 -27
- package/src/session-supervision/stop-notifications.js +43 -0
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
|
|
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.
|
|
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",
|
package/src/cli-contract.js
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
} from "./durable-local-task-steps-contract.js";
|
|
14
14
|
import { buildExecutionCapabilityContract } from "./execution-capability-contract.js";
|
|
15
15
|
|
|
16
|
-
export const CLI_VERSION = "0.6.
|
|
16
|
+
export const CLI_VERSION = "0.6.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
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
const
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
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
|
|
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
|
-
|
|
806
|
-
|
|
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
|
|
863
|
-
if (
|
|
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) {
|
|
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
|
-
!
|
|
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
|
-
|
|
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 = '
|
|
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,
|
|
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 === '
|
|
1153
|
-
if (!canChangeDeliveryContinuation(run)) throw new Error('Pause a managed task before changing its continuation
|
|
1154
|
-
if (
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1355
|
-
run.
|
|
1356
|
-
|
|
1357
|
-
run.
|
|
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.
|
|
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('
|
|
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
|
|
27
|
-
output('simy delivery
|
|
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-
|
|
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-
|
|
43
|
-
[
|
|
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-
|
|
55
|
-
if (!managed || typeof managed !== 'object') throw new Error('
|
|
56
|
-
managed = { ...managed,
|
|
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
|
-
...
|
|
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', '
|
|
69
|
-
|
|
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));
|