@awak-app/simy-cli 0.6.0 → 0.6.2
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 +20 -0
- package/package.json +3 -2
- package/src/cli-contract.js +1 -1
- package/src/codex-plugins/simy/hooks/delivery-loop.mjs +2 -0
- package/src/delivery-hooks/activity.js +125 -0
- package/src/delivery-hooks/command.js +3 -0
- package/src/desktop-host.js +9 -4
- package/src/hook-runtime.js +29 -0
- package/src/index.js +3 -0
- package/src/login-shell-path.js +67 -0
- package/src/sqm/agent-skill.js +2 -12
- package/src/sqm/claude-plugin.js +2 -4
- package/src/workflow-observation-sender.js +6 -2
package/README.md
CHANGED
|
@@ -104,6 +104,10 @@ Detached daemon and desktop-host diagnostics are saved to
|
|
|
104
104
|
(`SIMY_HOME` defaults to `~/.simy`). Each log is limited to 1 MiB plus two
|
|
105
105
|
backups; credentials, Cookie headers and URL queries are redacted. Installer
|
|
106
106
|
handoff diagnostics remain in `~/.simy/installer/desktop-update.log`.
|
|
107
|
+
The macOS window also saves the latest nonzero background-host exit in
|
|
108
|
+
`$SIMY_HOME/logs/desktop-host-exit.log`, including its timestamp, exit status and
|
|
109
|
+
up to 16 KiB of stderr. Credential-bearing lines are omitted; each abnormal exit
|
|
110
|
+
replaces the previous record. This includes failures before JavaScript starts.
|
|
107
111
|
`simy --version` or `simy -v` prints the package version without starting work.
|
|
108
112
|
|
|
109
113
|
Closing the window keeps the agent running. Open the app again or choose
|
|
@@ -188,6 +192,22 @@ checks the bundled CLI in ChatGPT and Codex apps in system and user Applications
|
|
|
188
192
|
Claude discovery likewise falls back to a compatible native or Desktop bundle
|
|
189
193
|
when an older PATH installation cannot meet its version requirement.
|
|
190
194
|
|
|
195
|
+
On macOS and Linux, the desktop host reads the user's interactive login-shell
|
|
196
|
+
PATH once at startup, so providers installed through nvm, Volta or custom prefixes
|
|
197
|
+
can be found from GUI launches, including Finder or the Dock on macOS. Shell paths
|
|
198
|
+
take priority; existing launcher paths remain available. If the five-second shell
|
|
199
|
+
probe cannot read PATH, startup uses the launcher PATH. Windows keeps its existing PATH.
|
|
200
|
+
|
|
201
|
+
Installed Codex and Claude hooks use an absolute Node runtime path. When available,
|
|
202
|
+
the SIMY installer's `current` link or Homebrew's `opt` link replaces a versioned
|
|
203
|
+
runtime path, allowing hooks to follow updates without relying on the provider's PATH.
|
|
204
|
+
|
|
205
|
+
Use `simy delivery activity --watch` for a read-only local snapshot every minute,
|
|
206
|
+
or add `--json` for one JSON object per sample. It separates execution, waits,
|
|
207
|
+
stops, failures and repeated attempts without new verified outcomes, with task
|
|
208
|
+
identity, agent, reasons and outcome timestamps. Missing evidence is shown as
|
|
209
|
+
unknown; open time is never counted as productive time. See [work activity](docs/work-activity.md).
|
|
210
|
+
|
|
191
211
|
The plugin also provides an opt-in, hook-based Delivery Loop in the user's existing
|
|
192
212
|
Codex or Claude Code conversation. Install the plugin, review and enable SIMY hooks
|
|
193
213
|
with `/hooks`, then request `/simy:delivery-loop` for the task. Installing or updating
|
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.2",
|
|
4
4
|
"description": "Local SIMY executor for non-coding Local Tasks and Agentic Loop runs.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"start": "node ./src/index.js",
|
|
15
15
|
"test": "node --test",
|
|
16
16
|
"test:e2e:console": "node ./scripts/console-e2e.js",
|
|
17
|
+
"test:e2e:work-activity": "node ./scripts/work-activity-e2e.js",
|
|
17
18
|
"test:e2e:sqm-complete-loop": "node ./scripts/sqm-complete-loop-e2e.js",
|
|
18
19
|
"test:e2e:sqm-local-review": "node ./scripts/sqm-local-review-e2e.js",
|
|
19
20
|
"test:e2e:console:narrow": "CLI_E2E_COLUMNS=78 CLI_E2E_ROWS=40 node ./scripts/console-e2e.js",
|
|
@@ -42,7 +43,7 @@
|
|
|
42
43
|
"check:workflow-observation-sender": "node --check ./src/workflow-observation-sender.js",
|
|
43
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",
|
|
44
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",
|
|
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
|
+
"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
47
|
"check:package": "node ./scripts/check-package-contents.js",
|
|
47
48
|
"build:macos-installer:dev": "bash packaging/build-macos-arm64.sh --unsigned-dev",
|
|
48
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.2";
|
|
17
17
|
export const CLI_API_CONTRACT_VERSION = 10;
|
|
18
18
|
|
|
19
19
|
export function withCliContract(
|
|
@@ -689,6 +689,8 @@ function newVerifiedCriterion(run, id, now) {
|
|
|
689
689
|
managed.verifiedCriterionHistory.push(id);
|
|
690
690
|
} else if (Object.hasOwn(managed.verified, id)) return;
|
|
691
691
|
managed.verifiedRevision += 1;
|
|
692
|
+
// Unlike the budget baseline, this is never initialized by enrollment/migration.
|
|
693
|
+
if (!Object.hasOwn(managed.verified, id)) managed.lastOutcomeAt = now;
|
|
692
694
|
managed.lastVerifiedProgressAt = Math.max(managed.lastVerifiedProgressAt ?? run.startedAt, now);
|
|
693
695
|
}
|
|
694
696
|
export async function verifyAutomaticCriteria(record, now) {
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
2
|
+
import { listRecords, processIdentity } from '../codex-plugins/simy/hooks/delivery-loop.mjs';
|
|
3
|
+
|
|
4
|
+
export const ACTIVITY_INTERVAL_MS = 60_000;
|
|
5
|
+
const clean = value => typeof value === 'string'
|
|
6
|
+
? value.replace(/[\x00-\x1f\x7f-\x9f\u202a-\u202e\u2066-\u2069]/g, ' ').trim() : '';
|
|
7
|
+
const timestamp = (value, now) => Number.isFinite(value) && value >= 0 && value <= now ? value : null;
|
|
8
|
+
const iso = value => value === null ? null : new Date(value).toISOString();
|
|
9
|
+
|
|
10
|
+
function stateOf(record, now, liveRuntime) {
|
|
11
|
+
const run = record.run;
|
|
12
|
+
const state = run?.state;
|
|
13
|
+
if (['failed', 'stopped', 'completed', 'reported_done'].includes(state)) return state;
|
|
14
|
+
if (state === 'paused') return 'stopped';
|
|
15
|
+
if (['blocked', 'verification_pending', 'budget_exhausted'].includes(state)) return 'waiting';
|
|
16
|
+
const recent = value => timestamp(value, now) !== null && now - value < ACTIVITY_INTERVAL_MS;
|
|
17
|
+
const runtime = record.runtime;
|
|
18
|
+
// An open process alone is not execution evidence. A positive hook observation
|
|
19
|
+
// also needs a matching live identity and a recent timestamp.
|
|
20
|
+
if (runtime?.isRunning === true && liveRuntime && recent(runtime.observedAt)) return 'running';
|
|
21
|
+
const attempt = run?.managed?.executionAttempt;
|
|
22
|
+
if (state === 'auto_resuming' && attempt?.phase === 'dispatching' &&
|
|
23
|
+
recent(attempt.lastActivityAt)) return 'running';
|
|
24
|
+
if (state === 'recovery_required') return 'waiting';
|
|
25
|
+
if (runtime?.isRunning === false && recent(runtime.observedAt)) return 'waiting';
|
|
26
|
+
if (attempt?.phase === 'claimed' && recent(attempt.claimedAt)) return 'waiting';
|
|
27
|
+
return 'unknown';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function workActivitySnapshot(records, { now = Date.now(), inspectProcess = processIdentity } = {}) {
|
|
31
|
+
const items = [];
|
|
32
|
+
for (const record of records) {
|
|
33
|
+
const run = record.run;
|
|
34
|
+
if (!run && !record.monitor && !record.tracked) continue;
|
|
35
|
+
const managed = run?.managed;
|
|
36
|
+
const runtime = record.runtime;
|
|
37
|
+
let liveRuntime = false;
|
|
38
|
+
if (runtime?.isRunning === true && runtime.process?.identity &&
|
|
39
|
+
timestamp(runtime.observedAt, now) !== null && now - runtime.observedAt < ACTIVITY_INTERVAL_MS) {
|
|
40
|
+
try { liveRuntime = await inspectProcess(runtime.process.pid) === runtime.process.identity; }
|
|
41
|
+
catch { /* Unknown identity cannot prove execution. */ }
|
|
42
|
+
}
|
|
43
|
+
const executionState = stateOf(record, now, liveRuntime);
|
|
44
|
+
const lastOutcomeAt = timestamp(managed?.lastOutcomeAt, now);
|
|
45
|
+
const attempts = Number.isInteger(managed?.noProgress) && managed.noProgress >= 0 ? managed.noProgress : null;
|
|
46
|
+
const progressState = attempts >= 2 ? 'repeating_without_verified_progress'
|
|
47
|
+
: lastOutcomeAt !== null ? 'verified_outcome_recorded' : 'unknown';
|
|
48
|
+
const observed = [record.observedAt, runtime?.observedAt, managed?.executionAttempt?.lastActivityAt]
|
|
49
|
+
.map(value => timestamp(value, now)).filter(value => value !== null);
|
|
50
|
+
const observedAt = observed.length ? Math.max(...observed) : null;
|
|
51
|
+
items.push({
|
|
52
|
+
taskId: clean(managed?.taskId) || clean(record.associationTaskId) || null,
|
|
53
|
+
provider: clean(record.provider), session: clean(record.session),
|
|
54
|
+
agent: `${clean(record.provider)}:${clean(record.session)}`, humanAssignee: null,
|
|
55
|
+
objective: clean(run?.objective) || null, executionState, recordedState: clean(run?.state) || null,
|
|
56
|
+
progressState, noProgressAttempts: attempts,
|
|
57
|
+
lastOutcomeAt: iso(lastOutcomeAt),
|
|
58
|
+
minutesSinceOutcome: lastOutcomeAt === null ? null : Math.floor((now - lastOutcomeAt) / ACTIVITY_INTERVAL_MS),
|
|
59
|
+
observedAt: iso(observedAt), observationAgeMinutes: observedAt === null ? null : Math.floor((now - observedAt) / ACTIVITY_INTERVAL_MS),
|
|
60
|
+
reason: executionState === 'unknown' ? 'No fresh execution observation; activity is unknown.'
|
|
61
|
+
: clean(run?.reason) || (executionState === 'running' ? 'Recent execution observation.' : 'Provider turn is waiting.'),
|
|
62
|
+
recordedReason: clean(run?.reason) || null,
|
|
63
|
+
waitOwner: clean(managed?.dependencyWait?.owner) || null,
|
|
64
|
+
nextCheck: clean(managed?.dependencyWait?.nextCheck) || null,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
items.sort((a, b) => a.agent.localeCompare(b.agent));
|
|
68
|
+
return { schemaVersion: 1, sampledAt: new Date(now).toISOString(), intervalMs: ACTIVITY_INTERVAL_MS,
|
|
69
|
+
measurement: 'point_in_time_observations_not_productive_duration', items };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function wrap(value, width = 78) {
|
|
73
|
+
const chars = Array.from(value);
|
|
74
|
+
const lines = [];
|
|
75
|
+
while (chars.length) lines.push(chars.splice(0, width).join(''));
|
|
76
|
+
return lines.join('\n');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function formatWorkActivity(snapshot) {
|
|
80
|
+
const lines = [`Work activity | ${snapshot.sampledAt}`, 'Sampling: 60s. Observations are not productive time.'];
|
|
81
|
+
if (!snapshot.items.length) lines.push('No locally recorded tasks or monitored sessions.');
|
|
82
|
+
for (const item of snapshot.items) {
|
|
83
|
+
lines.push('', `Task: ${item.taskId || 'unassigned (see session)'}`,
|
|
84
|
+
`Agent: ${item.agent}`, 'Human assignee: unknown',
|
|
85
|
+
`State: ${item.executionState} | Progress: ${item.progressState}`,
|
|
86
|
+
`Last verified outcome: ${item.lastOutcomeAt || 'unknown'}`,
|
|
87
|
+
`Attempts without new verified outcome: ${item.noProgressAttempts ?? 'unknown'}`,
|
|
88
|
+
`Last observation: ${item.observedAt || 'unknown'}`,
|
|
89
|
+
`Reason: ${item.reason}`);
|
|
90
|
+
if (item.recordedReason && item.recordedReason !== item.reason) lines.push(`Recorded reason: ${item.recordedReason}`);
|
|
91
|
+
if (item.waitOwner) lines.push(`Waiting on: ${item.waitOwner}`);
|
|
92
|
+
if (item.nextCheck) lines.push(`Next check: ${item.nextCheck}`);
|
|
93
|
+
}
|
|
94
|
+
return lines.map(line => wrap(line)).join('\n');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function runActivityCommand(args, { root, output = console.log, now = Date.now,
|
|
98
|
+
readRecords = listRecords, inspectProcess = processIdentity, wait = delay, signal } = {}) {
|
|
99
|
+
const options = {};
|
|
100
|
+
for (let i = 0; i < args.length; i++) {
|
|
101
|
+
const key = args[i];
|
|
102
|
+
if (!['--watch', '--json', '--provider', '--session'].includes(key) || Object.hasOwn(options, key)) {
|
|
103
|
+
throw new Error('Use delivery activity [--watch] [--json] [--provider codex|claude] [--session ID].');
|
|
104
|
+
}
|
|
105
|
+
if (['--watch', '--json'].includes(key)) options[key] = true;
|
|
106
|
+
else {
|
|
107
|
+
const value = args[++i];
|
|
108
|
+
if (!value || value.startsWith('--')) throw new Error(`Missing value for ${key}.`);
|
|
109
|
+
options[key] = value;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (options['--provider'] && !['codex', 'claude'].includes(options['--provider'])) throw new Error('Unsupported provider.');
|
|
113
|
+
do {
|
|
114
|
+
if (signal?.aborted) break;
|
|
115
|
+
const records = (await readRecords(root)).filter(record =>
|
|
116
|
+
(!options['--provider'] || record.provider === options['--provider']) &&
|
|
117
|
+
(!options['--session'] || record.session === options['--session']));
|
|
118
|
+
const snapshot = await workActivitySnapshot(records, { now: now(), inspectProcess });
|
|
119
|
+
output(options['--json'] ? JSON.stringify(snapshot) : formatWorkActivity(snapshot));
|
|
120
|
+
if (!options['--watch']) break;
|
|
121
|
+
try { await wait(ACTIVITY_INTERVAL_MS, undefined, { signal }); }
|
|
122
|
+
catch (error) { if (error.name === 'AbortError' && signal?.aborted) break; throw error; }
|
|
123
|
+
} while (!signal?.aborted);
|
|
124
|
+
return 0;
|
|
125
|
+
}
|
|
@@ -2,6 +2,7 @@ import { readFile } from 'node:fs/promises';
|
|
|
2
2
|
import { startDelivery, checkpointDelivery, controlDelivery, listRecords, rootDirectory, verifyDelivery } from '../codex-plugins/simy/hooks/delivery-loop.mjs';
|
|
3
3
|
import { continueSession } from '../session-supervision/index.js';
|
|
4
4
|
import { cancelHandoffSession, handoffSession } from '../session-supervision/handoff.js';
|
|
5
|
+
import { runActivityCommand } from './activity.js';
|
|
5
6
|
|
|
6
7
|
async function readCheckpointInput(input) {
|
|
7
8
|
const chunks = [];
|
|
@@ -17,7 +18,9 @@ async function readCheckpointInput(input) {
|
|
|
17
18
|
|
|
18
19
|
export async function runDeliveryCommand(argv, { root = rootDirectory(), cwd = process.cwd(), input = process.stdin, output = console.log } = {}) {
|
|
19
20
|
const [action, ...args] = argv;
|
|
21
|
+
if (action === 'activity') return runActivityCommand(args, { root, output });
|
|
20
22
|
if (!action || action === '--help') {
|
|
23
|
+
output('simy delivery activity [--watch] [--json] [--provider codex|claude] [--session ID] — local observations every 60 seconds; not productive time.');
|
|
21
24
|
output('Progress: --continuation-policy progress [--hard-max-minutes 1440] [--hard-max-continuations 50] [--provider-token-ceiling 15000000 (Codex only)]. Soft thresholds warn; approvals, missing usage and no verified progress still stop.');
|
|
22
25
|
output('simy delivery continuation_policy --provider PROVIDER --session ID --task-id ID --continuation-policy bounded|progress');
|
|
23
26
|
output('simy delivery extend --provider PROVIDER --session ID --task-id ID --hard-max-minutes N|--hard-max-continuations N|--provider-token-ceiling N');
|
package/src/desktop-host.js
CHANGED
|
@@ -23,7 +23,8 @@ import { dirname, join } from "node:path";
|
|
|
23
23
|
import { homedir } from "node:os";
|
|
24
24
|
import { chmod, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
25
25
|
import { startAgent } from "./agent.js";
|
|
26
|
-
import { createRuntimeDiagnostics } from "./runtime-diagnostics.js";
|
|
26
|
+
import { createRuntimeDiagnostics, captureRuntimeOutput } from "./runtime-diagnostics.js";
|
|
27
|
+
import { applyLoginShellPath } from "./login-shell-path.js";
|
|
27
28
|
import { createCliAutoUpdater, readCliPackageInfo } from "./auto-update.js";
|
|
28
29
|
import { createAgentShutdown } from "./shutdown.js";
|
|
29
30
|
import { resolveWebOrigin } from "./web-origin.js";
|
|
@@ -735,7 +736,7 @@ export async function openProviderLogin(provider, root, {
|
|
|
735
736
|
const directory = await mkdtemp(join(root, `${provider}-login-`));
|
|
736
737
|
const script = join(directory, `Sign in to ${label}.command`);
|
|
737
738
|
const quote = (s) => `'${s.replaceAll("'", "'\\''")}'`;
|
|
738
|
-
await writeFile(script, `#!/bin/bash\nexport PATH
|
|
739
|
+
await writeFile(script, `#!/bin/bash\nexport PATH=${quote(env.PATH || "/usr/bin:/bin")}\n${quote(executable.executable)} ${args.join(" ")}\nresult=$?\nrm -f -- ${quote(script)}\nrmdir -- ${quote(directory)}\nexit "$result"\n`, { mode: 0o700 });
|
|
739
740
|
await chmod(script, 0o700);
|
|
740
741
|
try { await execFileImpl("/usr/bin/open", ["-a", "Terminal", script], { timeout: 10_000 }); }
|
|
741
742
|
catch (error) { await rm(directory, { recursive: true, force: true }); throw error; }
|
|
@@ -750,8 +751,12 @@ if (isDesktopHostEntry()) {
|
|
|
750
751
|
let host;
|
|
751
752
|
const stop = () => { void host?.close().finally(() => process.exit(0)); };
|
|
752
753
|
process.once("SIGTERM", stop); process.once("SIGINT", stop);
|
|
753
|
-
|
|
754
|
-
|
|
754
|
+
const diagnostics = createRuntimeDiagnostics(process.env.SIMY_HOME?.trim() || join(homedir(), ".simy"), { name: "desktop-host" });
|
|
755
|
+
// stdout is the native JSONL channel; stderr is the only unstructured output
|
|
756
|
+
// and the native window does not display it, so keep it for support.
|
|
757
|
+
captureRuntimeOutput(diagnostics, { stderr: process.stderr });
|
|
758
|
+
applyLoginShellPath({ diagnostics }).catch(() => {}).then(() => startDesktopHost()).then((value) => { host = value; }).catch((error) => {
|
|
759
|
+
diagnostics.write(`startup failed: ${error?.stack || error}`);
|
|
755
760
|
const conflict = error?.code === PROCESS_CONFLICT_CODE;
|
|
756
761
|
process.stdout.write(`${JSON.stringify({
|
|
757
762
|
type: "error",
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
// Version-specific runtimes are replaced by the stable alias that follows
|
|
4
|
+
// upgrades: the installer's `current` release link and Homebrew's `opt` link.
|
|
5
|
+
const STABLE_RUNTIME_ALIASES = [
|
|
6
|
+
[/^(.*)\/installer\/releases\/[^/]+\/[^/]+\.app\/(Contents\/Resources\/runtime\/bin\/node)$/, "$1/installer/current/$2"],
|
|
7
|
+
[/^(.*)\/Cellar\/([^/]+)\/[^/]+\/(bin\/node)$/, "$1/opt/$2/$3"],
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
// Provider hooks are persisted into Codex/Claude configuration and run long
|
|
11
|
+
// after this process exits, often from GUI apps without the user's shell PATH.
|
|
12
|
+
// They must therefore name an absolute runtime that survives SIMY and Node updates.
|
|
13
|
+
export function stableHookRuntime({ execPath = process.execPath, fileExists = existsSync } = {}) {
|
|
14
|
+
for (const [pattern, replacement] of STABLE_RUNTIME_ALIASES) {
|
|
15
|
+
if (!pattern.test(execPath)) continue;
|
|
16
|
+
const alias = execPath.replace(pattern, replacement);
|
|
17
|
+
if (fileExists(alias)) return alias;
|
|
18
|
+
}
|
|
19
|
+
return execPath;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function hookRuntimeCommand(command, { platform = process.platform, ...options } = {}) {
|
|
23
|
+
if (typeof command !== "string" || !command.startsWith("node ")) return command;
|
|
24
|
+
return `${quoteRuntime(stableHookRuntime(options), platform)} ${command.slice("node ".length)}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function quoteRuntime(value, platform) {
|
|
28
|
+
return platform === "win32" ? `"${value.replaceAll('"', '""')}"` : `'${value.replaceAll("'", "'\\''")}'`;
|
|
29
|
+
}
|
package/src/index.js
CHANGED
|
@@ -57,6 +57,9 @@ if (!configuredWebOrigin) {
|
|
|
57
57
|
} else if (dispatchArgv[0] === "doctor") {
|
|
58
58
|
const { runDoctor } = await import("./doctor.js");
|
|
59
59
|
process.exitCode = await runDoctor({ webOrigin: configuredWebOrigin, cliVersion: packageInfo.version });
|
|
60
|
+
} else if (dispatchArgv[0] === "delivery" && dispatchArgv[1] === "activity") {
|
|
61
|
+
// A local observation must not install updates or require a network connection.
|
|
62
|
+
await runCommand();
|
|
60
63
|
} else if (dispatchArgv[0] === "update") {
|
|
61
64
|
const checkOnly = args.has("--check");
|
|
62
65
|
const result = await runCliUpdateGate({
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
const START = "__SIMY_LOGIN_PATH_START__";
|
|
6
|
+
const END = "__SIMY_LOGIN_PATH_END__";
|
|
7
|
+
|
|
8
|
+
// Apps opened from Finder or the Dock inherit launchd's minimal PATH, not the
|
|
9
|
+
// user's shell profile. Provider CLIs installed through nvm, volta, fnm, asdf,
|
|
10
|
+
// pnpm or custom prefixes are therefore invisible unless we ask the user's own
|
|
11
|
+
// login shell once. A slow or broken profile must never prevent startup.
|
|
12
|
+
export async function resolveLoginShellPath({
|
|
13
|
+
platform = process.platform,
|
|
14
|
+
env = process.env,
|
|
15
|
+
execFileImpl = execFile,
|
|
16
|
+
fileExists = existsSync,
|
|
17
|
+
timeoutMs = 5_000,
|
|
18
|
+
} = {}) {
|
|
19
|
+
if (platform === "win32") return null;
|
|
20
|
+
const shell = loginShell({ platform, env, fileExists });
|
|
21
|
+
if (!shell) return null;
|
|
22
|
+
const output = await new Promise((resolve) => {
|
|
23
|
+
// Read the exported environment rather than expanding $PATH in the shell:
|
|
24
|
+
// fish expands a quoted $PATH with spaces, but exports it colon-separated.
|
|
25
|
+
const child = execFileImpl(shell, ["-l", "-i", "-c", `echo ${START}; /usr/bin/env; echo ${END}`], {
|
|
26
|
+
env: { ...env, TERM: "dumb", DISABLE_AUTO_UPDATE: "true" },
|
|
27
|
+
timeout: timeoutMs,
|
|
28
|
+
killSignal: "SIGKILL",
|
|
29
|
+
maxBuffer: 1024 * 1024,
|
|
30
|
+
}, (_error, stdout) => resolve(String(stdout || "")));
|
|
31
|
+
child?.stdin?.end();
|
|
32
|
+
});
|
|
33
|
+
const start = output.lastIndexOf(START);
|
|
34
|
+
const end = output.indexOf(END, start);
|
|
35
|
+
if (start < 0 || end < 0) return null;
|
|
36
|
+
const line = output.slice(start + START.length, end).split(/\r?\n/).find((item) => item.startsWith("PATH="));
|
|
37
|
+
return line?.slice("PATH=".length) || null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Login-shell entries take priority, matching what the user sees in Terminal.
|
|
41
|
+
// Existing entries (including the launcher's fallback directories) are kept.
|
|
42
|
+
export function mergePathEntries(...values) {
|
|
43
|
+
const entries = [];
|
|
44
|
+
for (const value of values) {
|
|
45
|
+
for (const entry of String(value || "").split(path.delimiter)) {
|
|
46
|
+
if (path.isAbsolute(entry) && !entries.includes(entry)) entries.push(entry);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return entries.join(path.delimiter);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function applyLoginShellPath({ env = process.env, platform = process.platform, diagnostics, ...options } = {}) {
|
|
53
|
+
if (platform === "win32") return false;
|
|
54
|
+
const loginPath = await resolveLoginShellPath({ env, platform, ...options });
|
|
55
|
+
if (!loginPath) {
|
|
56
|
+
diagnostics?.write("login shell PATH unavailable; using launcher PATH");
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
env.PATH = mergePathEntries(loginPath, env.PATH);
|
|
60
|
+
diagnostics?.write(`login shell PATH applied (${env.PATH.split(path.delimiter).length} entries)`);
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function loginShell({ platform, env, fileExists }) {
|
|
65
|
+
const candidates = [env.SHELL, platform === "darwin" ? "/bin/zsh" : null, "/bin/sh"];
|
|
66
|
+
return candidates.find((candidate) => candidate && path.isAbsolute(candidate) && fileExists(candidate)) || null;
|
|
67
|
+
}
|
package/src/sqm/agent-skill.js
CHANGED
|
@@ -3,6 +3,7 @@ import { cp, lstat, mkdir, readFile, readdir, rename, rm, writeFile } from "node
|
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { hookRuntimeCommand } from "../hook-runtime.js";
|
|
6
7
|
|
|
7
8
|
import { inspectClaudePlugin, installClaudePlugin } from "./claude-plugin.js";
|
|
8
9
|
|
|
@@ -328,25 +329,14 @@ async function bundledPluginManifest({ overrides = new Map() } = {}) {
|
|
|
328
329
|
async function installedPluginOverrides() {
|
|
329
330
|
const relative = "hooks/hooks.json";
|
|
330
331
|
const source = JSON.parse(await readFile(path.join(BUNDLED_CODEX_PLUGIN_ROOT, relative), "utf8"));
|
|
331
|
-
const commandPrefix = "node ";
|
|
332
|
-
const runtime = shellQuote(process.execPath);
|
|
333
332
|
for (const groups of Object.values(source.hooks || {})) {
|
|
334
333
|
for (const group of groups || []) {
|
|
335
|
-
for (const hook of group.hooks || [])
|
|
336
|
-
if (typeof hook.command === "string" && hook.command.startsWith(commandPrefix)) {
|
|
337
|
-
hook.command = `${runtime} ${hook.command.slice(commandPrefix.length)}`;
|
|
338
|
-
}
|
|
339
|
-
}
|
|
334
|
+
for (const hook of group.hooks || []) hook.command = hookRuntimeCommand(hook.command);
|
|
340
335
|
}
|
|
341
336
|
}
|
|
342
337
|
return new Map([[relative, `${JSON.stringify(source, null, 2)}\n`]]);
|
|
343
338
|
}
|
|
344
339
|
|
|
345
|
-
function shellQuote(value) {
|
|
346
|
-
if (process.platform === "win32") return `"${value.replaceAll('"', '""')}"`;
|
|
347
|
-
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
348
|
-
}
|
|
349
|
-
|
|
350
340
|
async function directoryDigest(root, { missing = "throw", manifest, overrides = new Map() } = {}) {
|
|
351
341
|
let rootStat;
|
|
352
342
|
try {
|
package/src/sqm/claude-plugin.js
CHANGED
|
@@ -3,6 +3,7 @@ import { mkdir, readFile, readdir, rename, rm, lstat, writeFile } from "node:fs/
|
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { hookRuntimeCommand } from "../hook-runtime.js";
|
|
6
7
|
|
|
7
8
|
const SOURCE = fileURLToPath(new URL("../codex-plugins/simy/", import.meta.url));
|
|
8
9
|
export const CLAUDE_MARKETPLACE = "simy-local";
|
|
@@ -43,7 +44,7 @@ export async function claudePluginFiles() {
|
|
|
43
44
|
} else if (event === "Notification") {
|
|
44
45
|
hook.statusMessage = "SIMY Delivery Loop permission-notification tracking";
|
|
45
46
|
}
|
|
46
|
-
hook.command = hook.command
|
|
47
|
+
hook.command = hookRuntimeCommand(hook.command)
|
|
47
48
|
.replaceAll("process.env.PLUGIN_ROOT", "process.env.CLAUDE_PLUGIN_ROOT")
|
|
48
49
|
.replaceAll("hook.main('codex')", "hook.main('claude')");
|
|
49
50
|
}
|
|
@@ -136,6 +137,3 @@ function json(value) { return `${JSON.stringify(value, null, 2)}\n`; }
|
|
|
136
137
|
async function optionalRead(file) {
|
|
137
138
|
try { return await readFile(file, "utf8"); } catch (error) { if (error.code === "ENOENT") return null; throw error; }
|
|
138
139
|
}
|
|
139
|
-
function quoteRuntime(value) {
|
|
140
|
-
return process.platform === "win32" ? `"${value.replaceAll('"', '""')}"` : `'${value.replaceAll("'", "'\\''")}'`;
|
|
141
|
-
}
|
|
@@ -95,6 +95,7 @@ export function createWorkflowObservationSender({
|
|
|
95
95
|
let terminal = null;
|
|
96
96
|
let startPromise = null;
|
|
97
97
|
let admittedAtMs = null;
|
|
98
|
+
let submittedTerminalAt = null;
|
|
98
99
|
|
|
99
100
|
async function post(path, body, attempts) {
|
|
100
101
|
let response = null;
|
|
@@ -191,10 +192,13 @@ export function createWorkflowObservationSender({
|
|
|
191
192
|
// A very short provider turn can finish before the start RPC commits.
|
|
192
193
|
// For a new run, record when the terminal event is submitted after
|
|
193
194
|
// admission; the event itself remains the real provider event.
|
|
194
|
-
|
|
195
|
+
// The intake's immutable replay identity includes observed_at. Freeze it
|
|
196
|
+
// before the first send so another flush after a lost response (including
|
|
197
|
+
// concurrent flushes) resubmits the exact same event.
|
|
198
|
+
submittedTerminalAt ??= new Date(Math.max(now().getTime(), admittedAtMs + 1)).toISOString();
|
|
195
199
|
return sendTypedTerminal({
|
|
196
200
|
idempotencyKey: terminalIdempotencyKey(normalizedIntent, normalizedBinding),
|
|
197
|
-
observedAt,
|
|
201
|
+
observedAt: submittedTerminalAt,
|
|
198
202
|
});
|
|
199
203
|
}
|
|
200
204
|
|