@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.
- package/README.md +36 -36
- package/package.json +1 -1
- package/src/agent.js +1 -1
- package/src/auto-update.js +25 -13
- package/src/backend-executable.js +1 -1
- package/src/cli-contract.js +1 -1
- package/src/cli-observability.js +2 -2
- package/src/codex-plugins/simy/.codex-plugin/plugin.json +5 -5
- package/src/codex-plugins/simy/hooks/README.md +3 -3
- package/src/codex-plugins/simy/hooks/delivery-loop.mjs +242 -26
- package/src/codex-plugins/simy/hooks/purpose-workspace.mjs +4 -1
- package/src/codex-plugins/simy/hooks/registered-requirement-checker.mjs +201 -0
- package/src/codex-plugins/simy/hooks/requirement-acceptance.mjs +69 -0
- package/src/codex-plugins/simy/skills/continue/SKILL.md +4 -4
- package/src/codex-plugins/simy/skills/continue/agents/openai.yaml +3 -3
- package/src/codex-plugins/simy/skills/diagnose/SKILL.md +2 -2
- package/src/codex-plugins/simy/skills/sqm-loop/SKILL.md +3 -3
- package/src/codex-plugins/simy/skills/sqm-loop/agents/openai.yaml +2 -2
- package/src/console/app.js +7 -7
- package/src/delivery-hooks/claude-workflow.md +1 -1
- package/src/delivery-hooks/command.js +5 -5
- package/src/delivery-hooks/supervisor-watchdog.js +3 -2
- package/src/desktop-host.js +53 -23
- package/src/index.js +1 -1
- package/src/local-task.js +1 -1
- package/src/orchestrator/presentation.js +1 -1
- package/src/runner.js +2 -2
- package/src/session-supervision/acceptance-reads.js +11 -2
- package/src/session-supervision/analysis-manager.js +9 -2
- package/src/session-supervision/automatic-continuation.js +11 -11
- package/src/session-supervision/catalog-refresh.js +16 -0
- package/src/session-supervision/claude-native-agent-contract.js +62 -0
- package/src/session-supervision/claude-outcome-review.js +7 -6
- package/src/session-supervision/claude-sessions.js +137 -76
- package/src/session-supervision/codex-app-server.js +3 -3
- package/src/session-supervision/codex-children.js +2 -2
- package/src/session-supervision/continuation-analysis.js +1 -1
- package/src/session-supervision/continuation-command.js +3 -3
- package/src/session-supervision/continuation-readiness.js +1 -1
- package/src/session-supervision/conversation-context.js +58 -26
- package/src/session-supervision/conversation-presentation.js +3 -3
- package/src/session-supervision/decision-presentation.js +8 -8
- package/src/session-supervision/discovery-window.js +8 -2
- package/src/session-supervision/index.js +38 -25
- package/src/session-supervision/purpose-verification.js +12 -3
- package/src/session-supervision/remote-errors.js +2 -2
- package/src/session-supervision/work-state.js +3 -3
- package/src/sqm/advisory-guidance.js +1 -0
- package/src/sqm/agent-skill.js +3 -3
- package/src/sqm/checkers.js +35 -1
- package/src/sqm/claude-plugin.js +8 -2
- package/src/sqm/hypothesis-knowledge.js +3 -3
- package/src/sqm/knowledge-recall.js +34 -12
- package/src/sqm/reasoned-assessment.js +22 -9
- 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:
|
|
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
|
|
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
|
|
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
|
|
3
|
-
short_description: "
|
|
4
|
-
default_prompt: "Use $simy:continue to
|
|
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:
|
|
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
|
-
#
|
|
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
|
|
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
|
|
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
|
|
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
|
|
3
|
-
short_description: "Run SQM
|
|
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
|
package/src/console/app.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 —
|
|
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
|
|
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
|
|
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] —
|
|
239
|
-
output('simy delivery diagnose --provider codex|claude --session ID [--task-id ID] [--mode analyze|evidence] [--issues-file issues.json] [--detail brief|full] — advisory
|
|
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
|
|
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;
|