@awak-app/simy-cli 0.7.37 → 0.7.39
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 +55 -5
- package/package.json +2 -1
- package/src/auto-update.js +25 -13
- package/src/claude-desktop/bridge.js +156 -0
- package/src/claude-desktop/extension.js +84 -0
- package/src/claude-desktop/server.js +86 -0
- package/src/cli-contract.js +1 -1
- package/src/cli-observability.js +2 -2
- package/src/codex-plugins/simy/hooks/delivery-loop.mjs +240 -24
- package/src/codex-plugins/simy/hooks/mention-router.mjs +42 -0
- 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 +8 -3
- package/src/delivery-hooks/supervisor-watchdog.js +3 -2
- package/src/desktop-host.js +48 -18
- package/src/index.js +10 -0
- package/src/runner.js +28 -26
- package/src/session-supervision/acceptance-reads.js +11 -2
- package/src/session-supervision/analysis-manager.js +9 -2
- 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/conversation-context.js +58 -26
- package/src/session-supervision/discovery-window.js +8 -2
- package/src/session-supervision/index.js +25 -12
- package/src/session-supervision/purpose-verification.js +12 -3
- package/src/sqm/advisory-guidance.js +1 -0
- package/src/sqm/checkers.js +39 -1
- package/src/sqm/claude-plugin.js +15 -1
- package/src/sqm/claude-simy-agent.md +11 -0
- package/src/sqm/delivery-assessment.js +78 -0
- package/src/sqm/hypothesis-knowledge.js +3 -3
- package/src/sqm/knowledge-recall.js +34 -12
- package/src/sqm/reasoned-assessment.js +27 -12
- package/src/sqm/workflow-onboarding.js +2 -1
- package/src/update-detachment/runtime.js +2 -1
|
@@ -5,7 +5,7 @@ import { resolve, relative, isAbsolute, join } from 'node:path';
|
|
|
5
5
|
|
|
6
6
|
// Completion reviews bind to the actual local state. No resets, stashes, writes,
|
|
7
7
|
// shell commands from a worker, or special verifier registration are needed.
|
|
8
|
-
export async function workspaceIdentity(cwd, stateRoot, inventory, { signal, excludedPaths = [], inventoryLimit = 256,
|
|
8
|
+
export async function workspaceIdentity(cwd, stateRoot, inventory, { signal, excludedPaths = [], inventoryLimit = 256, inventoryPaths,
|
|
9
9
|
maxEntries = Infinity, maxBytes = Infinity, deadline = Infinity } = {}) {
|
|
10
10
|
signal?.throwIfAborted();
|
|
11
11
|
let entries = 0, bytes = 0;
|
|
@@ -18,6 +18,9 @@ export async function workspaceIdentity(cwd, stateRoot, inventory, { signal, exc
|
|
|
18
18
|
const paths = [];
|
|
19
19
|
const capture = entry => {
|
|
20
20
|
if (!inventory) return;
|
|
21
|
+
// A trusted read-only consumer may request only its relevant inventory
|
|
22
|
+
// rows. Every workspace byte/mode is still hashed and race-checked below.
|
|
23
|
+
if (inventoryPaths && !inventoryPaths.includes(entry.path)) return;
|
|
21
24
|
if (inventory.length < inventoryLimit) inventory.push(entry);
|
|
22
25
|
else inventory.truncated = true;
|
|
23
26
|
};
|
|
@@ -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
|
+
}
|
|
@@ -64,6 +64,11 @@ authorized. Do not disable its sandbox or switch to Full Access automatically.
|
|
|
64
64
|
|
|
65
65
|
Codex app users can select SIMY with `@` and ask it to continue. Codex CLI uses
|
|
66
66
|
its native skill selector, `$simy:continue`, after plugin installation. In Claude
|
|
67
|
-
Code
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
Code accepts a submitted `@simy` request through the enabled prompt hook. Its
|
|
68
|
+
native SIMY agent appears under the scoped name `simy:simy` in the @ picker;
|
|
69
|
+
that read-only guide returns intent to the parent, which performs authorized
|
|
70
|
+
controls with the original parent identity. Never use a child agent's ID.
|
|
71
|
+
`/simy:continue` also remains available. Claude Desktop normal chat uses the
|
|
72
|
+
separately installed SIMY MCP extension; select the exact existing local provider
|
|
73
|
+
conversation through its sessions operation rather than using the hosted chat ID.
|
|
74
|
+
Verify a real command receipt before claiming invocation or execution succeeded.
|
|
@@ -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;
|
package/src/desktop-host.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { createDesktopFrameWriter } from './desktop-frame-writer.js';
|
|
2
2
|
import { boundedDiscovery } from './session-supervision/progressive-discovery.js';
|
|
3
|
+
import { createCatalogRefreshPolicy } from './session-supervision/catalog-refresh.js';
|
|
4
|
+
import { supervisedSession } from './session-supervision/discovery-window.js';
|
|
3
5
|
import { createClaudeDesktopArchiveController } from './session-supervision/claude-desktop-archive.js';
|
|
4
6
|
import { createClaudeDesktopNativeAdapter } from './session-supervision/claude-desktop-native.js';
|
|
5
7
|
import { applyControllerExecutionContext } from './session-supervision/execution-context.js';
|
|
@@ -203,6 +205,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
203
205
|
status = { ...status, update: { ...status.update,
|
|
204
206
|
auto_update_supported: updateSettings.auto_update_supported === true,
|
|
205
207
|
auto_update_enabled: updateSettings.auto_update_enabled === true,
|
|
208
|
+
auto_update_unavailable_reason: updateSettings.auto_update_unavailable_reason || null,
|
|
206
209
|
auto_update_busy: updateSettings.auto_update_busy === true } };
|
|
207
210
|
if (env.SIMY_REMOTE_CHILD === "1") status = { ...status, remoteSessionProtocol: 1, remoteControl: { version: 2, actions: [...allowedActions] } };
|
|
208
211
|
ownerTransport?.publish({ type: 'status', status: { ...status, remoteSessionProtocol: 1, remoteControl: { version: 2, actions: [...allowedActions] } } });
|
|
@@ -480,6 +483,13 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
480
483
|
})().finally(() => { emailSync = null; });
|
|
481
484
|
await emailSync;
|
|
482
485
|
};
|
|
486
|
+
const saveEmailDeliveryObservation = (controller, completion, observation) => controllerPreferences.change(controller.controllerScope, state => {
|
|
487
|
+
const identity = JSON.stringify([completion.host_id || 'local', completion.provider, completion.provider_session_id]);
|
|
488
|
+
const row = state.conversations[identity] ||= {};
|
|
489
|
+
row.emailDeliveries ||= {};
|
|
490
|
+
row.emailDeliveries[completion.task_id] = { ...observation, checkedAt: Date.now(), provider: completion.provider,
|
|
491
|
+
provider_session_id: completion.provider_session_id, host_id: completion.host_id || 'local' };
|
|
492
|
+
});
|
|
483
493
|
const submitNativeDevelopmentReportCompletions = ({ completions, controller }) => {
|
|
484
494
|
const { scope, deviceSession } = controller;
|
|
485
495
|
if (!deviceSession?.token || !deviceSession?.api_base_url || !sameReportController(controller, nativeDevelopmentReportsController) || nativeDevelopmentReports.state !== "ready" || nativeDevelopmentReportsScope !== scope) return;
|
|
@@ -506,23 +516,31 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
506
516
|
if (receipt && nativeDevelopmentReportsGeneration === generation && accessScope(currentAccess) === scope) {
|
|
507
517
|
if (receipt.status === 'pending') nativeCompletionRetryAt.set(key, Date.now() + 30_000);
|
|
508
518
|
else { nativeCompletionSuccess.add(key); nativeCompletionRetryAt.delete(key); }
|
|
509
|
-
|
|
510
|
-
await controllerPreferences.change(receiptScope, state => {
|
|
511
|
-
const identity = JSON.stringify([hostId, completion.provider, completion.provider_session_id]);
|
|
512
|
-
const row = state.conversations[identity] ||= {};
|
|
513
|
-
row.emailDeliveries ||= {};
|
|
514
|
-
row.emailDeliveries[completion.task_id] = { ...receipt, checkedAt: Date.now(), provider: completion.provider, provider_session_id: completion.provider_session_id, host_id: hostId };
|
|
515
|
-
});
|
|
519
|
+
await saveEmailDeliveryObservation(controller, completion, receipt);
|
|
516
520
|
await requireReportController(controller);
|
|
517
521
|
if (!closed) void publish().catch(() => {});
|
|
518
522
|
}
|
|
519
523
|
return receipt;
|
|
520
|
-
}).catch(error => {
|
|
524
|
+
}).catch(async error => {
|
|
521
525
|
if (nativeDevelopmentReportsGeneration === generation && accessScope(currentAccess) === scope) {
|
|
522
526
|
// These are immutable authorization/contract rejections. A new
|
|
523
527
|
// enrollment revision or scope produces a fresh rejection identity.
|
|
524
|
-
|
|
528
|
+
const rejected = [400, 403, 409].includes(error?.status);
|
|
529
|
+
if (rejected) nativeCompletionRejected.add(rejectionKey);
|
|
525
530
|
else nativeCompletionRetryAt.set(key, Date.now() + 30_000);
|
|
531
|
+
// ON is consent, not a delivery receipt. Surface a failed submission
|
|
532
|
+
// without exposing backend errors, credentials or completion proofs.
|
|
533
|
+
// The next confirmed server receipt replaces this local observation.
|
|
534
|
+
try {
|
|
535
|
+
await requireReportController(controller);
|
|
536
|
+
if (closed || nativeDevelopmentReportsGeneration !== generation || accessScope(currentAccess) !== scope) return null;
|
|
537
|
+
await saveEmailDeliveryObservation(controller, completion, {
|
|
538
|
+
task_id: completion.task_id, enrollment_revision: completion.enrollment_revision,
|
|
539
|
+
status: rejected ? 'rejected' : 'retrying',
|
|
540
|
+
});
|
|
541
|
+
await requireReportController(controller);
|
|
542
|
+
if (!closed) void publish().catch(() => {});
|
|
543
|
+
} catch { /* A changed controller must not receive an old failure. */ }
|
|
526
544
|
}
|
|
527
545
|
return null;
|
|
528
546
|
}).finally(() => nativeCompletionInFlight.delete(key));
|
|
@@ -568,6 +586,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
568
586
|
let commands;
|
|
569
587
|
let sessionStatus = { sessions: [], providers: { codex: { discovery: "loading", reason: null }, claude: { discovery: "loading", reason: null } }, refreshedAt: null, initialLoading: true };
|
|
570
588
|
let sessionRefresh = null, sessionRefreshedAt = 0, sessionRefreshAgain = false, discoveryController = null, discoveryStopped = false;
|
|
589
|
+
const catalogRefresh = createCatalogRefreshPolicy();
|
|
571
590
|
let catalogRevision = 0;
|
|
572
591
|
let enrichmentController = null, sessionEnrichment = null;
|
|
573
592
|
let catalogAnalysisJob = null, catalogAnalysisAgain = false;
|
|
@@ -585,7 +604,9 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
585
604
|
const records = await recordReader.list();
|
|
586
605
|
if (closed || relay || discoveryStopped || manager !== analysisManager ||
|
|
587
606
|
!desktopAccessReady(currentAccess) || accessScope(currentAccess) !== scope || !coordinator.acceptsNewWork()) return;
|
|
588
|
-
await manager.update(analysisSessions(withCurrentSettings(sessionStatus.sessions, records))
|
|
607
|
+
await manager.update(analysisSessions(withCurrentSettings(sessionStatus.sessions, records)), {
|
|
608
|
+
priorityKeys: new Set(records.filter(supervisedSession).map(record => `${record.provider}:${record.session}`)),
|
|
609
|
+
});
|
|
589
610
|
}
|
|
590
611
|
}).catch(() => {}).finally(() => {
|
|
591
612
|
catalogAnalysisJob = null;
|
|
@@ -640,12 +661,14 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
640
661
|
unfinishedDesktopWork = false;
|
|
641
662
|
})();
|
|
642
663
|
const observePullRequests = dependencies.observePullRequests || createPullRequestObserver();
|
|
643
|
-
const refreshSessions = (force = false) => {
|
|
664
|
+
const refreshSessions = (force = false, { history = false } = {}) => {
|
|
665
|
+
if (history) catalogRefresh.invalidate();
|
|
644
666
|
if (discoveryStopped && !closed && !relay) return sessionStatus;
|
|
645
667
|
if (closed || relay || !desktopAccessReady(currentAccess)) return { sessions: [], providers: {}, refreshedAt: null };
|
|
646
668
|
if (force && sessionRefresh) sessionRefreshAgain = true;
|
|
647
669
|
if (!sessionRefresh && (force || Date.now() - sessionRefreshedAt >= 15_000)) {
|
|
648
670
|
const requestedScope = accessScope(currentAccess);
|
|
671
|
+
const refresh = catalogRefresh.begin(JSON.stringify([requestedScope, providerLinks]));
|
|
649
672
|
const controller = discoveryController = new AbortController();
|
|
650
673
|
const attempt = { id: randomUUID(), requestedAt: Date.now(), state: 'pending' };
|
|
651
674
|
sessionStatus = { ...sessionStatus, discoveryAttempt: attempt };
|
|
@@ -659,7 +682,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
659
682
|
}
|
|
660
683
|
const complete = new Set(value.completeProviders || (value.historyLoading === undefined ? PROVIDER_LINKS : []));
|
|
661
684
|
const merge = (previous, incoming) => {
|
|
662
|
-
const rows = new Map(previous.filter(item => !complete.has(item.provider)).map(item => [`${item.provider}:${item.session}`, item]));
|
|
685
|
+
const rows = new Map(previous.filter(item => refresh.recentOnly || !complete.has(item.provider)).map(item => [`${item.provider}:${item.session}`, item]));
|
|
663
686
|
for (const item of incoming || []) rows.set(`${item.provider}:${item.session}`, item);
|
|
664
687
|
return [...rows.values()];
|
|
665
688
|
};
|
|
@@ -674,7 +697,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
674
697
|
const firstPageTimeoutMs = dependencies.discoveryTimeoutMs ?? 30_000;
|
|
675
698
|
const historyTimeoutMs = dependencies.historyDiscoveryTimeoutMs ?? dependencies.discoveryTimeoutMs ?? 300_000;
|
|
676
699
|
sessionRefresh = boundedDiscovery((signal, startHistory) => (dependencies.discoverSessions || discoverSessions)({
|
|
677
|
-
root: deliveryRoot, signal, onDrain: trackDiscoveryDrain, includeArchived: true, includeSelectionCandidates: true,
|
|
700
|
+
root: deliveryRoot, signal, onDrain: trackDiscoveryDrain, includeArchived: true, includeSelectionCandidates: true, recentOnly: refresh.recentOnly,
|
|
678
701
|
readRecords: () => recordReader.list(),
|
|
679
702
|
connectedProviders: PROVIDER_LINKS.filter(provider => providerLinks[provider].connected),
|
|
680
703
|
timeoutMs: firstPageTimeoutMs, historyTimeoutMs,
|
|
@@ -685,6 +708,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
685
708
|
await applyCatalog(value);
|
|
686
709
|
if (closed || relay || controller.signal.aborted) return;
|
|
687
710
|
const failed = Object.values(value.providers || {}).some(provider => provider.discovery === 'unavailable');
|
|
711
|
+
refresh.complete(!failed);
|
|
688
712
|
sessionStatus = { ...sessionStatus, historyLoading: false,
|
|
689
713
|
discoveryAttempt: { ...attempt, state: failed ? 'failed' : 'succeeded', completedAt: Date.now() } };
|
|
690
714
|
// Publish before PR/network observation and queue maintenance. Neither
|
|
@@ -1507,7 +1531,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
1507
1531
|
if(['detach_update','update_reconnect'].includes(action)) {
|
|
1508
1532
|
const access=await accessSnapshot(); requireDesktopAccess(access);
|
|
1509
1533
|
updateRuntime.validate(command,coordinator.snapshot(),updater?.snapshot?.());
|
|
1510
|
-
if(action==='detach_update')await updater.maybeApply({detach:true});
|
|
1534
|
+
if(action==='detach_update')await updater.maybeApply({detach:true,manual:true});
|
|
1511
1535
|
else await reconnectWithHistoryRecovery({root,coordinator,eligible:()=>updateRuntime.historyRecoveryEligible()});
|
|
1512
1536
|
emit({type:'result',id:command.id,ok:coordinator.snapshot().state==='connected' && (action!=='detach_update' || updater.snapshot().state==='updated')});
|
|
1513
1537
|
await publish();return;
|
|
@@ -1578,7 +1602,11 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
1578
1602
|
if (['detach_update', 'update_reconnect'].includes(action)) requireDesktopAccess(access);
|
|
1579
1603
|
else if (!recoveryControl && desktopAccessReady(access) && !desktopActionAllowed({ state: 'unavailable' }, command)) await authorizeWork();
|
|
1580
1604
|
|
|
1581
|
-
|
|
1605
|
+
// Signing back into the same SIMY environment is a prerequisite for
|
|
1606
|
+
// recovery, not permission to resume work. Keep organization changes
|
|
1607
|
+
// and execution fenced until the original checkpoint is reconciled.
|
|
1608
|
+
const recoveryAuthorization = ["login", "authorization_open", "authorization_copy", "organization_cancel"].includes(action);
|
|
1609
|
+
if (!coordinator.acceptsNewWork() && !recoveryControl && !recoveryAuthorization && !["refresh", "reconnect", "task_detail", "check_update", "quit", "delivery_stop", "delivery_pause", "delivery_reject"].includes(action)) {
|
|
1582
1610
|
throw new Error("The update connection requires recovery.");
|
|
1583
1611
|
}
|
|
1584
1612
|
if (['team_threads_refresh','team_thread_open','team_thread_share'].includes(action)) {
|
|
@@ -1890,7 +1918,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
1890
1918
|
else if (desktopAccessReady(access)) await agent.refreshCodexStatus?.();
|
|
1891
1919
|
await agent.refreshDesktopStatus();
|
|
1892
1920
|
await refreshPlugins();
|
|
1893
|
-
refreshSessions(true);
|
|
1921
|
+
refreshSessions(true, { history: true });
|
|
1894
1922
|
if (desktopAccessReady(currentAccess) && coordinator.acceptsNewWork()) void actionAssociations.tick().then(publish).catch(() => {});
|
|
1895
1923
|
}
|
|
1896
1924
|
else if (action === "organizations_refresh") await agent.refreshOrganizations();
|
|
@@ -2227,7 +2255,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
2227
2255
|
} finally { clearInterval(watchdog); }
|
|
2228
2256
|
|
|
2229
2257
|
await recordProviderArchive(deliveryRoot, 'claude', command.session, archived, Date.now());
|
|
2230
|
-
refreshSessions(true);
|
|
2258
|
+
refreshSessions(true, { history: true });
|
|
2231
2259
|
emit({ type: 'result', id: command.id, ok: true, data });
|
|
2232
2260
|
void publish().catch(() => {});
|
|
2233
2261
|
return;
|
|
@@ -2245,7 +2273,9 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
2245
2273
|
listHidden: archived,
|
|
2246
2274
|
}));
|
|
2247
2275
|
}
|
|
2248
|
-
|
|
2276
|
+
// Archive/restore can move an old conversation outside both recent
|
|
2277
|
+
// pages. Explicit organization must be visible immediately.
|
|
2278
|
+
refreshSessions(true, { history: true });
|
|
2249
2279
|
} else if (action === "session_track" || action === "session_untrack") {
|
|
2250
2280
|
const candidate = await selectedSession(command);
|
|
2251
2281
|
if (candidate.capabilities?.canMonitor !== true) throw new Error('Tracking is unavailable for this conversation.');
|
package/src/index.js
CHANGED
|
@@ -62,6 +62,13 @@ if (!configuredWebOrigin) {
|
|
|
62
62
|
// Invalid host input is reported above before update discovery or startup.
|
|
63
63
|
} else if (args.has("--version") || args.has("-v")) {
|
|
64
64
|
console.log(packageInfo.version);
|
|
65
|
+
} else if (dispatchArgv[0] === "mcp") {
|
|
66
|
+
if (dispatchArgv.length !== 1) { console.error("Use simy mcp for the Claude Desktop stdio connection."); process.exitCode = 2; }
|
|
67
|
+
else { const { serveMcp } = await import("./claude-desktop/server.js"); await serveMcp(); }
|
|
68
|
+
} else if (dispatchArgv[0] === "integrations") {
|
|
69
|
+
const { runIntegrationCommand } = await import("./claude-desktop/extension.js");
|
|
70
|
+
try { process.exitCode = await runIntegrationCommand(dispatchArgv.slice(1)); }
|
|
71
|
+
catch (error) { console.error(error.message); process.exitCode = 2; }
|
|
65
72
|
} else if (dispatchArgv[0] === "sessions") {
|
|
66
73
|
const { runSessionListCommand } = await import("./session-supervision/session-list.js");
|
|
67
74
|
process.exitCode = await runSessionListCommand(dispatchArgv.slice(1));
|
|
@@ -145,6 +152,9 @@ async function runCommand(installMode) {
|
|
|
145
152
|
console.log(`Usage:
|
|
146
153
|
simy Start the local SIMY agent
|
|
147
154
|
simy --version Print the installed version without starting or updating
|
|
155
|
+
simy mcp Connect Claude Desktop to SIMY using MCP stdio
|
|
156
|
+
simy integrations claude-desktop prepare --output FILE.mcpb
|
|
157
|
+
Prepare a local Claude Desktop extension; install it in Claude
|
|
148
158
|
simy connections 別のパソコンへの接続や、つながらないときの手順を表示
|
|
149
159
|
simy doctor Read-only version, connection, provider and failure diagnostics
|
|
150
160
|
simy --daemon Start it in the background
|
package/src/runner.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { safetyCeilingRecoveryMessage } from "./orchestrator/recovery.js";
|
|
2
|
+
import { createDeliveryAssessmentRunner } from './sqm/delivery-assessment.js';
|
|
2
3
|
import { captureDispatchProvenance } from './update-detachment/provenance.js';
|
|
3
4
|
import { captureDispatchOwner, assertTaskAdmission, dispatchWithFence, observeDispatchEvent, queueDispatchReceipt } from './update-detachment/dispatch-gate.js';
|
|
4
5
|
import { execFile, execFileSync } from "node:child_process";
|
|
@@ -886,19 +887,7 @@ async function runLocalCodingRun(
|
|
|
886
887
|
}));
|
|
887
888
|
const sqmChecker =
|
|
888
889
|
run.sqmChecker ||
|
|
889
|
-
|
|
890
|
-
cwd: repositoryPath,
|
|
891
|
-
baseBranch: run.snapshot.charter.base_branch,
|
|
892
|
-
cliVersion: CLI_VERSION,
|
|
893
|
-
token: run.session?.token,
|
|
894
|
-
endpoint: sqmBundleEndpoint(run.session),
|
|
895
|
-
evidenceEndpoint: sqmEvidenceEndpoint(run.session),
|
|
896
|
-
webOrigin: run.apiOrigin,
|
|
897
|
-
historyRoot: run.sqmHistoryRoot,
|
|
898
|
-
accountId: run.session?.account_id || run.session?.auth_user_id || run.session?.device_id,
|
|
899
|
-
organizationId: run.session?.organization_id || run.session?.org_id,
|
|
900
|
-
deviceId: run.session?.device_id,
|
|
901
|
-
});
|
|
890
|
+
createLocalRunSqmChecker(run, repositoryPath);
|
|
902
891
|
run.sqmChecker = sqmChecker;
|
|
903
892
|
refreshSqmCheckerSession(run, sqmChecker);
|
|
904
893
|
|
|
@@ -983,19 +972,7 @@ async function recheckLocalCodingRunOwned(run, { collectEvidence, executeIndepen
|
|
|
983
972
|
}));
|
|
984
973
|
const sqmChecker =
|
|
985
974
|
run.sqmChecker ||
|
|
986
|
-
|
|
987
|
-
cwd: repositoryPath,
|
|
988
|
-
baseBranch: run.snapshot.charter.base_branch,
|
|
989
|
-
cliVersion: CLI_VERSION,
|
|
990
|
-
token: run.session?.token,
|
|
991
|
-
endpoint: sqmBundleEndpoint(run.session),
|
|
992
|
-
evidenceEndpoint: sqmEvidenceEndpoint(run.session),
|
|
993
|
-
webOrigin: run.apiOrigin,
|
|
994
|
-
historyRoot: run.sqmHistoryRoot,
|
|
995
|
-
accountId: run.session?.account_id || run.session?.auth_user_id || run.session?.device_id,
|
|
996
|
-
organizationId: run.session?.organization_id || run.session?.org_id,
|
|
997
|
-
deviceId: run.session?.device_id,
|
|
998
|
-
});
|
|
975
|
+
createLocalRunSqmChecker(run, repositoryPath);
|
|
999
976
|
run.sqmChecker = sqmChecker;
|
|
1000
977
|
refreshSqmCheckerSession(run, sqmChecker);
|
|
1001
978
|
await recheckPrReadiness({
|
|
@@ -1041,6 +1018,31 @@ export async function markCodingRunMonitorFailure(run, error) {
|
|
|
1041
1018
|
await updateRun(run, "pr_ready_for_review");
|
|
1042
1019
|
}
|
|
1043
1020
|
|
|
1021
|
+
export function createLocalRunSqmChecker(run, repositoryPath, dependencies = {}) {
|
|
1022
|
+
const { semanticExecutionOptions, ...checkerDependencies } = dependencies;
|
|
1023
|
+
const authorize = () => {
|
|
1024
|
+
assertRecoveryActor(run);
|
|
1025
|
+
if (run.stopRequested) throw Object.assign(new Error("SQM review cancelled"), { code: "analysis_cancelled" });
|
|
1026
|
+
};
|
|
1027
|
+
return createSqmChecker({
|
|
1028
|
+
...checkerDependencies,
|
|
1029
|
+
cwd: repositoryPath,
|
|
1030
|
+
baseBranch: run.snapshot.charter.base_branch,
|
|
1031
|
+
cliVersion: CLI_VERSION,
|
|
1032
|
+
token: run.session?.token,
|
|
1033
|
+
endpoint: sqmBundleEndpoint(run.session),
|
|
1034
|
+
evidenceEndpoint: sqmEvidenceEndpoint(run.session),
|
|
1035
|
+
webOrigin: run.apiOrigin,
|
|
1036
|
+
historyRoot: run.sqmHistoryRoot,
|
|
1037
|
+
accountId: run.session?.account_id || run.session?.auth_user_id || run.session?.device_id,
|
|
1038
|
+
organizationId: run.session?.organization_id || run.session?.org_id,
|
|
1039
|
+
deviceId: run.session?.device_id,
|
|
1040
|
+
assessImpl: createDeliveryAssessmentRunner(run, { ...semanticExecutionOptions, repositoryPath, authorize,
|
|
1041
|
+
persist: async () => { if (run.localStateRoot) await saveCodingRunSnapshot(run.localStateRoot, run.snapshot); },
|
|
1042
|
+
}),
|
|
1043
|
+
});
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1044
1046
|
function sqmBundleEndpoint(session) {
|
|
1045
1047
|
if (process.env.SIMY_SQM_BUNDLE_URL?.trim()) return process.env.SIMY_SQM_BUNDLE_URL.trim();
|
|
1046
1048
|
if (!session?.api_base_url) return undefined;
|