@awak-app/simy-cli 0.7.36 → 0.7.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +36 -36
  2. package/package.json +1 -1
  3. package/src/agent.js +1 -1
  4. package/src/auto-update.js +25 -13
  5. package/src/backend-executable.js +1 -1
  6. package/src/cli-contract.js +1 -1
  7. package/src/cli-observability.js +2 -2
  8. package/src/codex-plugins/simy/.codex-plugin/plugin.json +5 -5
  9. package/src/codex-plugins/simy/hooks/README.md +3 -3
  10. package/src/codex-plugins/simy/hooks/delivery-loop.mjs +242 -26
  11. package/src/codex-plugins/simy/hooks/purpose-workspace.mjs +4 -1
  12. package/src/codex-plugins/simy/hooks/registered-requirement-checker.mjs +201 -0
  13. package/src/codex-plugins/simy/hooks/requirement-acceptance.mjs +69 -0
  14. package/src/codex-plugins/simy/skills/continue/SKILL.md +4 -4
  15. package/src/codex-plugins/simy/skills/continue/agents/openai.yaml +3 -3
  16. package/src/codex-plugins/simy/skills/diagnose/SKILL.md +2 -2
  17. package/src/codex-plugins/simy/skills/sqm-loop/SKILL.md +3 -3
  18. package/src/codex-plugins/simy/skills/sqm-loop/agents/openai.yaml +2 -2
  19. package/src/console/app.js +7 -7
  20. package/src/delivery-hooks/claude-workflow.md +1 -1
  21. package/src/delivery-hooks/command.js +5 -5
  22. package/src/delivery-hooks/supervisor-watchdog.js +3 -2
  23. package/src/desktop-host.js +53 -23
  24. package/src/index.js +1 -1
  25. package/src/local-task.js +1 -1
  26. package/src/orchestrator/presentation.js +1 -1
  27. package/src/runner.js +2 -2
  28. package/src/session-supervision/acceptance-reads.js +11 -2
  29. package/src/session-supervision/analysis-manager.js +9 -2
  30. package/src/session-supervision/automatic-continuation.js +11 -11
  31. package/src/session-supervision/catalog-refresh.js +16 -0
  32. package/src/session-supervision/claude-native-agent-contract.js +62 -0
  33. package/src/session-supervision/claude-outcome-review.js +7 -6
  34. package/src/session-supervision/claude-sessions.js +137 -76
  35. package/src/session-supervision/codex-app-server.js +3 -3
  36. package/src/session-supervision/codex-children.js +2 -2
  37. package/src/session-supervision/continuation-analysis.js +1 -1
  38. package/src/session-supervision/continuation-command.js +3 -3
  39. package/src/session-supervision/continuation-readiness.js +1 -1
  40. package/src/session-supervision/conversation-context.js +58 -26
  41. package/src/session-supervision/conversation-presentation.js +3 -3
  42. package/src/session-supervision/decision-presentation.js +8 -8
  43. package/src/session-supervision/discovery-window.js +8 -2
  44. package/src/session-supervision/index.js +38 -25
  45. package/src/session-supervision/purpose-verification.js +12 -3
  46. package/src/session-supervision/remote-errors.js +2 -2
  47. package/src/session-supervision/work-state.js +3 -3
  48. package/src/sqm/advisory-guidance.js +1 -0
  49. package/src/sqm/agent-skill.js +3 -3
  50. package/src/sqm/checkers.js +35 -1
  51. package/src/sqm/claude-plugin.js +8 -2
  52. package/src/sqm/hypothesis-knowledge.js +3 -3
  53. package/src/sqm/knowledge-recall.js +34 -12
  54. package/src/sqm/reasoned-assessment.js +22 -9
  55. package/src/update-detachment/runtime.js +2 -1
@@ -0,0 +1,201 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { cp, lstat, mkdir, open, readFile, realpath, rm, stat, writeFile } from 'node:fs/promises';
3
+ import { isAbsolute, join, relative, resolve } from 'node:path';
4
+ import { spawn } from 'node:child_process';
5
+ import { normalizeRequirementContract } from './requirement-acceptance.mjs';
6
+ import { workspaceIdentity } from './purpose-workspace.mjs';
7
+
8
+ const handles = new WeakMap();
9
+ const observers = new WeakSet();
10
+ const digest = bytes => createHash('sha256').update(bytes).digest('hex');
11
+ const objectiveIdentity = objective => digest(typeof objective === 'string' ? objective.trim().slice(0, 4000) : '');
12
+ const sha = value => /^[a-f0-9]{64}$/.test(value || '');
13
+ const inside = (parent, path) => { const rel = relative(parent, path); return !rel || !rel.startsWith('..') && !isAbsolute(rel); };
14
+ const identity = info => Object.fromEntries(['dev', 'ino', 'mode', 'size', 'mtimeMs', 'ctimeMs'].map(key => [key, info[key]]));
15
+ const same = (left, right) => JSON.stringify(left) === JSON.stringify(right);
16
+ const registrations = root => join(root, 'host-requirement-checkers');
17
+ const registrationPath = (root, id) => join(registrations(root), `${id}.json`);
18
+
19
+ async function readJSON(path, limit = 128 * 1024) {
20
+ if ((await stat(path)).size > limit) throw new Error('Checker document exceeds its bound.');
21
+ return JSON.parse(await readFile(path, 'utf8'));
22
+ }
23
+ async function pinFile(file, cwd) {
24
+ if (!isAbsolute(file?.path) || !sha(file.sha256)) throw new Error('Checker inputs require absolute paths and SHA-256 pins.');
25
+ const path = await realpath(file.path);
26
+ if (inside(cwd, path)) throw new Error('Checker inputs must be independent of the candidate workspace.');
27
+ const before = await lstat(path);
28
+ if (!before.isFile() || digest(await readFile(path)) !== file.sha256 || !same(identity(before), identity(await lstat(path))))
29
+ throw new Error('Pinned checker input changed.');
30
+ return { path, sha256: file.sha256, identity: identity(before) };
31
+ }
32
+
33
+ // Host-only enrollment capability. JSON read from managed-file/checkpoint cannot
34
+ // recreate a WeakMap handle. This is not an OS sandbox against same-user Bash.
35
+ export async function registerRequirementChecker({ root, provider, session, cwd, objective, publicRequest,
36
+ command, runtime, requirements, timeoutMs = 140_000, cancellationGraceMs = 1000 }) {
37
+ if (provider !== 'claude') throw new Error('Registered requirement execution currently supports the Claude live-hook channel only.');
38
+ cwd = await realpath(cwd);
39
+ root = resolve(root);
40
+ if (inside(cwd, root)) throw new Error('Host checker state must be outside the task workspace.');
41
+ await mkdir(registrations(root), { recursive: true });
42
+ root = await realpath(root);
43
+ if (inside(cwd, root)) throw new Error('Host checker state resolves inside the task workspace.');
44
+ if (typeof objective !== 'string' || !objective.trim() || typeof session !== 'string' || !session ||
45
+ !Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 180_000 ||
46
+ !Number.isSafeInteger(cancellationGraceMs) || cancellationGraceMs < 1000 || cancellationGraceMs > 45_000 ||
47
+ !runtime || !sha(runtime.digest) || !['public-docker-v1', 'deterministic-fixture'].includes(runtime.profile) ||
48
+ !isAbsolute(command?.executable) || !Array.isArray(command.args) || command.args.some(arg => typeof arg !== 'string') ||
49
+ !Array.isArray(command.pins) || !command.pins.length || command.pins.length > 100)
50
+ throw new Error('Invalid host checker registration.');
51
+ const pins = await Promise.all(command.pins.map(file => pinFile(file, cwd)));
52
+ if (new Set(pins.map(pin => pin.path)).size !== pins.length) throw new Error('Duplicate checker input.');
53
+ const executable = await realpath(command.executable);
54
+ if (!pins.some(pin => pin.path === executable)) throw new Error('Checker executable must be pinned.');
55
+ const request = await pinFile(publicRequest, cwd);
56
+ const environment = command.env || {};
57
+ if (Object.entries(environment).some(([key, value]) => !/^[A-Z][A-Z0-9_]*$/.test(key) || typeof value !== 'string' ||
58
+ /^(NODE_OPTIONS|NODE_PATH|PYTHONPATH|PYTHONHOME|LD_|DYLD_)/.test(key))) throw new Error('Invalid fixed checker environment.');
59
+ const checker = { command: { executable, args: [...command.args], env: { ...environment } }, pins,
60
+ runtime: structuredClone(runtime), timeoutMs, cancellationGraceMs };
61
+ const contract = normalizeRequirementContract({ schemaVersion: 1, checkVersion: `registered-${digest(JSON.stringify(checker))}`, requirements });
62
+ const spec = { schemaVersion: 1, id: randomUUID(), provider, session, cwd, objectiveHash: objectiveIdentity(objective), publicRequest: request, checker, contract };
63
+ const bytes = JSON.stringify(spec), binding = { id: spec.id, digest: digest(bytes) };
64
+ await writeFile(registrationPath(root, spec.id), bytes, { flag: 'wx', mode: 0o600 });
65
+ await mkdir(join(registrations(root), 'executions'), { recursive: true });
66
+ const handle = Object.freeze({ contract: structuredClone(contract) });
67
+ handles.set(handle, { root, spec, binding });
68
+ return handle;
69
+ }
70
+
71
+ export async function bindRequirementChecker(handle, { root, provider, session, cwd, objective }) {
72
+ const registration = handles.get(handle);
73
+ if (!registration || registration.root !== await realpath(root) || registration.spec.provider !== provider ||
74
+ registration.spec.session !== session || registration.spec.cwd !== await realpath(cwd) ||
75
+ registration.spec.objectiveHash !== objectiveIdentity(objective)) throw new Error('A matching host enrollment capability is required.');
76
+ await loadRegistration(root, registration.binding, true);
77
+ return { requirementChecker: { ...registration.binding }, requirementContract: structuredClone(registration.spec.contract) };
78
+ }
79
+
80
+ async function loadRegistration(root, binding, full = false) {
81
+ if (!/^[a-f0-9-]{36}$/.test(binding?.id || '') || !sha(binding?.digest)) throw new Error('Invalid enrolled checker binding.');
82
+ const bytes = await readFile(registrationPath(root, binding.id), 'utf8');
83
+ if (Buffer.byteLength(bytes) > 128 * 1024 || digest(bytes) !== binding.digest) throw new Error('Host checker registration changed.');
84
+ const spec = JSON.parse(bytes);
85
+ for (const file of [...spec.checker.pins, spec.publicRequest]) {
86
+ if (!same(identity(await lstat(file.path)), file.identity) ||
87
+ full && digest(await readFile(file.path)) !== file.sha256) throw new Error('Pinned checker input changed.');
88
+ }
89
+ return spec;
90
+ }
91
+
92
+ // Stop only checks the enrollment and inexpensive source metadata. Actual
93
+ // execution validates the complete pinned bytes both before and afterwards.
94
+ export async function registeredCheckerCurrent(root, binding) {
95
+ try { await loadRegistration(root, binding); return true; } catch { return false; }
96
+ }
97
+
98
+ export function receiptFromCaseReport(contract, candidateHash, report, evidenceRef) {
99
+ const expected = contract.requirements.flatMap(row => row.assertionIds);
100
+ const cases = report?.cases;
101
+ const complete = report?.schemaVersion === 1 && report.execution?.status === 'completed' && Array.isArray(cases) &&
102
+ cases.length === expected.length && new Set(cases.map(row => row?.id)).size === expected.length &&
103
+ cases.every(row => expected.includes(row?.id) && ['passed', 'failed'].includes(row.status));
104
+ return { schemaVersion: 1, contractHash: contract.contractHash, checkVersion: contract.checkVersion, candidateHash,
105
+ execution: { status: complete ? 'completed' : 'infrastructure_error', assertionCount: complete ? cases.length : 0 },
106
+ observations: complete ? contract.requirements.map(requirement => ({ requirementId: requirement.id,
107
+ status: cases.some(row => requirement.assertionIds.includes(row.id) && row.status === 'failed') ? 'failed' : 'verified',
108
+ assertionIds: requirement.assertionIds, evidenceRefs: [evidenceRef] })) : [] };
109
+ }
110
+
111
+ async function runProcess(spec, request, directory, signal) {
112
+ const stdout = await open(join(directory, 'stdout.log'), 'wx', 0o600);
113
+ const stderr = await open(join(directory, 'stderr.log'), 'wx', 0o600);
114
+ try {
115
+ signal.throwIfAborted();
116
+ const child = spawn(spec.command.executable, spec.command.args, {
117
+ cwd: directory, env: spec.command.env, stdio: ['pipe', stdout.fd, stderr.fd],
118
+ });
119
+ let killTimer;
120
+ const abort = () => { child.kill('SIGTERM'); killTimer ??= setTimeout(() => child.kill('SIGKILL'), spec.cancellationGraceMs ?? 1000); };
121
+ let inspecting = false;
122
+ const outputLimit = setInterval(async () => {
123
+ if (inspecting) return;
124
+ inspecting = true;
125
+ try { if ((await stdout.stat()).size + (await stderr.stat()).size > 8 * 1024 * 1024) abort(); }
126
+ catch { abort(); } finally { inspecting = false; }
127
+ }, 100);
128
+ const completion = new Promise(accept => {
129
+ let spawnError;
130
+ child.once('error', error => { spawnError = error; });
131
+ child.once('close', (code, terminationSignal) => accept({ code, signal: terminationSignal, ...(spawnError ? { error: spawnError.message } : {}) }));
132
+ });
133
+ signal.addEventListener('abort', abort, { once: true });
134
+ if (signal.aborted) abort();
135
+ child.stdin.on('error', () => {});
136
+ child.stdin.end(JSON.stringify(request));
137
+ try {
138
+ await writeFile(join(directory, 'process.json'), JSON.stringify({ pid: child.pid, launchedAt: new Date().toISOString() }), { flag: 'wx' });
139
+ return await completion;
140
+ } catch (error) { abort(); await completion; throw error;
141
+ } finally { clearTimeout(killTimer); clearInterval(outputLimit); signal.removeEventListener('abort', abort); }
142
+ } finally { await stdout.close(); await stderr.close(); }
143
+ }
144
+
145
+ export function registeredRequirementObserver({ root, binding, provider, session, runId, signal: outerSignal }) {
146
+ const observer = async ({ cwd, contract, candidateHash, signal = outerSignal }) => {
147
+ const spec = await loadRegistration(root, binding, true);
148
+ if (spec.provider !== provider || spec.session !== session || spec.cwd !== await realpath(cwd) ||
149
+ spec.contract.contractHash !== contract.contractHash) throw new Error('Registered checker target changed.');
150
+ const executions = join(registrations(root), 'executions', digest(JSON.stringify([binding, provider, session, runId, candidateHash])));
151
+ // Atomic one-use claim also covers a cold checkpoint subprocess. An unknown
152
+ // process is never replayed; a completed report has an explicit reuse link.
153
+ try { await mkdir(executions); }
154
+ catch (error) {
155
+ if (error.code !== 'EEXIST') throw error;
156
+ const result = await readJSON(join(executions, 'result.json'));
157
+ if (result.bindingDigest !== binding.digest || result.candidateHash !== candidateHash || result.status !== 'complete' || result.snapshotRemoved !== true)
158
+ throw new Error('Previous checker execution is unresolved; do not replay.');
159
+ const bytes = await readFile(join(executions, 'cases.json'));
160
+ if (digest(bytes) !== result.reportHash) throw new Error('Previous checker report changed.');
161
+ return receiptFromCaseReport(contract, candidateHash, JSON.parse(bytes), join(executions, 'result.json'));
162
+ }
163
+ await writeFile(join(executions, 'intent.json'), JSON.stringify({ binding, provider, session, runId, candidateHash,
164
+ createdAt: new Date().toISOString() }), { flag: 'wx' });
165
+ const controller = new AbortController();
166
+ const abort = () => controller.abort(signal.reason);
167
+ signal?.addEventListener('abort', abort, { once: true });
168
+ if (signal?.aborted) abort();
169
+ const timeout = setTimeout(() => controller.abort(new Error('Checker deadline exceeded.')), spec.checker.timeoutMs);
170
+ const snapshot = join(executions, 'candidate');
171
+ try {
172
+ const filter = path => !relative(cwd, path).split(/[\\/]/).some(part => ['.git', 'node_modules', '.npm-cache'].includes(part));
173
+ await cp(cwd, snapshot, { recursive: true, dereference: false, filter, preserveTimestamps: true });
174
+ if (await workspaceIdentity(snapshot, undefined, undefined, { signal: controller.signal }) !== candidateHash ||
175
+ await workspaceIdentity(cwd, root, undefined, { signal: controller.signal }) !== candidateHash) throw new Error('Candidate changed while freezing checker input.');
176
+ const process = await runProcess(spec.checker, { schemaVersion: 1, candidateHash, cwd: snapshot,
177
+ reportPath: join(executions, 'cases.json'), contract: spec.contract, runtime: spec.checker.runtime }, executions, controller.signal);
178
+ await loadRegistration(root, binding, true);
179
+ if (controller.signal.aborted || process.signal || process.error || ![0, 1].includes(process.code) ||
180
+ await workspaceIdentity(snapshot) !== candidateHash) throw new Error('Checker process or immutable input is unresolved.');
181
+ const report = await readJSON(join(executions, 'cases.json'));
182
+ const receipt = receiptFromCaseReport(contract, candidateHash, report, join(executions, 'result.json'));
183
+ if (receipt.execution.status !== 'completed' || process.code === 1 && !report.cases.some(row => row.status === 'failed')) throw new Error('Checker did not execute the complete required case set.');
184
+ await rm(snapshot, { recursive: true });
185
+ await writeFile(join(executions, 'result.json'), JSON.stringify({ status: 'complete', bindingDigest: binding.digest,
186
+ candidateHash, process, snapshotRemoved: true, reportHash: digest(await readFile(join(executions, 'cases.json'))) }), { flag: 'wx' });
187
+ return receipt;
188
+ } catch (error) {
189
+ await writeFile(join(executions, 'error.json'), JSON.stringify({ status: 'unresolved', message: error.message }), { flag: 'wx' });
190
+ throw error;
191
+ } finally {
192
+ clearTimeout(timeout); signal?.removeEventListener('abort', abort);
193
+ // Only our copied input, never the original workspace or Docker resources.
194
+ await rm(snapshot, { recursive: true, force: true });
195
+ }
196
+ };
197
+ observers.add(observer);
198
+ return observer;
199
+ }
200
+
201
+ export const isRegisteredRequirementObserver = observer => observers.has(observer);
@@ -0,0 +1,69 @@
1
+ import { createHash } from 'node:crypto';
2
+
3
+ const hash = value => createHash('sha256').update(JSON.stringify(value)).digest('hex');
4
+ const identifier = value => typeof value === 'string' && /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,119}$/.test(value);
5
+ const strings = (values, maximum) => Array.isArray(values) && values.length > 0 && values.length <= maximum &&
6
+ values.every(identifier) && new Set(values).size === values.length;
7
+
8
+ // This opt-in contract is frozen at enrollment. Receipts cannot redefine its
9
+ // requirements or replace a broad behavior requirement with a literal PASS file.
10
+ export function normalizeRequirementContract(input) {
11
+ if (input?.schemaVersion !== 1 || !identifier(input.checkVersion) || !Array.isArray(input.requirements) ||
12
+ !input.requirements.length || input.requirements.length > 20) throw new Error('Invalid behavior requirement contract.');
13
+ const requirements = input.requirements.map(row => {
14
+ if (!identifier(row?.id) || typeof row.description !== 'string' || !row.description.trim() || row.description.length > 500 ||
15
+ !strings(row.assertionIds, 100)) throw new Error('Each behavior requirement needs an ID, description and distinct assertions.');
16
+ return { id: row.id, description: row.description, assertionIds: [...row.assertionIds].sort() };
17
+ }).sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
18
+ if (new Set(requirements.map(row => row.id)).size !== requirements.length) throw new Error('Duplicate behavior requirement.');
19
+ const assertionIds = requirements.flatMap(row => row.assertionIds);
20
+ if (new Set(assertionIds).size !== assertionIds.length) throw new Error('Behavior assertion IDs must be unique across requirements.');
21
+ const contract = { schemaVersion: 1, checkVersion: input.checkVersion, requirements };
22
+ const contractHash = hash(contract);
23
+ if (input.contractHash !== undefined && input.contractHash !== contractHash) throw new Error('Behavior contract digest changed.');
24
+ return { ...contract, contractHash };
25
+ }
26
+
27
+ export function assessRequirementReceipt(contractInput, receipt, candidateHash) {
28
+ const contract = normalizeRequirementContract(contractInput);
29
+ const unknown = detail => contract.requirements.map(row => ({ requirementId: row.id, status: 'unknown', detail }));
30
+ if (!receipt || receipt.schemaVersion !== 1 || receipt.contractHash !== contract.contractHash ||
31
+ receipt.checkVersion !== contract.checkVersion || receipt.candidateHash !== candidateHash ||
32
+ !/^[a-f0-9]{64}$/.test(candidateHash || '')) return unknown('Missing or stale independently bound behavior observation.');
33
+ if (receipt.execution?.status !== 'completed' || !Number.isSafeInteger(receipt.execution.assertionCount) ||
34
+ receipt.execution.assertionCount <= 0) return unknown('Behavior observation did not execute assertions; infrastructure is unresolved.');
35
+ if (!Array.isArray(receipt.observations) || receipt.observations.length > 20) return unknown('Invalid behavior observation rows.');
36
+ const ids = receipt.observations.map(row => row?.requirementId);
37
+ if (new Set(ids).size !== ids.length || ids.some(id => !contract.requirements.some(row => row.id === id)))
38
+ return unknown('Duplicate or unexpected behavior observation rows.');
39
+ let observedCount = 0;
40
+ const rows = contract.requirements.map(requirement => {
41
+ const row = receipt.observations.find(item => item.requirementId === requirement.id);
42
+ if (!row || !['verified', 'failed', 'unknown'].includes(row.status) || !strings(row.assertionIds, 100) ||
43
+ !Array.isArray(row.evidenceRefs) || !row.evidenceRefs.length || row.evidenceRefs.length > 20 ||
44
+ row.evidenceRefs.some(ref => typeof ref !== 'string' || !ref.trim() || ref.length > 1000) ||
45
+ row.assertionIds.some(id => !requirement.assertionIds.includes(id)))
46
+ return { requirementId: requirement.id, status: 'unknown', detail: 'Requirement has no usable assertion/evidence observation.' };
47
+ observedCount += row.assertionIds.length;
48
+ if (row.status === 'verified' && requirement.assertionIds.some(id => !row.assertionIds.includes(id)))
49
+ return { requirementId: requirement.id, status: 'unknown', detail: 'Only part of the required behavior was observed.' };
50
+ return { requirementId: requirement.id, status: row.status,
51
+ detail: row.status === 'failed' ? 'Executed behavior assertion failed.' : row.status === 'unknown' ?
52
+ 'Behavior remains unresolved.' : 'All required behavior assertions were independently observed.' };
53
+ });
54
+ return receipt.execution.assertionCount < observedCount ? unknown('Assertion count does not bind the observed rows.') : rows;
55
+ }
56
+
57
+ export function pendingRequirements(managed) {
58
+ if (!managed?.requirementContract) return [];
59
+ let contract;
60
+ try { contract = normalizeRequirementContract(managed.requirementContract); }
61
+ catch { return [{ id: 'behavior-contract', description: 'Frozen behavior requirements', check: 'behavior', status: 'unknown', detail: 'Behavior contract changed; inspect enrollment.' }]; }
62
+ const observation = managed.requirementObservation;
63
+ const rows = observation?.source === 'host_requirement_observer' && observation.current === true
64
+ ? assessRequirementReceipt(contract, observation.receipt, observation.candidateHash)
65
+ : contract.requirements.map(row => ({ requirementId: row.id, status: 'unknown', detail: 'Current host behavior observation is missing.' }));
66
+ return rows.filter(row => row.status !== 'verified').map(row => ({ id: `behavior:${row.requirementId}`,
67
+ description: contract.requirements.find(item => item.id === row.requirementId).description,
68
+ check: 'behavior', status: row.status, detail: row.detail }));
69
+ }
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: continue
3
- description: Start SIMY Auto Continue for the original conversation when the user explicitly asks SIMY or @simy to take over or continue unfinished work. Also read its real execution status or turn automatic continuation off. This is a controller entry, not an independent delivery agent or an SQM check.
3
+ description: Turn on SIMY Delivery Loop (Continue automatically) for the original conversation when the user explicitly asks SIMY or @simy to take over or continue unfinished work. Also read its real execution status or turn Delivery Loop off for this conversation. This is a controller entry, not an independent delivery agent or an SQM check.
4
4
  ---
5
5
 
6
6
  # Continue the original conversation with SIMY
@@ -32,8 +32,8 @@ existing conversation reference before changing anything.
32
32
  Use this command on the original local host. Preserve a remote selection's host
33
33
  and use its existing SIMY remote controls instead of treating it as local.
34
34
 
35
- This command delegates to the running SIMY app's existing Auto Continue control
36
- and background coordinator. It preserves the original goal, task history,
35
+ This command delegates to the running SIMY app's existing Delivery Loop control
36
+ (the Continue automatically switch) and background coordinator. It preserves the original goal, task history,
37
37
  corrections, permissions, failure budgets and approvals. It does not start a
38
38
  second provider process, create a replacement task, change accounts or models,
39
39
  trust hooks, or grant native permissions. The original current turn continues
@@ -51,7 +51,7 @@ result and acceptance. Continue already-authorized work in this conversation.
51
51
  To inspect again, use the same exact target with `auto-continue status` in place
52
52
  of `auto-continue on`. If the response was lost or status reading failed after
53
53
  saving, query status; never automatically repeat a mutation to infer success.
54
- When asked to turn automatic continuation off, use `auto-continue off` for the
54
+ When asked to turn Delivery Loop off, use `auto-continue off` for the
55
55
  same target. OFF preserves the current native turn; an explicit stop uses the
56
56
  existing task Stop control. A stopped, rejected, archived, settled, disconnected
57
57
  or unavailable task stays within its existing boundary: report the returned
@@ -1,6 +1,6 @@
1
1
  interface:
2
- display_name: "SIMY Auto Continue"
3
- short_description: "Continue the original task through SIMY"
4
- default_prompt: "Use $simy:continue to enable SIMY Auto Continue for this conversation and report its actual work state."
2
+ display_name: "SIMY Continue automatically"
3
+ short_description: "Turn on Delivery Loop for this conversation"
4
+ default_prompt: "Use $simy:continue to turn on SIMY Delivery Loop for this conversation and report its actual work state."
5
5
  policy:
6
6
  allow_implicit_invocation: true
@@ -1,9 +1,9 @@
1
1
  ---
2
2
  name: diagnose
3
- description: Diagnose a stopped or unresponsive SIMY task when the user says “SIMY CLIに相談して”, asks why it stopped, or requests manual SIMY diagnosis. Distinguish read-only diagnosis from an explicit request to repair and continue the original work.
3
+ description: Check why it stopped (停止理由の確認) for a stopped or unresponsive SIMY task when the user says “SIMY CLIに相談して” or asks why it stopped. Distinguish a read-only check from an explicit request to repair and continue the original work. Environment diagnosis is `simy doctor`, not this entry.
4
4
  ---
5
5
 
6
- # Consult SIMY CLI
6
+ # Check why it stopped (停止理由の確認)
7
7
 
8
8
  Use this entry for an explicit consultation, diagnosis, or repair-and-continue request, independently of Delivery Loop enrollment or permission to resume. Prefer `$SIMY_HOME/bin/simy` or `$HOME/.simy/bin/simy`. Inspect `--version` and `delivery --help` to confirm `consult` is available. If only `diagnose` exists, use the legacy command below for diagnosis and report repair as unavailable; never reinterpret a failed consult invocation as execution. An older installed CLI is a compatibility gap, never permission to switch its live runtime.
9
9
 
@@ -1,9 +1,9 @@
1
1
  ---
2
2
  name: sqm-loop
3
- description: Run the SIMY Software Quality Monitor against the repository and changes discussed in the current Codex thread. Use when the user asks SIMY to start SQM, quality-check the current work, or prepare a PR quality proof.
3
+ description: Run a SIMY SQM check on the repository and changes discussed in the current Codex thread, on request. Use when the user asks SIMY to start SQM or quality-check the current work now. The opted-in pre-PR gate is the separate simy-sqm-pr-check skill.
4
4
  ---
5
5
 
6
- # SIMY SQM Loop
6
+ # SIMY SQM check
7
7
 
8
8
  Treat the current Codex thread, its attachments, and the active repository as the request context. Convert that context into explicit quality expectations, then verify the actual repository state; do not merely summarize the conversation.
9
9
 
@@ -22,7 +22,7 @@ Investigate and repair authorized reversible setup; preserve real account,
22
22
  organization, device and provider authority. Do not guess credentials or invent a
23
23
  successful scope. A connected scope is not quality acceptance.
24
24
 
25
- ## Run the quality loop
25
+ ## Run the SQM check
26
26
 
27
27
  1. Identify the repository root and the user-visible behavior or incident discussed in the current thread.
28
28
  2. Preserve unrelated user changes and follow repository instructions.
@@ -1,6 +1,6 @@
1
1
  interface:
2
- display_name: "SIMY SQM Loop"
3
- short_description: "Run SQM from the current Codex thread"
2
+ display_name: "SIMY SQM check"
3
+ short_description: "Run an SQM check on the current work"
4
4
  default_prompt: "Use $sqm-loop to quality-check the work in this thread."
5
5
  policy:
6
6
  allow_implicit_invocation: true
@@ -176,7 +176,7 @@ export function AgenticLoopConsole({ agent, onQuit = () => {}, remoteConnections
176
176
  }
177
177
  if (!selectedRun) {
178
178
  if (command.type !== "guidance") {
179
- setNotice("That command requires a selected Agentic Loop run.");
179
+ setNotice("That command requires a selected Delivery Loop run.");
180
180
  return;
181
181
  }
182
182
  setBusy(true);
@@ -213,12 +213,12 @@ export function AgenticLoopConsole({ agent, onQuit = () => {}, remoteConnections
213
213
  setNotice("Guidance queued for the next executor handoff.");
214
214
  } else {
215
215
  await agent.controls.continue(selectedRun, command.message);
216
- setNotice("Human guidance accepted. The agentic loop is continuing.");
216
+ setNotice("Human guidance accepted. The Delivery Loop is continuing.");
217
217
  }
218
218
  break;
219
219
  case "decision":
220
220
  await agent.controls.applyDecision(selectedRun, command);
221
- setNotice("Human decision recorded. The agentic loop is continuing.");
221
+ setNotice("Human decision recorded. The Delivery Loop is continuing.");
222
222
  break;
223
223
  case "recheck":
224
224
  await agent.controls.recheck(selectedRun);
@@ -297,7 +297,7 @@ export function AgenticLoopConsole({ agent, onQuit = () => {}, remoteConnections
297
297
  await agent.controls.selectRepository(selectedRun, match);
298
298
  setDetailOpen(true);
299
299
  setDetailScrollFromEnd(0);
300
- setNotice(`Authorized ${match.repository}. The same Agentic Loop run is continuing.`);
300
+ setNotice(`Authorized ${match.repository}. The same Delivery Loop run is continuing.`);
301
301
  } else if (selectedRun) {
302
302
  setNotice(
303
303
  `${selectedRun.request.repository} was not found under ${result.root}. Run /scan <path> for another authorized root.`,
@@ -328,7 +328,7 @@ export function AgenticLoopConsole({ agent, onQuit = () => {}, remoteConnections
328
328
  setRepositoryPicker(null);
329
329
  setDetailOpen(true);
330
330
  setDetailScrollFromEnd(0);
331
- setNotice(`Authorized ${selected.repository}. The same Agentic Loop run is continuing.`);
331
+ setNotice(`Authorized ${selected.repository}. The same Delivery Loop run is continuing.`);
332
332
  } else {
333
333
  const verified = await agent.controls.selectRepository(null, selected);
334
334
  setDraft((current) => ({
@@ -510,7 +510,7 @@ export function AgenticLoopConsole({ agent, onQuit = () => {}, remoteConnections
510
510
  if (confirmStop) void execute("/stop");
511
511
  else {
512
512
  setConfirmStop(true);
513
- setNotice("Press x again to stop the selected Agentic Loop run.");
513
+ setNotice("Press x again to stop the selected Delivery Loop run.");
514
514
  }
515
515
  return;
516
516
  }
@@ -641,7 +641,7 @@ function NewTask({ draft, loginUrl, width }) {
641
641
  dimColor: true,
642
642
  wrap: "wrap",
643
643
  children:
644
- "Describe the outcome. SIMY will choose a one-time Local Task or an Agentic Loop.",
644
+ "Describe the outcome. SIMY will choose a one-time Local Task or a Delivery Loop.",
645
645
  }),
646
646
  jsx(Text, { children: " " }),
647
647
  jsx(Text, {
@@ -44,7 +44,7 @@ resume or reset a task. Prompt hooks enroll eligible conversations under CLI aut
44
44
  Use when the user requests this skill or SIMY Delivery Loop, or current hook context confirms automatic enrollment. Preserve that existing loop and proceed to checkpoints without starting again. Installing this plugin does not opt a task in. Do not run SQM unless separately requested.
45
45
 
46
46
  1. Resolve the exact original provider and session ID from the SIMY hook context or verified existing session admission. If no valid connection exists, report the observed missing capability and the smallest confirmed connection step. Never invent a session ID, change hook trust, bypass permissions, or replace the conversation. Codex settings can be opened at `codex://settings`; a hooks subpage deep link is not guaranteed.
47
- 2. Only start or resume a Delivery Loop when the user requests it. For a new regular Loop, use `simy delivery start --provider <provider> --session <exact-session-id> --objective '<user outcome and acceptance criteria>' --next-step '<concrete next action>'`. For a Loop that must track acceptance, use the existing managed specification with `--managed-file <task.json>` as described below: freeze the original user outcome, selected delivery level when one exists, and necessary applicable acceptance criteria before work. A regular start can only end as `reported_done`; do not present it as a fully verified Agentic Loop. A verified original `codex_app_server` admission and existing user continuation delegation can use `simy delivery continue --provider codex --session <exact-original-session-id>` without SessionStart when the user requests continuation. Preserve the original objective and acceptance. Never start another loop to reset limits or resume user-stopped work without a user request. Authorized automatic recovery from ordinary blockers is distinct from resuming user-stopped work.
47
+ 2. Only start or resume a Delivery Loop when the user requests it. For a new regular Loop, use `simy delivery start --provider <provider> --session <exact-session-id> --objective '<user outcome and acceptance criteria>' --next-step '<concrete next action>'`. For a Loop that must track acceptance, use the existing managed specification with `--managed-file <task.json>` as described below: freeze the original user outcome, selected delivery level when one exists, and necessary applicable acceptance criteria before work. A regular start can only end as `reported_done`; do not present it as a fully verified managed Delivery Loop. A verified original `codex_app_server` admission and existing user continuation delegation can use `simy delivery continue --provider codex --session <exact-original-session-id>` without SessionStart when the user requests continuation. Preserve the original objective and acceptance. Never start another loop to reset limits or resume user-stopped work without a user request. Authorized automatic recovery from ordinary blockers is distinct from resuming user-stopped work.
48
48
 
49
49
  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.
50
50
 
@@ -225,18 +225,18 @@ export async function runDeliveryCommand(argv, { root = rootDirectory(), cwd = p
225
225
  }
226
226
  if (action === 'activity') return runActivityCommand(args, { root, output });
227
227
  if (!action || action === '--help') {
228
- output('simy delivery auto-continue on|off|status --provider codex|claude --session UUID — use the running app’s Auto Continue control and read its actual work state.');
229
- output('simy delivery observability status — show the state-only telemetry status for this device. The single default-OFF consent checkbox is in SIMY settings; disabling it stops uploads and discards pending telemetry. It never enables Auto-continue or starts a task.');
228
+ output('simy delivery auto-continue on|off|status --provider codex|claude --session UUID — Continue this conversation automatically: turns Delivery Loop on or off for one exact conversation through the running app’s existing control, and reads its actual work state.');
229
+ output('simy delivery observability status — show the state-only telemetry status for this device. The single default-OFF consent checkbox is in SIMY settings; disabling it stops uploads and discards pending telemetry. It never turns on Delivery Loop or starts a task.');
230
230
  output('simy delivery followup --provider codex --session ID --previous-run-id ID --objective TEXT — explicitly supervise the current goal after an exact finished run. A new goal archives the prior run; the same ordinary reported_done goal retains its identity and budgets for read-only reconciliation. Uses the existing supervisor; never reopens stopped, failed or budget-exhausted work.');
231
- output('simy delivery auto on|off|status --provider codex|claude — inspect or override CLI automatic start for this project directory (on by default). The next nonempty prompt in a conversation without a loop starts checkpoint tracking. Existing loops are preserved; provider hooks must already be enabled.');
231
+ output('simy delivery auto on|off|status --provider codex|claude — Start Delivery Loop automatically in this workspace: inspect or override automatic start for future conversations in this project directory (on by default). The next nonempty prompt in a conversation without a Delivery Loop starts checkpoint tracking. Existing loops are preserved; provider hooks must already be enabled.');
232
232
  output('simy delivery redelegate-native --provider codex --session ID --task-id ID --consent-ref REF — explicitly delegate current native local edit/test permissions to an exact legacy task. Preserves enrollment, history, budgets and stops; does not resume or grant external actions.');
233
233
  output('simy delivery runtime install|status|workflows — install an immutable local candidate, inspect its version/digest, or refresh existing plugin sources/cache while preserving enabled state and hook trust. Never enables a task.');
234
234
  output('simy delivery runtime doctor|desktop-install|desktop-activate|watch — audit live host/state/monitor alignment, or prepare a separate local desktop using the verified fixed runtime. Does not start or resume tasks.');
235
235
  output('simy delivery runtime policy-adopt [--task-id ID [--criterion-id ID]] — explicitly delegate decisions within existing permissions. Manual acceptance stays manual unless the user names the exact task and criterion to delegate its verifier.');
236
236
  output('simy delivery link_parent --provider codex|claude --session CHILD --parent-provider codex|claude --parent-session PARENT — record an explicit observed navigation relationship without starting work.');
237
237
  output('simy delivery supervise [--once] — service already opted-in tasks with this runtime; refuses an active desktop host.');
238
- output('simy delivery consult [--scope diagnose|repair_continue] [--request-id USER_TURN_ID] [--context-file context.json] [--provider codex|claude --session ID | --task-id ID] — use observed current conversation or selected original work; repair scope never implies completion.');
239
- output('simy delivery diagnose --provider codex|claude --session ID [--task-id ID] [--mode analyze|evidence] [--issues-file issues.json] [--detail brief|full] — advisory diagnosis in a separate process; never resumes the target. Exact --task-id alone is also supported.');
238
+ output('simy delivery consult [--scope diagnose|repair_continue] [--request-id USER_TURN_ID] [--context-file context.json] [--provider codex|claude --session ID | --task-id ID] — diagnose scope: check why it stopped, using the observed current conversation or selected original work; repair_continue scope never implies completion.');
239
+ output('simy delivery diagnose --provider codex|claude --session ID [--task-id ID] [--mode analyze|evidence] [--issues-file issues.json] [--detail brief|full] — advisory check of why it stopped, in a separate process; never resumes the target. Exact --task-id alone is also supported.');
240
240
  output('simy delivery evidence --provider codex|claude --session ID — read-only execution and acceptance receipts.');
241
241
  output('simy delivery activity [--watch] [--json] [--provider codex|claude] [--session ID] — local observations every 60 seconds; not productive time.');
242
242
  output('A task stops after repeated failures, and pauses for review when no progress is confirmed. Resume starts a new progress window.');
@@ -20,12 +20,13 @@ async function verifiedPin(root) {
20
20
  return { digest: pin.digest, release: pin.release };
21
21
  }
22
22
  export async function setSupervisorWatchdog({ root, enabled, webOrigin,
23
- inspectPin = verifiedPin, now = Date.now(), expectedEpoch, target } = {}) {
23
+ inspectPin = verifiedPin, now = Date.now(), expectedEpoch, target,
24
+ identity = supervisorProcessIdentity } = {}) {
24
25
  if (target && !enabled) throw new Error('A recovery target requires explicit enable.');
25
26
  const binding = target ? await supervisorTarget(root, target) : null;
26
27
  const pin = enabled ? await inspectPin(root) : null;
27
28
  const lock = await acquireDesktopProcessLock(join(folder(root), 'control-lock'), {
28
- alive: supervisorProcessAlive, identity: supervisorProcessIdentity });
29
+ alive: supervisorProcessAlive, identity });
29
30
  try {
30
31
  const previous = await readSupervisorState(controlPath(root));
31
32
  if (expectedEpoch !== undefined && previous?.epoch !== expectedEpoch) return previous;