@linzumi/cli 1.0.110 → 1.0.112
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 +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/scripts/build-core-commander-seams-parity-oracle.mjs +586 -0
- package/scripts/build-core-daemon-parity-oracle.mjs +573 -0
- package/scripts/build-core-dispatch-input-queue-parity-oracle.mjs +219 -0
- package/scripts/build-core-fate-ladder-parity-oracle.mjs +250 -0
- package/scripts/build-core-generation-overlap-parity-oracle.mjs +288 -0
- package/scripts/build-core-integration-parity-oracle.mjs +361 -0
- package/scripts/build-core-lifecycle-guards-parity-oracle.mjs +443 -0
- package/scripts/build-core-lock-parity-oracle.mjs +455 -0
- package/scripts/build-core-reconnect-resume-parity-oracle.mjs +256 -0
- package/scripts/build-core-startup-resume-scan-parity-oracle.mjs +296 -0
- package/scripts/build-core-supervisor-parity-oracle.mjs +734 -0
- package/scripts/build-pipeline-parity-oracle.mjs +610 -0
|
@@ -0,0 +1,573 @@
|
|
|
1
|
+
// Spec: kandan/plans/2026-07-10-compute-nodes-rescript-commander-zero-ts-program.md
|
|
2
|
+
// (M3 cluster 10, runner core / supervisor / daemon).
|
|
3
|
+
// Relationship: Builds the TS-side PARITY ORACLE for the Commander daemon
|
|
4
|
+
// start/status/wait/stop primitives of the ReScript commander port, the
|
|
5
|
+
// sibling of build-core-commander-seams-parity-oracle.mjs. ONE bundle
|
|
6
|
+
// imports the REAL './src/commanderDaemon' exports behind the one-shot
|
|
7
|
+
// batch driver: a JSON array of cases on stdin, a JSON array of results on
|
|
8
|
+
// stdout. The ReScript package's CoreDaemonParityConformance drives the
|
|
9
|
+
// SAME inputs through src/core/CommanderDaemon.res and asserts
|
|
10
|
+
// accept/reject/normalized-output equivalence AND byte-identical status
|
|
11
|
+
// record files - the cross-impl contract that lets both commanders share
|
|
12
|
+
// ~/.linzumi/commanders through M4.
|
|
13
|
+
//
|
|
14
|
+
// Determinism / seams per op:
|
|
15
|
+
// - paths: HOME is redirected to the case scratch dir for the whole
|
|
16
|
+
// derivation (os.homedir() reads $HOME per call on posix).
|
|
17
|
+
// - start: timestamps are pinned by a scoped global-Date override
|
|
18
|
+
// (startedAt is not injectable in the TS surface), process.cwd() is
|
|
19
|
+
// pinned by chdir into $DIR/cwdpin, the spawn rides the TS spawnImpl
|
|
20
|
+
// injection point as a scripted seam with a fixed pid, and the
|
|
21
|
+
// hard-wired readProcessIdentity probe runs the REAL `ps` against a
|
|
22
|
+
// pid that cannot exist (999999999 exceeds every platform pid space),
|
|
23
|
+
// so the probe deterministically reports no identity on both impls.
|
|
24
|
+
// The TS audit-log writer (called directly by the module) is redirected
|
|
25
|
+
// via LINZUMI_CLI_AUDIT_LOG to a scratch OUTSIDE the snapshot dir and
|
|
26
|
+
// its lines come back projected to the stable fields (ts dropped).
|
|
27
|
+
// - start with real:true: where the seam is bypassed, the REAL spawn runs
|
|
28
|
+
// `node -e` against the module's own command shape ('commander' is the
|
|
29
|
+
// evaluated script - a bare ReferenceError, the child exits instantly).
|
|
30
|
+
// MASKING (structural, mirrored by the conformance side): pid is
|
|
31
|
+
// replaced by '<PID>' after asserting it is a positive number,
|
|
32
|
+
// processStartedAt is dropped from records/key lists (the `ps` probe
|
|
33
|
+
// racing a dying child is the one genuinely unpinnable bit), and the
|
|
34
|
+
// log file's node error output is replaced by '<VOLATILE_LOG>'.
|
|
35
|
+
// - status/stop: process liveness is the REAL process.kill(pid, 0) - the
|
|
36
|
+
// conformance feeds both impls the SAME genuinely-live pid (its own)
|
|
37
|
+
// and genuinely-dead pid (a reaped spawnSync child), so classification
|
|
38
|
+
// compares real kernel answers. The identity reader rides the TS
|
|
39
|
+
// injection point (scripted, with a call counter) except in stop,
|
|
40
|
+
// where the TS surface hard-wires the default: the liveChild leg runs
|
|
41
|
+
// a REAL detached `node -e 'setInterval(...)'` child so the real `ps`
|
|
42
|
+
// probe classifies it and the real SIGINT terminates it (childExited
|
|
43
|
+
// reports the observed exit; the volatile pid is masked to '<PID>').
|
|
44
|
+
// - wait: `now`, readTextFile, and statusImpl all ride the TS injection
|
|
45
|
+
// points as scripted sequences with call counters (the counters are
|
|
46
|
+
// part of the parity contract - both impls must consume the seams in
|
|
47
|
+
// the same order); the hard-wired fs.watch leg is exercised for real
|
|
48
|
+
// via the realLog case (a scheduled append to a real file), and the
|
|
49
|
+
// scripted-clock cases drive its timeout path with remaining=0 so no
|
|
50
|
+
// leg depends on wall time.
|
|
51
|
+
//
|
|
52
|
+
// SECRETS: the oracle only ever sees the conformance suite's synthetic
|
|
53
|
+
// ids/paths; nothing here logs case contents.
|
|
54
|
+
//
|
|
55
|
+
// The output is a local test artifact (.differential/ is gitignored, never
|
|
56
|
+
// published) and stays unminified for debuggability.
|
|
57
|
+
import { mkdir } from 'node:fs/promises';
|
|
58
|
+
import { dirname, join } from 'node:path';
|
|
59
|
+
import { fileURLToPath } from 'node:url';
|
|
60
|
+
import { build } from 'esbuild';
|
|
61
|
+
|
|
62
|
+
const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
63
|
+
|
|
64
|
+
const driverSource = `
|
|
65
|
+
import {
|
|
66
|
+
appendFileSync,
|
|
67
|
+
mkdirSync,
|
|
68
|
+
mkdtempSync,
|
|
69
|
+
readdirSync,
|
|
70
|
+
readFileSync,
|
|
71
|
+
realpathSync,
|
|
72
|
+
statSync,
|
|
73
|
+
writeFileSync,
|
|
74
|
+
} from 'node:fs';
|
|
75
|
+
import { spawn } from 'node:child_process';
|
|
76
|
+
import { tmpdir } from 'node:os';
|
|
77
|
+
import { join, relative } from 'node:path';
|
|
78
|
+
import {
|
|
79
|
+
commanderDaemonStatus,
|
|
80
|
+
commanderStatusDir,
|
|
81
|
+
commanderStatusFile,
|
|
82
|
+
defaultCommanderLogFile,
|
|
83
|
+
startCommanderDaemon,
|
|
84
|
+
stopCommanderDaemon,
|
|
85
|
+
waitForCommanderDaemon,
|
|
86
|
+
} from './src/commanderDaemon';
|
|
87
|
+
|
|
88
|
+
// --- Placeholder + snapshot plumbing --------------------------------------------
|
|
89
|
+
|
|
90
|
+
const dirPlaceholder = '$DIR';
|
|
91
|
+
|
|
92
|
+
function scratch() {
|
|
93
|
+
// realpath so macOS /var -> /private/var never splits the two impls'
|
|
94
|
+
// placeholder substitution.
|
|
95
|
+
return realpathSync(mkdtempSync(join(tmpdir(), 'linzumi-core-daemon-oracle-')));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function substitute(value, from, to) {
|
|
99
|
+
if (typeof value === 'string') {
|
|
100
|
+
return value.split(from).join(to);
|
|
101
|
+
}
|
|
102
|
+
if (Array.isArray(value)) {
|
|
103
|
+
return value.map((entry) => substitute(entry, from, to));
|
|
104
|
+
}
|
|
105
|
+
if (typeof value === 'object' && value !== null) {
|
|
106
|
+
return Object.fromEntries(
|
|
107
|
+
Object.entries(value).map(([key, entry]) => [
|
|
108
|
+
substitute(key, from, to),
|
|
109
|
+
substitute(entry, from, to),
|
|
110
|
+
])
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
return value;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function snapshotFiles(dir) {
|
|
117
|
+
const files = {};
|
|
118
|
+
const walk = (current) => {
|
|
119
|
+
for (const name of readdirSync(current).sort()) {
|
|
120
|
+
const full = join(current, name);
|
|
121
|
+
if (statSync(full).isDirectory()) {
|
|
122
|
+
walk(full);
|
|
123
|
+
} else {
|
|
124
|
+
files[relative(dir, full)] = readFileSync(full, 'utf8');
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
walk(dir);
|
|
129
|
+
return files;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function outcome(run) {
|
|
133
|
+
try {
|
|
134
|
+
const value = run();
|
|
135
|
+
return value === undefined ? { defined: false } : { defined: true, value };
|
|
136
|
+
} catch (error) {
|
|
137
|
+
return {
|
|
138
|
+
threw: true,
|
|
139
|
+
message: error instanceof Error ? error.message : String(error),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const RealDate = Date;
|
|
145
|
+
|
|
146
|
+
function withFixedNow(nowMs, run) {
|
|
147
|
+
if (nowMs === undefined) {
|
|
148
|
+
return run();
|
|
149
|
+
}
|
|
150
|
+
class FixedDate extends RealDate {
|
|
151
|
+
constructor(...args) {
|
|
152
|
+
if (args.length === 0) {
|
|
153
|
+
super(nowMs);
|
|
154
|
+
} else {
|
|
155
|
+
super(...args);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
static now() {
|
|
159
|
+
return nowMs;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
globalThis.Date = FixedDate;
|
|
163
|
+
try {
|
|
164
|
+
return run();
|
|
165
|
+
} finally {
|
|
166
|
+
globalThis.Date = RealDate;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function withEnv(key, value, run) {
|
|
171
|
+
const previous = process.env[key];
|
|
172
|
+
process.env[key] = value;
|
|
173
|
+
try {
|
|
174
|
+
return run();
|
|
175
|
+
} finally {
|
|
176
|
+
if (previous === undefined) {
|
|
177
|
+
delete process.env[key];
|
|
178
|
+
} else {
|
|
179
|
+
process.env[key] = previous;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// The daemon status normalizer shared by the status/stop ops.
|
|
185
|
+
function statusJson(status) {
|
|
186
|
+
if (status.status === 'missing') {
|
|
187
|
+
return { status: 'missing', runnerId: status.runnerId, statusFile: status.statusFile };
|
|
188
|
+
}
|
|
189
|
+
return { status: status.status, record: status.record };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// --- paths -----------------------------------------------------------------------
|
|
193
|
+
|
|
194
|
+
function handlePaths(testCase) {
|
|
195
|
+
const dir = scratch();
|
|
196
|
+
return withEnv('HOME', dir, () => {
|
|
197
|
+
const derived = {
|
|
198
|
+
statusDir: outcome(() => commanderStatusDir()),
|
|
199
|
+
statusFileDefault: outcome(() => commanderStatusFile(testCase.runnerId)),
|
|
200
|
+
statusFileScoped: outcome(() =>
|
|
201
|
+
commanderStatusFile(testCase.runnerId, join(dir, 'custom'))
|
|
202
|
+
),
|
|
203
|
+
logFileDefault: outcome(() => defaultCommanderLogFile(testCase.runnerId)),
|
|
204
|
+
};
|
|
205
|
+
return substitute(derived, dir, dirPlaceholder);
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// --- start -----------------------------------------------------------------------
|
|
210
|
+
|
|
211
|
+
// Audit-line projection: ts is wall-clock noise (pinned only under the
|
|
212
|
+
// scoped Date override, so dropped for uniformity); the compared surface is
|
|
213
|
+
// the event vocabulary and the payload the module hands the writer.
|
|
214
|
+
function projectAuditLine(line) {
|
|
215
|
+
const parsed = JSON.parse(line);
|
|
216
|
+
const projected = {};
|
|
217
|
+
for (const key of ['event', 'sessionId', 'command', 'args', 'cwd', 'pid', 'purpose']) {
|
|
218
|
+
if (parsed[key] !== undefined) {
|
|
219
|
+
projected[key] = parsed[key];
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return projected;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function readAuditEvents(auditFile) {
|
|
226
|
+
let raw;
|
|
227
|
+
try {
|
|
228
|
+
raw = readFileSync(auditFile, 'utf8');
|
|
229
|
+
} catch {
|
|
230
|
+
return [];
|
|
231
|
+
}
|
|
232
|
+
return raw
|
|
233
|
+
.split('\\n')
|
|
234
|
+
.filter((line) => line.trim() !== '')
|
|
235
|
+
.map(projectAuditLine);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function maskRealSpawn(result, files, auditEvents) {
|
|
239
|
+
const masked = { result, statusKeys: null, logFilePresent: false };
|
|
240
|
+
if (result.defined === true) {
|
|
241
|
+
const record = { ...result.value };
|
|
242
|
+
record.pid = typeof record.pid === 'number' && record.pid > 0 ? '<PID>' : '<INVALID_PID>';
|
|
243
|
+
delete record.processStartedAt;
|
|
244
|
+
masked.result = { defined: true, value: record };
|
|
245
|
+
}
|
|
246
|
+
const fileEntries = {};
|
|
247
|
+
for (const [name, contents] of Object.entries(files)) {
|
|
248
|
+
if (name.endsWith('.log')) {
|
|
249
|
+
masked.logFilePresent = true;
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
const parsed = JSON.parse(contents);
|
|
253
|
+
masked.statusKeys = Object.keys(parsed).filter((key) => key !== 'processStartedAt');
|
|
254
|
+
parsed.pid = typeof parsed.pid === 'number' && parsed.pid > 0 ? '<PID>' : '<INVALID_PID>';
|
|
255
|
+
delete parsed.processStartedAt;
|
|
256
|
+
fileEntries[name] = parsed;
|
|
257
|
+
}
|
|
258
|
+
masked.files = fileEntries;
|
|
259
|
+
// The ps identity probe races the instantly-dying real child (present or
|
|
260
|
+
// absent exec_file_completed, live-vs-zombie command line), so only the
|
|
261
|
+
// spawn/spawned pair is compared for the real leg.
|
|
262
|
+
masked.auditEvents = auditEvents
|
|
263
|
+
.filter((event) => event.event === 'process.spawn' || event.event === 'process.spawned')
|
|
264
|
+
.map((event) =>
|
|
265
|
+
event.pid === undefined
|
|
266
|
+
? event
|
|
267
|
+
: {
|
|
268
|
+
...event,
|
|
269
|
+
pid: typeof event.pid === 'number' && event.pid > 0 ? '<PID>' : '<INVALID_PID>',
|
|
270
|
+
}
|
|
271
|
+
);
|
|
272
|
+
return masked;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function handleStart(testCase) {
|
|
276
|
+
const dir = scratch();
|
|
277
|
+
const auditDir = mkdtempSync(join(tmpdir(), 'linzumi-core-daemon-audit-'));
|
|
278
|
+
const auditFile = join(auditDir, 'audit.jsonl');
|
|
279
|
+
const spec = substitute(testCase, dirPlaceholder, dir);
|
|
280
|
+
const cwdPin = join(dir, 'cwdpin');
|
|
281
|
+
mkdirSync(cwdPin, { recursive: true });
|
|
282
|
+
const previousCwd = process.cwd();
|
|
283
|
+
const spawnCalls = [];
|
|
284
|
+
let realChildPid;
|
|
285
|
+
try {
|
|
286
|
+
process.chdir(cwdPin);
|
|
287
|
+
return withEnv('HOME', dir, () =>
|
|
288
|
+
withEnv('LINZUMI_CLI_AUDIT_LOG', auditFile, () => {
|
|
289
|
+
const spawnImpl =
|
|
290
|
+
spec.real === true
|
|
291
|
+
? undefined
|
|
292
|
+
: (command, args, options) => {
|
|
293
|
+
spawnCalls.push({
|
|
294
|
+
command,
|
|
295
|
+
args,
|
|
296
|
+
detached: options.detached === true,
|
|
297
|
+
stdio: options.stdio.map((entry) =>
|
|
298
|
+
typeof entry === 'number' ? '<fd>' : String(entry)
|
|
299
|
+
),
|
|
300
|
+
envIsProcessEnv: options.env === process.env,
|
|
301
|
+
});
|
|
302
|
+
return {
|
|
303
|
+
pid: spec.scriptedPid === null ? undefined : spec.scriptedPid,
|
|
304
|
+
unref() {},
|
|
305
|
+
};
|
|
306
|
+
};
|
|
307
|
+
const result = withFixedNow(spec.nowMs, () =>
|
|
308
|
+
outcome(() => {
|
|
309
|
+
const record = startCommanderDaemon({
|
|
310
|
+
runnerId: spec.runnerId,
|
|
311
|
+
args: spec.args ?? [],
|
|
312
|
+
cwd: spec.cwd,
|
|
313
|
+
logFile: spec.logFile,
|
|
314
|
+
statusDir: spec.statusDir,
|
|
315
|
+
nodeBin: spec.real === true ? process.execPath : spec.nodeBin,
|
|
316
|
+
entrypoint: spec.real === true ? '-e' : spec.entrypoint,
|
|
317
|
+
spawnImpl,
|
|
318
|
+
});
|
|
319
|
+
realChildPid = spec.real === true ? record.pid : undefined;
|
|
320
|
+
return record;
|
|
321
|
+
})
|
|
322
|
+
);
|
|
323
|
+
const files = snapshotFiles(dir);
|
|
324
|
+
const auditEvents = readAuditEvents(auditFile);
|
|
325
|
+
const payload =
|
|
326
|
+
spec.real === true
|
|
327
|
+
? maskRealSpawn(result, files, auditEvents)
|
|
328
|
+
: { result, files, spawnCalls, auditEvents };
|
|
329
|
+
return substitute(payload, dir, dirPlaceholder);
|
|
330
|
+
})
|
|
331
|
+
);
|
|
332
|
+
} finally {
|
|
333
|
+
process.chdir(previousCwd);
|
|
334
|
+
if (realChildPid !== undefined) {
|
|
335
|
+
try {
|
|
336
|
+
process.kill(realChildPid, 'SIGKILL');
|
|
337
|
+
} catch {
|
|
338
|
+
// already exited - the expected disposition.
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// --- status ----------------------------------------------------------------------
|
|
345
|
+
|
|
346
|
+
function handleStatus(testCase) {
|
|
347
|
+
const dir = scratch();
|
|
348
|
+
const spec = substitute(testCase, dirPlaceholder, dir);
|
|
349
|
+
if (spec.contents !== undefined) {
|
|
350
|
+
writeFileSync(commanderStatusFile(spec.recordId ?? spec.runnerId, dir), spec.contents);
|
|
351
|
+
}
|
|
352
|
+
let identityCalls = 0;
|
|
353
|
+
const reader =
|
|
354
|
+
spec.identity === undefined
|
|
355
|
+
? undefined
|
|
356
|
+
: (pid) => {
|
|
357
|
+
identityCalls += 1;
|
|
358
|
+
if (spec.identity.mode === 'none') {
|
|
359
|
+
return undefined;
|
|
360
|
+
}
|
|
361
|
+
return {
|
|
362
|
+
command: spec.identity.command,
|
|
363
|
+
...(spec.identity.startedAt === undefined
|
|
364
|
+
? {}
|
|
365
|
+
: { startedAt: spec.identity.startedAt }),
|
|
366
|
+
};
|
|
367
|
+
};
|
|
368
|
+
const result = outcome(() => statusJson(commanderDaemonStatus(spec.runnerId, dir, reader)));
|
|
369
|
+
return substitute({ result, identityCalls }, dir, dirPlaceholder);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// --- wait ------------------------------------------------------------------------
|
|
373
|
+
|
|
374
|
+
async function handleWait(testCase) {
|
|
375
|
+
const dir = scratch();
|
|
376
|
+
const spec = substitute(testCase, dirPlaceholder, dir);
|
|
377
|
+
let nowCalls = 0;
|
|
378
|
+
let statusCalls = 0;
|
|
379
|
+
let readCalls = 0;
|
|
380
|
+
const now =
|
|
381
|
+
spec.nows === undefined
|
|
382
|
+
? undefined
|
|
383
|
+
: () => {
|
|
384
|
+
const value = spec.nows[Math.min(nowCalls, spec.nows.length - 1)];
|
|
385
|
+
nowCalls += 1;
|
|
386
|
+
return value;
|
|
387
|
+
};
|
|
388
|
+
const statusImpl =
|
|
389
|
+
spec.statuses === undefined
|
|
390
|
+
? undefined
|
|
391
|
+
: () => {
|
|
392
|
+
const value = spec.statuses[Math.min(statusCalls, spec.statuses.length - 1)];
|
|
393
|
+
statusCalls += 1;
|
|
394
|
+
if (value.status === 'missing') {
|
|
395
|
+
return {
|
|
396
|
+
status: 'missing',
|
|
397
|
+
runnerId: spec.runnerId,
|
|
398
|
+
statusFile: value.statusFile,
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
return { status: value.status, record: value.record };
|
|
402
|
+
};
|
|
403
|
+
const readTextFile =
|
|
404
|
+
spec.logs === undefined
|
|
405
|
+
? undefined
|
|
406
|
+
: () => {
|
|
407
|
+
const value = spec.logs[Math.min(readCalls, spec.logs.length - 1)];
|
|
408
|
+
readCalls += 1;
|
|
409
|
+
return value === null ? undefined : value;
|
|
410
|
+
};
|
|
411
|
+
if (spec.realLog !== undefined) {
|
|
412
|
+
writeFileSync(join(dir, 'daemon.log'), spec.realLog.initial);
|
|
413
|
+
if (spec.realLog.append !== undefined) {
|
|
414
|
+
setTimeout(() => {
|
|
415
|
+
try {
|
|
416
|
+
appendFileSync(join(dir, 'daemon.log'), spec.realLog.append);
|
|
417
|
+
} catch {
|
|
418
|
+
// the wait already settled and the scratch is being torn down.
|
|
419
|
+
}
|
|
420
|
+
}, spec.realLog.appendAfterMs);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
let result;
|
|
424
|
+
try {
|
|
425
|
+
const value = await waitForCommanderDaemon({
|
|
426
|
+
runnerId: spec.runnerId,
|
|
427
|
+
timeoutMs: spec.timeoutMs,
|
|
428
|
+
statusDir: dir,
|
|
429
|
+
now,
|
|
430
|
+
readTextFile,
|
|
431
|
+
statusImpl,
|
|
432
|
+
});
|
|
433
|
+
result = { defined: true, value };
|
|
434
|
+
} catch (error) {
|
|
435
|
+
result = {
|
|
436
|
+
threw: true,
|
|
437
|
+
message: error instanceof Error ? error.message : String(error),
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
return substitute(
|
|
441
|
+
{
|
|
442
|
+
result,
|
|
443
|
+
nowCalls: spec.nows === undefined ? null : nowCalls,
|
|
444
|
+
statusCalls: spec.statuses === undefined ? null : statusCalls,
|
|
445
|
+
readCalls: spec.logs === undefined ? null : readCalls,
|
|
446
|
+
},
|
|
447
|
+
dir,
|
|
448
|
+
dirPlaceholder
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// --- stop ------------------------------------------------------------------------
|
|
453
|
+
|
|
454
|
+
function maskStatusPid(result, pid) {
|
|
455
|
+
if (result.defined !== true || pid === undefined) {
|
|
456
|
+
return result;
|
|
457
|
+
}
|
|
458
|
+
const value = result.value;
|
|
459
|
+
if (value.record === undefined || value.record.pid !== pid) {
|
|
460
|
+
return result;
|
|
461
|
+
}
|
|
462
|
+
return {
|
|
463
|
+
defined: true,
|
|
464
|
+
value: { ...value, record: { ...value.record, pid: '<PID>' } },
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
async function handleStop(testCase) {
|
|
469
|
+
const dir = scratch();
|
|
470
|
+
const spec = substitute(testCase, dirPlaceholder, dir);
|
|
471
|
+
let child;
|
|
472
|
+
let childExited = null;
|
|
473
|
+
if (spec.mode === 'liveChild') {
|
|
474
|
+
child = spawn(process.execPath, ['-e', 'setInterval(() => {}, 1000)'], {
|
|
475
|
+
stdio: 'ignore',
|
|
476
|
+
});
|
|
477
|
+
// Let the exec settle so the real ps probe sees the command line.
|
|
478
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
479
|
+
const record = {
|
|
480
|
+
runnerId: spec.runnerId,
|
|
481
|
+
pid: child.pid,
|
|
482
|
+
cwd: '/',
|
|
483
|
+
logFile: '/nonexistent-daemon.log',
|
|
484
|
+
startedAt: '2026-07-01T00:00:00.000Z',
|
|
485
|
+
command: [],
|
|
486
|
+
};
|
|
487
|
+
writeFileSync(
|
|
488
|
+
commanderStatusFile(spec.runnerId, dir),
|
|
489
|
+
JSON.stringify(record, null, 2) + '\\n'
|
|
490
|
+
);
|
|
491
|
+
} else if (spec.contents !== undefined) {
|
|
492
|
+
writeFileSync(commanderStatusFile(spec.runnerId, dir), spec.contents);
|
|
493
|
+
}
|
|
494
|
+
const result = outcome(() => statusJson(stopCommanderDaemon(spec.runnerId, dir)));
|
|
495
|
+
if (child !== undefined) {
|
|
496
|
+
childExited = await new Promise((resolve) => {
|
|
497
|
+
if (child.exitCode !== null || child.signalCode !== null) {
|
|
498
|
+
resolve(true);
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
const timer = setTimeout(() => resolve(false), 5000);
|
|
502
|
+
child.on('exit', () => {
|
|
503
|
+
clearTimeout(timer);
|
|
504
|
+
resolve(true);
|
|
505
|
+
});
|
|
506
|
+
});
|
|
507
|
+
try {
|
|
508
|
+
child.kill('SIGKILL');
|
|
509
|
+
} catch {
|
|
510
|
+
// already exited - the expected disposition.
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
return substitute(
|
|
514
|
+
{ result: maskStatusPid(result, child?.pid), childExited },
|
|
515
|
+
dir,
|
|
516
|
+
dirPlaceholder
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// --- Dispatch --------------------------------------------------------------------
|
|
521
|
+
|
|
522
|
+
async function handle(testCase) {
|
|
523
|
+
switch (testCase.op) {
|
|
524
|
+
case 'paths':
|
|
525
|
+
return handlePaths(testCase);
|
|
526
|
+
case 'start':
|
|
527
|
+
return handleStart(testCase);
|
|
528
|
+
case 'status':
|
|
529
|
+
return handleStatus(testCase);
|
|
530
|
+
case 'wait':
|
|
531
|
+
return handleWait(testCase);
|
|
532
|
+
case 'stop':
|
|
533
|
+
return handleStop(testCase);
|
|
534
|
+
default:
|
|
535
|
+
return { threw: true, message: 'unknown op ' + String(testCase.op) };
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
let stdin = '';
|
|
540
|
+
process.stdin.setEncoding('utf8');
|
|
541
|
+
for await (const chunk of process.stdin) {
|
|
542
|
+
stdin += chunk;
|
|
543
|
+
}
|
|
544
|
+
const cases = JSON.parse(stdin);
|
|
545
|
+
const results = [];
|
|
546
|
+
for (const testCase of cases) {
|
|
547
|
+
results.push(await handle(testCase));
|
|
548
|
+
}
|
|
549
|
+
process.stdout.write(JSON.stringify(results));
|
|
550
|
+
`;
|
|
551
|
+
|
|
552
|
+
await mkdir(join(packageRoot, '.differential'), { recursive: true });
|
|
553
|
+
|
|
554
|
+
await build({
|
|
555
|
+
stdin: {
|
|
556
|
+
contents: driverSource,
|
|
557
|
+
resolveDir: packageRoot,
|
|
558
|
+
sourcefile: 'core-daemon-parity-driver.ts',
|
|
559
|
+
loader: 'ts',
|
|
560
|
+
},
|
|
561
|
+
bundle: true,
|
|
562
|
+
platform: 'node',
|
|
563
|
+
target: 'node20',
|
|
564
|
+
format: 'esm',
|
|
565
|
+
outfile: join(packageRoot, '.differential/core-daemon-parity-oracle.mjs'),
|
|
566
|
+
minify: false,
|
|
567
|
+
sourcemap: false,
|
|
568
|
+
legalComments: 'none',
|
|
569
|
+
// commanderDaemon.ts pulls only the runnerLogger/namingDualEmit slice of
|
|
570
|
+
// the commander graph; the runtime externals stay aligned with the other
|
|
571
|
+
// cluster oracles so a future graph widening cannot silently inline them.
|
|
572
|
+
external: ['ws', 'undici', 'blessed', '@anthropic-ai/claude-agent-sdk'],
|
|
573
|
+
});
|