@awak-app/simy-cli 0.5.72 → 0.5.73
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 +84 -0
- package/package.json +2 -2
- package/src/agent.js +191 -11
- package/src/backend-executable.js +21 -1
- package/src/cli-contract.js +1 -1
- package/src/codex-plugins/simy/hooks/README.md +22 -0
- package/src/codex-plugins/simy/hooks/continuation-policy.mjs +27 -0
- package/src/codex-plugins/simy/hooks/delivery-loop.mjs +179 -34
- package/src/codex-plugins/simy/hooks/outcome-progress.mjs +129 -0
- package/src/coding-run-recovery.js +88 -0
- package/src/coding-run-store.js +2 -2
- package/src/continuation-policy.js +2 -0
- package/src/delivery-hooks/command.js +11 -5
- package/src/delivery-hooks/desktop.js +12 -3
- package/src/delivery-hooks/supervisor.js +14 -10
- package/src/desktop-host.js +8 -6
- package/src/desktop-sqm-settings.js +24 -7
- package/src/device-signing-identity.js +65 -0
- package/src/diagnostic-log.js +68 -0
- package/src/doctor.js +55 -0
- package/src/index.js +7 -0
- package/src/local-task.js +68 -3
- package/src/orchestrator/budget.js +55 -4
- package/src/orchestrator/charter-validation.js +33 -0
- package/src/orchestrator/completion-contract.js +1 -1
- package/src/orchestrator/contract.js +2 -0
- package/src/orchestrator/delivery/references/state-and-receipts.md +21 -3
- package/src/orchestrator/independent-audit.js +9 -0
- package/src/orchestrator/instruction.js +6 -1
- package/src/orchestrator/loop.js +38 -41
- package/src/orchestrator/outcome-monitor.js +58 -38
- package/src/orchestrator/outcome-progress.js +2 -0
- package/src/purpose-local-proof.js +380 -0
- package/src/runner.js +224 -36
- package/src/session-supervision/codex-app-server.js +3 -2
- package/src/session-supervision/index.js +16 -4
- package/src/sqm/bundle-store.js +11 -7
- package/src/sqm/claude-plugin.js +2 -0
- package/src/sqm/command.js +36 -4
- package/src/sqm/diagnostics.js +1 -1
- package/src/sqm/evidence-client.js +23 -7
- package/src/sqm/history-store.js +4 -0
- package/src/sqm/index.js +104 -14
- package/src/sqm/proof.js +3 -23
- package/src/sqm/request-recovery.js +82 -0
- package/src/sqm/review-worker.js +59 -12
- package/src/sqm/session.js +33 -3
- package/src/startup-preflight.js +44 -0
package/README.md
CHANGED
|
@@ -2,6 +2,67 @@
|
|
|
2
2
|
|
|
3
3
|
Local execution agent for SIMY Local Tasks and Agentic Loop runs.
|
|
4
4
|
|
|
5
|
+
`simy --version` prints the installed CLI version without starting an agent or
|
|
6
|
+
checking for updates. `simy doctor` performs read-only local connection and
|
|
7
|
+
provider login checks and prints a JSON diagnostic report: installed/running
|
|
8
|
+
versions, stale discovery records, session expiry, provider readiness, and recent
|
|
9
|
+
structured SQM failures. It does not run a model, renew login, or restart tasks.
|
|
10
|
+
Provider login is separate from model access; unverified model access remains
|
|
11
|
+
`unknown`. Diagnostic journals retain stage/code/HTTP status only, use private
|
|
12
|
+
files scoped to the Web/account/organization, rotate each writer's four 64 KiB
|
|
13
|
+
segments, and keep lifetime failure counts after event rotation.
|
|
14
|
+
|
|
15
|
+
The organization menu's **Allow Codex / Claude fallback** setting defaults to
|
|
16
|
+
off and grants only read-only SQM analysis permission to use the other signed-in
|
|
17
|
+
local provider. It is saved per Web/account/organization. Switching requires an
|
|
18
|
+
explicit native model rejection or unavailable executable, a server that supports
|
|
19
|
+
actual-provider provenance, and fresh actor/settings/lease checks. Authentication,
|
|
20
|
+
quota, malformed output, arbitrary failures and coding tasks do not switch engines.
|
|
21
|
+
Both providers share one analysis deadline. A deterministic provider failure pauses
|
|
22
|
+
new claims and retains failed history until environment/settings change or **Retry
|
|
23
|
+
analysis** is selected.
|
|
24
|
+
|
|
25
|
+
Agentic Loop preflight accepts the task's existing requirement, acceptance
|
|
26
|
+
criteria, required checks and charter context alongside the repository/provider.
|
|
27
|
+
Its `startup` summary lists all blockers, unverified conditions and recovery
|
|
28
|
+
actions together. Environment-only calls remain supported. Task checks reuse the
|
|
29
|
+
execution charter policy; model entitlement, external test resources and assurance
|
|
30
|
+
selection evidence are not guessed from a successful login or recommendation.
|
|
31
|
+
|
|
32
|
+
### Explicit progress continuation
|
|
33
|
+
|
|
34
|
+
Existing tasks use **Bounded** continuation. Select **Progress** before starting
|
|
35
|
+
an Agentic Loop or Local Task, or when enrolling an App session, to treat the
|
|
36
|
+
ordinary token/attempt/time limits as warning thresholds. New independently
|
|
37
|
+
verified acceptance criteria permit further work within finite hard ceilings;
|
|
38
|
+
repeated work without new proof pauses the same task. Approval requests, explicit
|
|
39
|
+
stops and authorization failures still pause execution. Provider text claiming
|
|
40
|
+
success does not count as verification.
|
|
41
|
+
|
|
42
|
+
Local Tasks retain valid output after a soft token warning and stop at their
|
|
43
|
+
explicit hard ceiling (at most 10 million reported provider tokens). The Agentic
|
|
44
|
+
controller limits a whole progress run to its approved allocation, at most
|
|
45
|
+
15 million tokens, and persists the allocation with canonical receipts. Missing
|
|
46
|
+
reliable usage is charged conservatively against the reserved allocation. The
|
|
47
|
+
Web controller supports this mode for the connected local Codex/Claude engines;
|
|
48
|
+
unsupported cloud execution is refused before dispatch.
|
|
49
|
+
|
|
50
|
+
In the App, pause a managed Delivery task before changing its continuation
|
|
51
|
+
policy or extending its bounds. Extension keeps usage, task identity, acceptance
|
|
52
|
+
and approvals, and requires a separate resume action. Delivery hard ceilings are
|
|
53
|
+
24 hours and 50 continuations; host-observed Codex usage can also impose a token
|
|
54
|
+
ceiling. A Claude live-hook session without reliable token telemetry explicitly
|
|
55
|
+
uses time/continuation bounds instead of inventing usage.
|
|
56
|
+
|
|
57
|
+
For interrupted Agentic work, `GET /v1/agentic-loop/:id/recovery` provides a
|
|
58
|
+
read-only objective and checkout fingerprint. Reconcile an uncertain side effect
|
|
59
|
+
with `POST` to that endpoint (`action: reconcile`, authoritative
|
|
60
|
+
`operation_status`, `observation`, `evidence_refs`) before resuming. A paused
|
|
61
|
+
progress run accepts explicit increases through
|
|
62
|
+
`POST /v1/agentic-loop/:id/continuation-ceilings`; it never resets already used
|
|
63
|
+
tokens or automatically resumes. Both routes require the original authorized
|
|
64
|
+
account, organization and device throughout the request.
|
|
65
|
+
|
|
5
66
|
## Apple Silicon Mac app
|
|
6
67
|
|
|
7
68
|
The macOS installer includes a native status window and menu bar app. Open
|
|
@@ -198,6 +259,29 @@ that task.
|
|
|
198
259
|
|
|
199
260
|
## Local Tasks
|
|
200
261
|
|
|
262
|
+
Purpose runs may opt into independent local evidence by freezing user-approved
|
|
263
|
+
`verification_recipes` in the server claim. `capabilities.purpose_verification`
|
|
264
|
+
advertises the device's Ed25519 public key; the server pins that key for the
|
|
265
|
+
attempt. Older clients without this capability cannot supply this evidence.
|
|
266
|
+
`POST /v1/local-tasks/:id/verify-purpose` with `{}` checks the original plan only
|
|
267
|
+
after the task succeeds. GET only reads the cached `task.host_verification`.
|
|
268
|
+
File recipes independently check bounded regular files and optional JSON format;
|
|
269
|
+
command recipes execute approved structured argv for Node tests or package
|
|
270
|
+
`test`, `build`, `check`, and `lint` scripts. Commands require write permission,
|
|
271
|
+
share a five-minute deadline, and retain only output hashes and exit status.
|
|
272
|
+
Optional `protected_paths` bind check definitions to their pre-execution hashes.
|
|
273
|
+
|
|
274
|
+
A receipt binds the actor, device, attempt, objective, recipe plan, local task,
|
|
275
|
+
and before/after repository identity. Changed repositories, stopped or uncertain
|
|
276
|
+
tasks, unknown prior check effects, and changed keys fail closed. Fresh POSTs are
|
|
277
|
+
idempotent; an expired confirmed receipt may be refreshed explicitly on the same
|
|
278
|
+
task, with at most three check rounds and no new Provider invocation. Checks
|
|
279
|
+
never upload source files or raw command output. Exit zero proves the specified
|
|
280
|
+
command's result; it does not prove product acceptance or that a package script
|
|
281
|
+
has no external effects. This host evidence boundary does not isolate a malicious
|
|
282
|
+
process running as the same OS user.
|
|
283
|
+
|
|
284
|
+
|
|
201
285
|
CLI API contract v9 promotes non-coding work to a first-class **Local Task**
|
|
202
286
|
runtime. A Local Task is separate from Agentic Loop: it runs one bounded Codex
|
|
203
287
|
or Claude Code session without Git branches, pull requests, audits, or automatic
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@awak-app/simy-cli",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.73",
|
|
4
4
|
"description": "Local SIMY executor for non-coding Local Tasks and Agentic Loop runs.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"check:workflow-observation-sender": "node --check ./src/workflow-observation-sender.js",
|
|
43
43
|
"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",
|
|
44
44
|
"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",
|
|
45
|
-
"check:syntax": "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/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",
|
|
45
|
+
"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/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
46
|
"check:package": "node ./scripts/check-package-contents.js",
|
|
47
47
|
"build:macos-installer:dev": "bash packaging/build-macos-arm64.sh --unsigned-dev",
|
|
48
48
|
"build:macos-installer": "bash packaging/build-macos-arm64.sh --production",
|
package/src/agent.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { normalizePurposeVerification, assertPurposeActor, preparePurposeVerification, verifyPurposeLocalTask, purposeSigningKeyPath, purposeVerificationCapability, purposeWorkspaceBusy } from "./purpose-local-proof.js";
|
|
2
|
+
import { startupRequirementChecks, summarizeStartupChecks } from "./startup-preflight.js";
|
|
1
3
|
import { BROWSER_HANDOFF_TRANSPORT, browserHandoffPage, decryptBrowserHandoff } from "./browser-handoff.js";
|
|
2
4
|
import { createAuthorizationWindow } from "./browser-authorization.js";
|
|
3
5
|
import { createSqmHistoryStore, projectSqmHistory, sqmHistoryScope } from "./sqm/history-store.js";
|
|
4
6
|
import { createDesktopAccessPolicy, desktopAccessReady, requireDesktopAccess } from './desktop-access.js';
|
|
5
|
-
import { readSqmEnabled, writeSqmEnabled } from "./desktop-sqm-settings.js";
|
|
7
|
+
import { readSqmEnabled, writeSqmEnabled, readSqmSettings, writeSqmEngineFallback } from "./desktop-sqm-settings.js";
|
|
6
8
|
import { createServer } from "node:http";
|
|
7
9
|
import { createHash, randomBytes } from "node:crypto";
|
|
8
10
|
import { createReadStream } from "node:fs";
|
|
@@ -14,10 +16,13 @@ import {
|
|
|
14
16
|
continueLocalCodingRunAfterRepositoryApproval,
|
|
15
17
|
createRun,
|
|
16
18
|
increaseLocalCodingRunProviderTokenBudget,
|
|
19
|
+
extendLocalCodingRunCeilings,
|
|
17
20
|
LocalRunRegistry,
|
|
18
21
|
markCodingRunMonitorFailure,
|
|
19
22
|
isLocalRepositoryApprovalPending,
|
|
20
23
|
pauseLocalCodingRun,
|
|
24
|
+
prepareLocalCodingRunRecovery,
|
|
25
|
+
reconcileLocalCodingRun,
|
|
21
26
|
queueLocalCodingGuidance,
|
|
22
27
|
recheckLocalCodingRun,
|
|
23
28
|
publishRestoredLocalCodingRun,
|
|
@@ -30,6 +35,7 @@ import {
|
|
|
30
35
|
toCompatibleCodingLoopState,
|
|
31
36
|
toLedgerSnapshot,
|
|
32
37
|
} from "./runner.js";
|
|
38
|
+
import { assertRecoveryActor } from "./coding-run-recovery.js";
|
|
33
39
|
import { codingRunsRoot, loadCodingRunSnapshots, saveCodingRunSnapshot } from "./coding-run-store.js";
|
|
34
40
|
import {
|
|
35
41
|
defaultSessionRoot,
|
|
@@ -196,7 +202,15 @@ export async function startAgent({
|
|
|
196
202
|
// as its device (see /v1/auth/complete), so keep it after it is cleared.
|
|
197
203
|
let reclaimableSession = null;
|
|
198
204
|
if (session && sessionRequiresWebAuthorization(session, apiOrigin)) session = null;
|
|
199
|
-
|
|
205
|
+
const purposeKeyPath = dependencies.purposeSigningKeyPath || purposeSigningKeyPath(sessionRoot);
|
|
206
|
+
let purposeSigner = null;
|
|
207
|
+
const refreshPurposeSigner = async () => {
|
|
208
|
+
try { purposeSigner = await purposeVerificationCapability(purposeKeyPath, { create: true }); }
|
|
209
|
+
catch { purposeSigner = null; } // Invalid/private-key state fails closed without blocking ordinary tasks.
|
|
210
|
+
};
|
|
211
|
+
await refreshPurposeSigner();
|
|
212
|
+
let sqmEngineSettings = await readSqmSettings(sessionRoot, apiOrigin, session);
|
|
213
|
+
let sqmEnabled = sqmEngineSettings.enabled;
|
|
200
214
|
let organizationItems = [];
|
|
201
215
|
let organizationState = "unknown";
|
|
202
216
|
let organizationRefresh = null;
|
|
@@ -251,7 +265,8 @@ export async function startAgent({
|
|
|
251
265
|
const features = contractSourceCapabilities.features;
|
|
252
266
|
availableCapabilities = withCliContract({
|
|
253
267
|
...contractSourceCapabilities,
|
|
254
|
-
...(
|
|
268
|
+
...(purposeSigner ? { purpose_verification: purposeSigner } : {}),
|
|
269
|
+
...(features ? { features: { ...features, purpose_host_verification: Boolean(purposeSigner) && isSessionValid(session, Date.now(), apiOrigin), ...(Object.hasOwn(features, "sqm_local_review")
|
|
255
270
|
? { sqm_local_review: features.sqm_local_review === true && sqmEnabled && !pendingOrganization && desktopWorkAllowed() } : {}) } } : {}),
|
|
256
271
|
...(providerStatuses ? { provider_statuses: providerStatuses } : {}),
|
|
257
272
|
}, installMode, {
|
|
@@ -576,6 +591,9 @@ export async function startAgent({
|
|
|
576
591
|
const localRun = registry.get(snapshot.id);
|
|
577
592
|
localRun.session = session;
|
|
578
593
|
localRun.apiOrigin = apiOrigin;
|
|
594
|
+
// A newer ledger is not permission to replace an active local operation.
|
|
595
|
+
// Keep the owner, child, checkpoint and approval state until it settles.
|
|
596
|
+
if (localRun.operation || localRun.child) continue;
|
|
579
597
|
const remoteTime = Date.parse(snapshot.updated_at || "") || 0;
|
|
580
598
|
const localTime = Date.parse(localRun.snapshot.updated_at || "") || 0;
|
|
581
599
|
if (remoteTime > localTime) {
|
|
@@ -874,11 +892,33 @@ export async function startAgent({
|
|
|
874
892
|
sqmLocalReviewWorker = createWorker({
|
|
875
893
|
api: sqmApi,
|
|
876
894
|
beginWork: () => acquireUpdateWork(updateManager),
|
|
877
|
-
runReview: async (request, provider) => {
|
|
895
|
+
runReview: async (request, provider, context = {}) => {
|
|
878
896
|
const scope = await authorizeDesktopWork();
|
|
879
|
-
const
|
|
880
|
-
|
|
897
|
+
const actor = JSON.stringify([apiOrigin, session?.account_id, session?.organization_id, session?.device_id]);
|
|
898
|
+
const settings = await readSqmSettings(sessionRoot, apiOrigin, session);
|
|
899
|
+
const engineSettings = JSON.stringify([settings.enable_engine_fallback, settings.allowed_fallback_providers]);
|
|
900
|
+
const beforeDispatch = async () => {
|
|
901
|
+
await authorizeDesktopWork(scope);
|
|
902
|
+
const current = await readSqmSettings(sessionRoot, apiOrigin, session);
|
|
903
|
+
if (actor !== JSON.stringify([apiOrigin, session?.account_id, session?.organization_id, session?.device_id]) ||
|
|
904
|
+
engineSettings !== JSON.stringify([current.enable_engine_fallback, current.allowed_fallback_providers])) {
|
|
905
|
+
throw Object.assign(new Error("SQM actor or fallback settings changed."), { code: "desktop_access_required" });
|
|
906
|
+
}
|
|
907
|
+
await context.beforeLeaseDispatch?.();
|
|
908
|
+
await authorizeDesktopWork(scope);
|
|
909
|
+
const finalSettings = await readSqmSettings(sessionRoot, apiOrigin, session);
|
|
910
|
+
if (actor !== JSON.stringify([apiOrigin, session?.account_id, session?.organization_id, session?.device_id]) ||
|
|
911
|
+
engineSettings !== JSON.stringify([finalSettings.enable_engine_fallback, finalSettings.allowed_fallback_providers])) {
|
|
912
|
+
throw Object.assign(new Error("SQM actor or fallback settings changed."), { code: "desktop_access_required" });
|
|
913
|
+
}
|
|
914
|
+
};
|
|
915
|
+
return (dependencies.runSqmLocalReview || ((value, selected, options) => runStructuredProviderReview(selected, value, options)))(request, provider, {
|
|
916
|
+
beforeDispatch, readOnlyAnalysis: true, enableEngineFallback: settings.enable_engine_fallback,
|
|
917
|
+
allowedFallbackProviders: settings.allowed_fallback_providers, providerReportingSupported: context.providerReportingSupported === true,
|
|
918
|
+
});
|
|
881
919
|
},
|
|
920
|
+
getEnvironmentKey: () => JSON.stringify([apiOrigin, session?.account_id, session?.organization_id, session?.device_id,
|
|
921
|
+
session?.expires_at, sqmEnabled, sqmEngineSettings, ["codex", "claude"].map(provider => [providerStatuses[provider]?.state, providerStatuses[provider]?.version])]),
|
|
882
922
|
onChange: notifyDesktopStatusChange,
|
|
883
923
|
getWorkspaceUrl: () => {
|
|
884
924
|
const slug = desktopAccess.snapshot().organizationSlug;
|
|
@@ -1164,6 +1204,7 @@ export async function startAgent({
|
|
|
1164
1204
|
: indexedRepository?.local_path || null,
|
|
1165
1205
|
dependencies,
|
|
1166
1206
|
providerStatus: (await readProviderStatuses())[backend],
|
|
1207
|
+
request: body,
|
|
1167
1208
|
});
|
|
1168
1209
|
json(res, 200, environment);
|
|
1169
1210
|
return;
|
|
@@ -1271,7 +1312,8 @@ export async function startAgent({
|
|
|
1271
1312
|
authNonce = randomBytes(16).toString("base64url");
|
|
1272
1313
|
pendingOrganization = null;
|
|
1273
1314
|
desktopLoginPending = false;
|
|
1274
|
-
|
|
1315
|
+
sqmEngineSettings = await readSqmSettings(sessionRoot, apiOrigin, session);
|
|
1316
|
+
sqmEnabled = sqmEngineSettings.enabled;
|
|
1275
1317
|
if (changingScope) sqmLocalReviewWorker?.resetHistory?.();
|
|
1276
1318
|
organizationState = "unknown";
|
|
1277
1319
|
organizationItems = [];
|
|
@@ -1321,6 +1363,35 @@ export async function startAgent({
|
|
|
1321
1363
|
.pipe(res);
|
|
1322
1364
|
return;
|
|
1323
1365
|
}
|
|
1366
|
+
const purposeVerifyMatch = url.pathname.match(/^\/v1\/local-tasks\/([^/]+)\/verify-purpose$/);
|
|
1367
|
+
if (req.method === "POST" && purposeVerifyMatch) {
|
|
1368
|
+
const actorScope = desktopScopeRevision();
|
|
1369
|
+
const actorSession = session;
|
|
1370
|
+
const task = localTaskRegistry.get(decodeURIComponent(purposeVerifyMatch[1]));
|
|
1371
|
+
if (!task) { json(res, 404, { error: "Local Task not found" }); return; }
|
|
1372
|
+
const body = await readBoundedJson(req, 1024).catch(() => { throw Object.assign(new Error("Purpose verification body must be an empty JSON object."), { code: "purpose_verification_plan_invalid", status: 400 }); });
|
|
1373
|
+
if (!body || Array.isArray(body) || typeof body !== "object" || Object.keys(body).length) {
|
|
1374
|
+
json(res, 400, { error: "Verification uses only the frozen task plan.", code: "purpose_verification_plan_invalid" }); return;
|
|
1375
|
+
}
|
|
1376
|
+
const selected = await ensureRepositoryIndexed(task.repository);
|
|
1377
|
+
const approvedInventoryPath = findRepository(repositoryInventory, task.repository)?.local_path;
|
|
1378
|
+
const authorize = async () => {
|
|
1379
|
+
if (actorScope !== desktopScopeRevision() || !isSessionValid(session, Date.now(), apiOrigin) || shuttingDown ||
|
|
1380
|
+
selected?.local_path !== task.workspace_path || findRepository(repositoryInventory, task.repository)?.local_path !== approvedInventoryPath ||
|
|
1381
|
+
registry.list().some(run => run.request?.repository === task.repository && (run.operation || run.child || ["queued", "dispatching", "coding"].includes(run.status))) ||
|
|
1382
|
+
localTaskRegistry.list().some(other => other !== task && other.workspace_path === task.workspace_path && ["queued", "running", "stopping"].includes(other.status))) {
|
|
1383
|
+
throw Object.assign(new Error("Purpose verification scope or workspace changed."), { code: "purpose_verification_scope_changed", status: 403 });
|
|
1384
|
+
}
|
|
1385
|
+
await authorizeDesktopWork(requestAccessScope);
|
|
1386
|
+
if (actorScope !== desktopScopeRevision()) throw Object.assign(new Error("Purpose verification actor changed."), { code: "purpose_verification_scope_changed", status: 403 });
|
|
1387
|
+
};
|
|
1388
|
+
await authorize();
|
|
1389
|
+
await withUpdateWork(updateManager, async () => verifyPurposeLocalTask(task, {
|
|
1390
|
+
session: actorSession, apiOrigin, keyPath: purposeKeyPath, authorize, persist: persistLocalTask,
|
|
1391
|
+
}));
|
|
1392
|
+
json(res, 200, { ok: true, task: localTaskSnapshot(task) });
|
|
1393
|
+
return;
|
|
1394
|
+
}
|
|
1324
1395
|
const localTaskStreamMatch = url.pathname.match(/^\/v1\/local-tasks\/([^/]+)\/stream$/);
|
|
1325
1396
|
if (req.method === "GET" && localTaskStreamMatch) {
|
|
1326
1397
|
if (!isSessionValid(session, Date.now(), apiOrigin)) {
|
|
@@ -1391,7 +1462,13 @@ export async function startAgent({
|
|
|
1391
1462
|
json(res, 401, { error: "simy session expired; run simy again" });
|
|
1392
1463
|
return;
|
|
1393
1464
|
}
|
|
1465
|
+
const purposeStartScope = desktopScopeRevision();
|
|
1394
1466
|
const { body, attachments } = await readCodingLoopStart(req);
|
|
1467
|
+
const purposeVerification = normalizePurposeVerification(body.purpose_verification);
|
|
1468
|
+
if (purposeVerification) {
|
|
1469
|
+
assertPurposeActor(purposeVerification, session, apiOrigin, null, String(req.headers["idempotency-key"] || body.client_request_id || "").trim());
|
|
1470
|
+
if (purposeStartScope !== desktopScopeRevision() || !purposeSigner || purposeSigner.key_id !== purposeVerification.key_id) throw Object.assign(new Error("Purpose verification signer or actor changed."), { code: "purpose_verification_scope_changed", status: 403 });
|
|
1471
|
+
}
|
|
1395
1472
|
const instruction = String(body.instruction || "").trim();
|
|
1396
1473
|
if (!instruction) {
|
|
1397
1474
|
json(res, 400, { error: "instruction is required" });
|
|
@@ -1510,11 +1587,14 @@ export async function startAgent({
|
|
|
1510
1587
|
repository,
|
|
1511
1588
|
expectedOutputs: body.expected_outputs,
|
|
1512
1589
|
providerTokenBudget: body.provider_token_budget,
|
|
1590
|
+
continuationPolicy: body.continuation_policy,
|
|
1591
|
+
providerTokenCeiling: body.provider_token_ceiling,
|
|
1513
1592
|
timeoutMs: body.timeout_ms,
|
|
1514
1593
|
attachmentManifest: preparedAttachments,
|
|
1515
1594
|
scenarioPack,
|
|
1516
1595
|
workflowObservationBinding,
|
|
1517
1596
|
workflowStartIntent,
|
|
1597
|
+
purposeVerification,
|
|
1518
1598
|
});
|
|
1519
1599
|
hashLocalTaskIdempotencyKey(idempotencyKey);
|
|
1520
1600
|
} catch (error) {
|
|
@@ -1636,20 +1716,36 @@ export async function startAgent({
|
|
|
1636
1716
|
expectedOutputs: body.expected_outputs,
|
|
1637
1717
|
scenarioPack,
|
|
1638
1718
|
providerTokenBudget: body.provider_token_budget,
|
|
1719
|
+
continuationPolicy: body.continuation_policy,
|
|
1720
|
+
providerTokenCeiling: body.provider_token_ceiling,
|
|
1639
1721
|
timeoutMs: body.timeout_ms,
|
|
1640
1722
|
idempotencyKeyHash: hashLocalTaskIdempotencyKey(idempotencyKey),
|
|
1641
1723
|
requestFingerprint,
|
|
1642
1724
|
workflowObservationBinding,
|
|
1643
1725
|
workflowStartIntent,
|
|
1726
|
+
purposeVerification,
|
|
1644
1727
|
});
|
|
1728
|
+
if (purposeVerification) await preparePurposeVerification(task, { session, apiOrigin, clientRequestId: idempotencyKey, keyPath: purposeKeyPath,
|
|
1729
|
+
authorize: async () => {
|
|
1730
|
+
await authorizeDesktopWork(requestAccessScope);
|
|
1731
|
+
if (purposeStartScope !== desktopScopeRevision() || !isSessionValid(session, Date.now(), apiOrigin)) throw Object.assign(new Error("Purpose verification actor changed."), { code: "purpose_verification_scope_changed", status: 403 });
|
|
1732
|
+
} });
|
|
1645
1733
|
await persistLocalTask(task);
|
|
1646
1734
|
localTaskRegistry.create(task);
|
|
1647
1735
|
requestClaim.complete(task);
|
|
1648
1736
|
requestClaimCompleted = true;
|
|
1649
1737
|
const runLocalTask =
|
|
1650
1738
|
dependencies.runLocalTask ||
|
|
1651
|
-
((localTask) => startLocalTask(localTask, { session, apiOrigin
|
|
1739
|
+
((localTask) => startLocalTask(localTask, { session, apiOrigin, beforeSpawn: async () => {
|
|
1740
|
+
if (purposeWorkspaceBusy(localTask.workspace_path)) throw Object.assign(new Error("Independent verification owns this workspace."), { code: "purpose_verification_workspace_busy" });
|
|
1741
|
+
if (purposeVerification) {
|
|
1742
|
+
await authorizeDesktopWork(requestAccessScope);
|
|
1743
|
+
assertPurposeActor(purposeVerification, session, apiOrigin, localTask, idempotencyKey);
|
|
1744
|
+
if (purposeStartScope !== desktopScopeRevision() || !isSessionValid(session, Date.now(), apiOrigin)) throw Object.assign(new Error("Purpose verification actor changed."), { code: "purpose_verification_scope_changed" });
|
|
1745
|
+
}
|
|
1746
|
+
} }));
|
|
1652
1747
|
await authorizeDesktopWork(requestAccessScope);
|
|
1748
|
+
if (purposeVerification && (purposeStartScope !== desktopScopeRevision() || !isSessionValid(session, Date.now(), apiOrigin))) throw Object.assign(new Error("Purpose verification actor changed."), { code: "purpose_verification_scope_changed", status: 403 });
|
|
1653
1749
|
task.operation_promise = Promise.resolve(runLocalTask(task))
|
|
1654
1750
|
.catch(async (error) => {
|
|
1655
1751
|
task.status = "failed";
|
|
@@ -1917,6 +2013,10 @@ export async function startAgent({
|
|
|
1917
2013
|
: indexedRepository?.local_path || null,
|
|
1918
2014
|
base_branch: typeof body.base_branch === "string" ? body.base_branch : "dev",
|
|
1919
2015
|
max_attempts: body.max_attempts,
|
|
2016
|
+
continuation_policy: body.continuation_policy,
|
|
2017
|
+
provider_token_ceiling: body.provider_token_ceiling,
|
|
2018
|
+
attempt_ceiling: body.attempt_ceiling,
|
|
2019
|
+
max_no_progress_attempts: body.max_no_progress_attempts,
|
|
1920
2020
|
retry_budget: body.retry_budget,
|
|
1921
2021
|
token_budget: body.provider_token_budget ?? body.token_budget,
|
|
1922
2022
|
ui_evidence_root:
|
|
@@ -2011,6 +2111,66 @@ export async function startAgent({
|
|
|
2011
2111
|
return;
|
|
2012
2112
|
}
|
|
2013
2113
|
|
|
2114
|
+
const recoveryMatch = agenticLoopPath.match(/^\/v1\/agentic-loop\/([^/]+)\/recovery$/);
|
|
2115
|
+
if (recoveryMatch && ["GET", "POST"].includes(req.method)) {
|
|
2116
|
+
if (!isSessionValid(session, Date.now(), apiOrigin)) {
|
|
2117
|
+
json(res, 401, { error: "Restore the authorized SIMY session before recovering this run." }); return;
|
|
2118
|
+
}
|
|
2119
|
+
const run = registry.get(decodeURIComponent(recoveryMatch[1]));
|
|
2120
|
+
if (!run) { json(res, 404, { error: "run not found" }); return; }
|
|
2121
|
+
try {
|
|
2122
|
+
const recoveryScope = desktopScopeRevision();
|
|
2123
|
+
const authorizeRecovery = async () => {
|
|
2124
|
+
await authorizeDesktopWork(requestAccessScope);
|
|
2125
|
+
if (recoveryScope !== desktopScopeRevision() || !isSessionValid(session, Date.now(), apiOrigin)) {
|
|
2126
|
+
throw Object.assign(new Error('The authorized desktop session changed.'), { code: 'desktop_access_required' });
|
|
2127
|
+
}
|
|
2128
|
+
assertRecoveryActor({ ...run, session, apiOrigin });
|
|
2129
|
+
};
|
|
2130
|
+
assertRecoveryActor({ ...run, session, apiOrigin });
|
|
2131
|
+
if (req.method === "GET") {
|
|
2132
|
+
const recovery = await prepareLocalCodingRunRecovery(run);
|
|
2133
|
+
await authorizeRecovery();
|
|
2134
|
+
json(res, 200, { recovery });
|
|
2135
|
+
} else {
|
|
2136
|
+
const body = await readJson(req);
|
|
2137
|
+
await authorizeRecovery();
|
|
2138
|
+
if (body.action !== "reconcile") { json(res, 400, { error: "Choose reconcile after inspecting the interrupted operation." }); return; }
|
|
2139
|
+
const recovery = await reconcileLocalCodingRun(run, {
|
|
2140
|
+
operationStatus: body.operation_status, observation: body.observation, evidenceRefs: body.evidence_refs, authorize: authorizeRecovery,
|
|
2141
|
+
});
|
|
2142
|
+
json(res, 200, { recovery, run_id: run.id, resumed: false });
|
|
2143
|
+
}
|
|
2144
|
+
} catch (error) {
|
|
2145
|
+
json(res, 409, { error: error instanceof Error ? error.message : "Recovery could not be verified.", code: error.code || "recovery_rejected" });
|
|
2146
|
+
}
|
|
2147
|
+
return;
|
|
2148
|
+
}
|
|
2149
|
+
|
|
2150
|
+
const ceilingMatch = agenticLoopPath.match(/^\/v1\/agentic-loop\/([^/]+)\/continuation-ceilings$/);
|
|
2151
|
+
if (req.method === "POST" && ceilingMatch) {
|
|
2152
|
+
if (!isSessionValid(session, Date.now(), apiOrigin)) {
|
|
2153
|
+
json(res, 401, { error: "Restore the authorized SIMY session before extending this run." }); return;
|
|
2154
|
+
}
|
|
2155
|
+
const run = registry.get(decodeURIComponent(ceilingMatch[1]));
|
|
2156
|
+
if (!run) { json(res, 404, { error: "run not found" }); return; }
|
|
2157
|
+
try {
|
|
2158
|
+
const recoveryScope = desktopScopeRevision();
|
|
2159
|
+
assertRecoveryActor({ ...run, session, apiOrigin });
|
|
2160
|
+
const body = await readJson(req);
|
|
2161
|
+
await authorizeDesktopWork(requestAccessScope);
|
|
2162
|
+
if (recoveryScope !== desktopScopeRevision() || !isSessionValid(session, Date.now(), apiOrigin)) {
|
|
2163
|
+
throw Object.assign(new Error('The authorized desktop session changed.'), { code: 'desktop_access_required' });
|
|
2164
|
+
}
|
|
2165
|
+
assertRecoveryActor({ ...run, session, apiOrigin });
|
|
2166
|
+
const budget = await extendLocalCodingRunCeilings(run, body);
|
|
2167
|
+
json(res, 200, { run_id: run.id, budget, resumed: false });
|
|
2168
|
+
} catch (error) {
|
|
2169
|
+
json(res, 409, { error: error instanceof Error ? error.message : "Continuation ceilings could not be extended.", code: error.code || "continuation_ceiling_rejected" });
|
|
2170
|
+
}
|
|
2171
|
+
return;
|
|
2172
|
+
}
|
|
2173
|
+
|
|
2014
2174
|
const providerBudgetMatch = agenticLoopPath.match(
|
|
2015
2175
|
/^\/v1\/agentic-loop\/([^/]+)\/provider-token-budget$/,
|
|
2016
2176
|
);
|
|
@@ -2213,6 +2373,7 @@ export async function startAgent({
|
|
|
2213
2373
|
json(res, 400, localTaskFileErrorPayload(err));
|
|
2214
2374
|
return;
|
|
2215
2375
|
}
|
|
2376
|
+
if (String(err?.code || "").startsWith("purpose_verification_")) { json(res, err.status || 409, { error: err.message, code: err.code }); return; }
|
|
2216
2377
|
json(res, 500, { error: err instanceof Error ? err.message : "internal error" });
|
|
2217
2378
|
}
|
|
2218
2379
|
});
|
|
@@ -2330,7 +2491,9 @@ export async function startAgent({
|
|
|
2330
2491
|
tasks: desktopTaskStatus([registry, directRegistry, localTaskRegistry]),
|
|
2331
2492
|
organizations: { state: organizationState, items: organizationItems, selectedId: session?.organization_id ?? null,
|
|
2332
2493
|
switching: Boolean(pendingOrganization), canSwitch: auth.state === "signed_in" && !organizationBusy() },
|
|
2333
|
-
sqm: { enabled: sqmEnabled,
|
|
2494
|
+
sqm: { enabled: sqmEnabled, engineFallbackEnabled: sqmEngineSettings.enable_engine_fallback,
|
|
2495
|
+
allowedFallbackProviders: sqmEngineSettings.allowed_fallback_providers,
|
|
2496
|
+
paused: sqmLocalReviewWorker?.snapshot?.().paused === true, pauseReason: sqmLocalReviewWorker?.snapshot?.().pause_reason ?? null, canEnable: auth.state === "signed_in" && Boolean(session?.account_id && session?.organization_id) && !pendingOrganization && desktopWorkAllowed(),
|
|
2334
2497
|
activeJobId: sqmLocalReviewWorker?.snapshot?.().active_job_id ?? null,
|
|
2335
2498
|
issue: sqmLocalReviewWorker?.snapshot?.().issue ?? null,
|
|
2336
2499
|
providerPause: sqmLocalReviewWorker?.snapshot?.().provider_pause ?? null,
|
|
@@ -2382,10 +2545,22 @@ export async function startAgent({
|
|
|
2382
2545
|
await writeSqmEnabled(sessionRoot, apiOrigin, settingsSession, enabled);
|
|
2383
2546
|
if (session !== settingsSession) return;
|
|
2384
2547
|
sqmEnabled = enabled;
|
|
2548
|
+
sqmEngineSettings = { ...sqmEngineSettings, enabled };
|
|
2549
|
+
sqmLocalReviewWorker?.retry?.();
|
|
2385
2550
|
refreshAvailableCapabilities();
|
|
2386
2551
|
notifyDesktopStatusChange();
|
|
2387
2552
|
void heartbeat().catch(error => reportHeartbeatError(error, quiet));
|
|
2388
2553
|
},
|
|
2554
|
+
async setSqmEngineFallback({ enabled, allowedProviders }) {
|
|
2555
|
+
await authorizeDesktopWork();
|
|
2556
|
+
if (!isSessionValid(session, Date.now(), apiOrigin) || pendingOrganization) throw new Error("Sign in to an organization first.");
|
|
2557
|
+
const settingsSession = session;
|
|
2558
|
+
await writeSqmEngineFallback(sessionRoot, apiOrigin, settingsSession, { enabled, allowedProviders });
|
|
2559
|
+
if (session !== settingsSession) return;
|
|
2560
|
+
sqmEngineSettings = await readSqmSettings(sessionRoot, apiOrigin, session);
|
|
2561
|
+
sqmLocalReviewWorker?.retry?.(); notifyDesktopStatusChange();
|
|
2562
|
+
},
|
|
2563
|
+
async retrySqmEnvironment() { await authorizeDesktopWork(); sqmLocalReviewWorker?.retry?.(); notifyDesktopStatusChange(); },
|
|
2389
2564
|
getDesktopStatus,
|
|
2390
2565
|
getDesktopTask: async (key) => {
|
|
2391
2566
|
if (key?.startsWith("sqm:") || key?.startsWith("sqm-check:")) {
|
|
@@ -2831,6 +3006,7 @@ function capabilitiesFromInspections(codex, claude) {
|
|
|
2831
3006
|
local_task_streaming: true,
|
|
2832
3007
|
local_task_idempotency: true,
|
|
2833
3008
|
local_task_model: true,
|
|
3009
|
+
progress_continuation_policy: true,
|
|
2834
3010
|
local_task_reconciliation: true,
|
|
2835
3011
|
local_task_control_plane: true,
|
|
2836
3012
|
pipeline_local_task_artifacts: true,
|
|
@@ -2862,6 +3038,7 @@ async function inspectCodingLoopEnvironment({
|
|
|
2862
3038
|
localPath,
|
|
2863
3039
|
dependencies,
|
|
2864
3040
|
providerStatus,
|
|
3041
|
+
request = {},
|
|
2865
3042
|
}) {
|
|
2866
3043
|
const checks = [
|
|
2867
3044
|
{
|
|
@@ -2922,13 +3099,16 @@ async function inspectCodingLoopEnvironment({
|
|
|
2922
3099
|
if (inspection.compatible) {
|
|
2923
3100
|
checks.push({
|
|
2924
3101
|
key: "provider_auth",
|
|
2925
|
-
status: authFailure ? "failed" : "passed",
|
|
3102
|
+
status: authFailure ? (providerStatus?.state === "signed_out" ? "failed" : "unknown") : "passed",
|
|
2926
3103
|
summary: authFailure?.summary || `${backend === "claude" ? "Claude Code" : "Codex"} is signed in.`,
|
|
2927
3104
|
...(authFailure ? { recovery: authFailure.recovery } : {}),
|
|
2928
3105
|
});
|
|
2929
3106
|
}
|
|
3107
|
+
checks.push(...startupRequirementChecks(request));
|
|
3108
|
+
const startup = summarizeStartupChecks(checks);
|
|
2930
3109
|
return {
|
|
2931
|
-
ready: Boolean(repositoryPath && inspection.compatible && !authFailure),
|
|
3110
|
+
ready: Boolean(repositoryPath && inspection.compatible && !authFailure && startup.blocker_keys.length === 0),
|
|
3111
|
+
startup,
|
|
2932
3112
|
repository,
|
|
2933
3113
|
backend,
|
|
2934
3114
|
execution_target: executionTarget,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import spawn from "cross-spawn";
|
|
2
2
|
import os from "node:os";
|
|
3
|
+
import { readdir } from "node:fs/promises";
|
|
3
4
|
import { readdirSync, statSync } from "node:fs";
|
|
4
5
|
import path from "node:path";
|
|
5
6
|
|
|
@@ -26,11 +27,12 @@ export async function resolveBackendExecutable(backend, options = {}) {
|
|
|
26
27
|
|
|
27
28
|
export async function inspectBackendExecutable(
|
|
28
29
|
backend,
|
|
29
|
-
{ spawnImpl = spawn, candidates
|
|
30
|
+
{ spawnImpl = spawn, candidates, timeoutMs = 5_000, signal, discoveryOptions = {} } = {},
|
|
30
31
|
) {
|
|
31
32
|
const assertActive = () => { if (signal?.aborted) throw Object.assign(new Error('Backend inspection was cancelled.'), { code: 'cancelled' }); };
|
|
32
33
|
assertActive();
|
|
33
34
|
const policy = versionPolicy(backend);
|
|
35
|
+
candidates ??= [...backendCandidates(backend, discoveryOptions), ...await bundledClaudeCandidates(backend, discoveryOptions)];
|
|
34
36
|
for (const candidate of [...new Set(candidates.filter(Boolean))]) {
|
|
35
37
|
assertActive();
|
|
36
38
|
const probe = await probeExecutable(candidate, { spawnImpl, timeoutMs, signal });
|
|
@@ -96,6 +98,24 @@ export function backendCandidates(backend, {
|
|
|
96
98
|
];
|
|
97
99
|
}
|
|
98
100
|
|
|
101
|
+
// Claude Desktop can install below <version>/<content hash>/claude.app while
|
|
102
|
+
// an older PATH wrapper still points at <version>/claude.app. Discover only the
|
|
103
|
+
// known app layout; never recursively execute arbitrary files from user folders.
|
|
104
|
+
export async function bundledClaudeCandidates(backend, { platform = process.platform, home = os.homedir(), readDirectory = readdir } = {}) {
|
|
105
|
+
if (backend !== "claude" || platform !== "darwin") return [];
|
|
106
|
+
const root = path.posix.join(home, "Library", "Application Support", "Claude", "claude-code");
|
|
107
|
+
const entries = async target => { try { return await readDirectory(target, { withFileTypes: true }); } catch { return []; } };
|
|
108
|
+
const versions = (await entries(root)).filter(item => item.isDirectory() && /^\d+\.\d+\.\d+$/.test(item.name))
|
|
109
|
+
.sort((a, b) => compareVersions(b.name, a.name)).slice(0, 8);
|
|
110
|
+
const result = [];
|
|
111
|
+
for (const version of versions) {
|
|
112
|
+
const base = path.posix.join(root, version.name);
|
|
113
|
+
const hashes = (await entries(base)).filter(item => item.isDirectory() && /^[a-f0-9]{8,128}$/i.test(item.name)).slice(0, 8);
|
|
114
|
+
for (const hash of hashes) result.push(path.posix.join(base, hash.name, "claude.app", "Contents", "MacOS", "claude"));
|
|
115
|
+
result.push(path.posix.join(base, "claude.app", "Contents", "MacOS", "claude"));
|
|
116
|
+
}
|
|
117
|
+
return result;
|
|
118
|
+
}
|
|
99
119
|
// Desktop installs use either <version>/claude.app or <version>/<hash>/claude.app.
|
|
100
120
|
// Only inspect these two known levels; never recurse through arbitrary home paths
|
|
101
121
|
// or descend through version/hash directory symlinks. Existing explicit/PATH installs retain priority.
|
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.5.
|
|
16
|
+
export const CLI_VERSION = "0.5.73";
|
|
17
17
|
export const CLI_API_CONTRACT_VERSION = 10;
|
|
18
18
|
|
|
19
19
|
export function withCliContract(
|
|
@@ -31,6 +31,28 @@ 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.
|
|
55
|
+
|
|
34
56
|
## Project 実装状況 (dev-work) hook
|
|
35
57
|
|
|
36
58
|
`dev-work.mjs` runs on Stop in both distributions. In a Git work tree with a
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Missing policy means the persisted legacy budget contract, never implicit consent.
|
|
2
|
+
export const CONTINUATION_POLICIES = Object.freeze(["bounded", "progress"]);
|
|
3
|
+
export const MAX_COMPLETION_TOKENS = 15_000_000;
|
|
4
|
+
export const DEFAULT_COMPLETION_ATTEMPT_CEILING = 50;
|
|
5
|
+
|
|
6
|
+
export function continuationMode(value) {
|
|
7
|
+
if (value === undefined || value === null) return "bounded";
|
|
8
|
+
if (!CONTINUATION_POLICIES.includes(value)) throw new Error("Invalid continuation policy.");
|
|
9
|
+
return value;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function progressLimits(input = {}) {
|
|
13
|
+
return {
|
|
14
|
+
continuation_policy: continuationMode(input.continuation_policy),
|
|
15
|
+
provider_token_ceiling: finiteLimit(input.provider_token_ceiling, MAX_COMPLETION_TOKENS, MAX_COMPLETION_TOKENS),
|
|
16
|
+
attempt_ceiling: finiteLimit(input.attempt_ceiling, DEFAULT_COMPLETION_ATTEMPT_CEILING, 100),
|
|
17
|
+
max_no_progress_attempts: finiteLimit(input.max_no_progress_attempts, 3, 20),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function finiteLimit(value, fallback, maximum) {
|
|
22
|
+
if (value === undefined || value === null) return fallback;
|
|
23
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > maximum) {
|
|
24
|
+
throw new Error(`Continuation ceiling must be an integer between 1 and ${maximum}.`);
|
|
25
|
+
}
|
|
26
|
+
return value;
|
|
27
|
+
}
|