kijito-tools 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +6 -0
  3. package/README.md +220 -0
  4. package/bin/cli.js +34 -0
  5. package/install.sh +161 -0
  6. package/package.json +50 -0
  7. package/providers/_shared/wake-core.mjs +358 -0
  8. package/providers/claude/CLAUDE.md.snippet +31 -0
  9. package/providers/claude/install.sh +186 -0
  10. package/providers/claude/scripts/arm-session.sh +66 -0
  11. package/providers/claude/scripts/claude-armed.sh +101 -0
  12. package/providers/claude/scripts/heartbeat-watchdog.sh +209 -0
  13. package/providers/claude/scripts/inbox-selftest.sh +277 -0
  14. package/providers/claude/scripts/kijito-persona-lib.sh +94 -0
  15. package/providers/claude/scripts/kijito-qa-pass.sh +12 -0
  16. package/providers/claude/scripts/lifecycle-lib.sh +167 -0
  17. package/providers/claude/scripts/myctx.sh +46 -0
  18. package/providers/claude/scripts/self-clear.sh +134 -0
  19. package/providers/claude/scripts/session-autosend.sh +85 -0
  20. package/providers/claude/scripts/session-catchup-hint.sh +330 -0
  21. package/providers/claude/scripts/statusline-context.sh +67 -0
  22. package/providers/claude/skills/kijito-qa-memory/SKILL.md +110 -0
  23. package/providers/claude/skills/kijito-recall/SKILL.md +53 -0
  24. package/providers/claude/skills/kijito-start/SKILL.md +122 -0
  25. package/providers/claude/wiring/README.md +93 -0
  26. package/providers/claude/wiring/kijito-heartbeat@.service +21 -0
  27. package/providers/codex/README.md +75 -0
  28. package/providers/codex/codex-kijito-parity-plan.md +265 -0
  29. package/providers/codex/docs-codex-setup.md +64 -0
  30. package/providers/codex/install.mjs +184 -0
  31. package/providers/codex/n0-capability-probe-protocol.md +472 -0
  32. package/providers/codex/n0-harness/README.md +57 -0
  33. package/providers/codex/n0-harness/cli.mjs +58 -0
  34. package/providers/codex/n0-harness/evidence-manifest.mjs +77 -0
  35. package/providers/codex/n0-harness/fixture.mjs +279 -0
  36. package/providers/codex/n0-harness/lib.mjs +141 -0
  37. package/providers/codex/n0-harness/manifest.mjs +36 -0
  38. package/providers/codex/n0-harness/oracle.mjs +464 -0
  39. package/providers/codex/n0-harness/parser.mjs +200 -0
  40. package/providers/codex/n0-harness/prompt.mjs +43 -0
  41. package/providers/codex/n0-harness/snapshot.mjs +120 -0
  42. package/providers/codex/n0-harness/specimen.mjs +37 -0
  43. package/providers/codex/notify/kijito-notify-count.mjs +117 -0
  44. package/providers/codex/notify/kijito-notify-count.test.mjs +76 -0
  45. package/providers/codex/plans/gate4-battery-log.md +184 -0
  46. package/providers/codex/plans/gate4-battery-protocol.md +43 -0
  47. package/providers/codex/plans/hive-user-first-plan-QA.md +34 -0
  48. package/providers/codex/plans/hive-user-first-plan.md +247 -0
  49. package/providers/codex/release-manifest.json +53 -0
  50. package/providers/codex/same-chat-continuation-plan-gate.md +235 -0
  51. package/providers/codex/same-chat-continuation-plan.md +458 -0
  52. package/providers/codex/skills/kijito-qa-memory/SKILL.md +193 -0
  53. package/providers/codex/skills/kijito-qa-memory/agents/openai.yaml +4 -0
  54. package/providers/codex/skills/kijito-recall/SKILL.md +53 -0
  55. package/providers/codex/skills/kijito-start/SKILL.md +173 -0
  56. package/providers/codex/skills/kijito-start/agents/openai.yaml +4 -0
  57. package/providers/codex/test/n0-cli-refusal.test.mjs +31 -0
  58. package/providers/codex/test/n0-counterexample-matrix.test.mjs +118 -0
  59. package/providers/codex/test/n0-guard-census-core.mjs +1372 -0
  60. package/providers/codex/test/n0-guard-census.json +35479 -0
  61. package/providers/codex/test/n0-guard-census.mjs +31 -0
  62. package/providers/codex/test/n0-guard-census.test.mjs +374 -0
  63. package/providers/codex/test/n0-guard-counterexamples.mjs +1068 -0
  64. package/providers/codex/test/n0-guard-independent-projection.mjs +70 -0
  65. package/providers/codex/test/n0-guard-independent-projection.test.mjs +53 -0
  66. package/providers/codex/test/n0-guard-manifest-author.mjs +424 -0
  67. package/providers/codex/test/n0-guard-mutation-operators.test.mjs +244 -0
  68. package/providers/codex/test/n0-guard-mutation-runner.mjs +599 -0
  69. package/providers/codex/test/n0-guard-remainder-classes.test.mjs +314 -0
  70. package/providers/codex/test/n0-guard-sanitizer-pairs.test.mjs +68 -0
  71. package/providers/codex/test/n0-guard-shared-context-predicates.test.mjs +84 -0
  72. package/providers/codex/test/n0-harness.test.mjs +553 -0
  73. package/providers/codex/test/n0-notable-regressions.test.mjs +70 -0
  74. package/providers/codex/test/n0-subject-boundary.mjs +97 -0
  75. package/providers/codex/test/n0-subject-boundary.test.mjs +72 -0
  76. package/providers/codex/test/same-chat-plan-preflight.sh +167 -0
  77. package/providers/codex/tools/refresh-manifest.mjs +81 -0
  78. package/providers/codex/wake-helper/TRANSPORT-NOTES.md +25 -0
  79. package/providers/codex/wake-helper/integration.test.mjs +285 -0
  80. package/providers/codex/wake-helper/kijito-wake-helper.mjs +488 -0
  81. package/providers/codex/wake-helper/kijito-wake-helper.test.mjs +128 -0
  82. package/providers/codex/wake-helper/mock-daemon.mjs +64 -0
  83. package/providers/codex/wake-helper/status-probe.mjs +45 -0
  84. package/providers/codex/wake-helper/ws-uds.mjs +153 -0
  85. package/providers/monitor/.github/workflows/publish-npm.yml +38 -0
  86. package/providers/monitor/.github/workflows/publish-pypi.yml +30 -0
  87. package/providers/monitor/CHANGELOG.md +465 -0
  88. package/providers/monitor/IMPORT-PROVENANCE.md +68 -0
  89. package/providers/monitor/LICENSE +202 -0
  90. package/providers/monitor/NOTICE +7 -0
  91. package/providers/monitor/OPAQUE-OUTPUT-ENFORCEMENT.md +45 -0
  92. package/providers/monitor/README.md +571 -0
  93. package/providers/monitor/RELEASING.md +125 -0
  94. package/providers/monitor/arm-hive-monitor.sh +13 -0
  95. package/providers/monitor/bin/cli.js +43 -0
  96. package/providers/monitor/com.kijito.inbox-monitor.plist.template +59 -0
  97. package/providers/monitor/docs/DESIGN.md +984 -0
  98. package/providers/monitor/kijito-inbox-monitor@.service.template +95 -0
  99. package/providers/monitor/kijito_inbox_monitor.py +3666 -0
  100. package/providers/monitor/package.json +35 -0
  101. package/providers/monitor/pyproject.toml +42 -0
  102. package/providers/monitor/scripts/mutation-check.py +399 -0
  103. package/providers/monitor/scripts/prepublish-gate.sh +284 -0
  104. package/providers/monitor/test_kijito_monitor.py +5609 -0
@@ -0,0 +1,285 @@
1
+ // Integration tests: the helper `run` loop against the mock daemon — every §6-relevant
2
+ // behavior the battery will later prove on the REAL daemon, proven first in isolation:
3
+ // doorbell→wake with the fixed _shared text, defer-until-idle, double-arm idempotence,
4
+ // and the loud exits (daemon absent 3, producer absent/vanished 4, thread gone 5).
5
+ import test from "node:test";
6
+ import assert from "node:assert/strict";
7
+ import fs from "node:fs";
8
+ import os from "node:os";
9
+ import path from "node:path";
10
+ import { spawn } from "node:child_process";
11
+ import { once } from "node:events";
12
+ import { MockDaemon } from "./mock-daemon.mjs";
13
+ import { WAKE_PREFIX } from "../../_shared/wake-core.mjs";
14
+
15
+ const HELPER = new URL("./kijito-wake-helper.mjs", import.meta.url).pathname;
16
+
17
+ function mkEnv(name) {
18
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), `wake-int-${name}-`));
19
+ const events = path.join(dir, "events.ndjson");
20
+ fs.writeFileSync(events, "");
21
+ return { dir, events, sock: path.join(dir, "d.sock"), runtime: path.join(dir, "rt") };
22
+ }
23
+
24
+ function startHelper(env, threadId = "T1", extra = []) {
25
+ const child = spawn(process.execPath, [HELPER, "run",
26
+ "--persona", "codex", "--thread-id", threadId, "--events", env.events,
27
+ "--sock", env.sock, "--runtime", env.runtime, ...extra], { stdio: ["ignore", "pipe", "pipe"] });
28
+ child.stdoutText = ""; child.stderrText = "";
29
+ child.stdout.on("data", (d) => { child.stdoutText += d; });
30
+ child.stderr.on("data", (d) => { child.stderrText += d; });
31
+ return child;
32
+ }
33
+
34
+ const waitFor = async (fn, ms = 5_000, step = 50) => {
35
+ const end = Date.now() + ms;
36
+ while (Date.now() < end) {
37
+ if (fn()) return true;
38
+ await new Promise((r) => setTimeout(r, step));
39
+ }
40
+ return false;
41
+ };
42
+
43
+ const mailLine = (id) => JSON.stringify({ source: "kijito-inbox", persona: "codex", event: "new", id }) + "\n";
44
+
45
+ test("doorbell on idle thread -> one turn/start with the fixed wake text", async () => {
46
+ const env = mkEnv("happy");
47
+ const daemon = new MockDaemon(env.sock);
48
+ await daemon.listen();
49
+ const child = startHelper(env);
50
+ assert.ok(await waitFor(() => child.stdoutText.includes('"event":"armed"')), `no armed: ${child.stderrText}`);
51
+ fs.appendFileSync(env.events, mailLine(101));
52
+ assert.ok(await waitFor(() => daemon.turnStarts().length === 1), "wake not delivered");
53
+ const turn = daemon.turnStarts()[0];
54
+ assert.equal(turn.params.threadId, "T1");
55
+ const text = turn.params.input[0].text;
56
+ assert.ok(text.startsWith(WAKE_PREFIX));
57
+ assert.match(text, /Message IDs: 101/);
58
+ child.kill("SIGKILL");
59
+ await daemon.close();
60
+ });
61
+
62
+ test("busy thread defers; idle notification releases the queued wake", async () => {
63
+ const env = mkEnv("defer");
64
+ const daemon = new MockDaemon(env.sock, { initialStatus: "active" });
65
+ await daemon.listen();
66
+ const child = startHelper(env);
67
+ assert.ok(await waitFor(() => child.stdoutText.includes('"event":"armed"')));
68
+ fs.appendFileSync(env.events, mailLine(202));
69
+ assert.ok(await waitFor(() => child.stdoutText.includes('"event":"doorbell"')));
70
+ await new Promise((r) => setTimeout(r, 1200)); // longer than a poll+recheck cycle
71
+ assert.equal(daemon.turnStarts().length, 0, "must not stomp a busy thread");
72
+ daemon.setStatus("idle");
73
+ assert.ok(await waitFor(() => daemon.turnStarts().length === 1), "deferred wake never delivered");
74
+ assert.match(daemon.turnStarts()[0].params.input[0].text, /Message IDs: 202/);
75
+ child.kill("SIGKILL");
76
+ await daemon.close();
77
+ });
78
+
79
+ test("daemon socket absent -> loud exit 3", async () => {
80
+ const env = mkEnv("nosock");
81
+ const child = startHelper(env);
82
+ const [code] = await once(child, "exit");
83
+ assert.equal(code, 3);
84
+ assert.match(child.stdoutText, /daemon-unavailable/);
85
+ });
86
+
87
+ test("events stream absent -> loud exit 4", async () => {
88
+ const env = mkEnv("noevents");
89
+ fs.unlinkSync(env.events);
90
+ const daemon = new MockDaemon(env.sock);
91
+ await daemon.listen();
92
+ const child = startHelper(env);
93
+ const [code] = await once(child, "exit");
94
+ assert.equal(code, 4);
95
+ assert.match(child.stdoutText, /producer-stream-absent/);
96
+ await daemon.close();
97
+ });
98
+
99
+ test("thread rejected on resume -> loud exit 5", async () => {
100
+ const env = mkEnv("badthread");
101
+ const daemon = new MockDaemon(env.sock, { failResume: true });
102
+ await daemon.listen();
103
+ const child = startHelper(env);
104
+ const [code] = await once(child, "exit");
105
+ assert.equal(code, 5);
106
+ assert.match(child.stdoutText, /thread-gone/);
107
+ await daemon.close();
108
+ });
109
+
110
+ test("producer stream vanishing mid-run -> gasp turn + loud exit 4", async () => {
111
+ const env = mkEnv("vanish");
112
+ const daemon = new MockDaemon(env.sock);
113
+ await daemon.listen();
114
+ const child = startHelper(env);
115
+ assert.ok(await waitFor(() => child.stdoutText.includes('"event":"armed"')));
116
+ fs.unlinkSync(env.events);
117
+ const [code] = await once(child, "exit");
118
+ assert.equal(code, 4);
119
+ assert.match(child.stdoutText, /producer-stream-vanished/);
120
+ const gasps = daemon.turnStarts().filter((t) => t.params.input[0].text.includes("KIJITO WAKE HELPER NOTICE"));
121
+ assert.equal(gasps.length, 1, "death must be visible in-session (assay §6-row-5 ruling)");
122
+ assert.match(gasps[0].params.input[0].text, /catch-up-only/);
123
+ await daemon.close();
124
+ });
125
+
126
+ test("second run on the same thread -> already-armed, exit 0 (double-arm probe shape)", async () => {
127
+ const env = mkEnv("double");
128
+ const daemon = new MockDaemon(env.sock);
129
+ await daemon.listen();
130
+ const first = startHelper(env);
131
+ assert.ok(await waitFor(() => first.stdoutText.includes('"event":"armed"')));
132
+ const second = startHelper(env);
133
+ const [code] = await once(second, "exit");
134
+ assert.equal(code, 0);
135
+ assert.match(second.stdoutText, /already-armed/);
136
+ first.kill("SIGKILL");
137
+ await daemon.close();
138
+ });
139
+
140
+ test("live helper on a DIFFERENT thread -> refuse exit 6, first helper untouched", async () => {
141
+ const env = mkEnv("otherthread");
142
+ const daemon = new MockDaemon(env.sock);
143
+ await daemon.listen();
144
+ const first = startHelper(env, "T1");
145
+ assert.ok(await waitFor(() => first.stdoutText.includes('"event":"armed"')));
146
+ const second = startHelper(env, "T2");
147
+ const [code] = await once(second, "exit");
148
+ assert.equal(code, 6);
149
+ assert.match(second.stderrText, /live-helper-other-thread/);
150
+ assert.equal(first.exitCode, null, "first helper must stay alive");
151
+ first.kill("SIGKILL");
152
+ await daemon.close();
153
+ });
154
+
155
+ test("owned producer child dies (file remains) -> gasp + loud exit 4 (gate-3 A-4 binding control)", async () => {
156
+ const env = mkEnv("proddeath");
157
+ const daemon = new MockDaemon(env.sock);
158
+ await daemon.listen();
159
+ // A stand-in producer that appends nothing and sleeps; the events file already exists, so
160
+ // its death leaves the classic silent-tail scenario — which must now be LOUD.
161
+ const child = startHelper(env, "T1", ["--producer-cmd", `${process.execPath} -e setTimeout(()=>{},60000)`]);
162
+ assert.ok(await waitFor(() => child.stdoutText.includes('"event":"armed"')), child.stdoutText);
163
+ // Find and kill the producer child (the helper's only node child with the sleep script).
164
+ const rec = JSON.parse(fs.readFileSync(path.join(env.runtime, "helper-codex.pid"), "utf8"));
165
+ assert.equal(rec.pid, child.pid);
166
+ fs.appendFileSync(env.events, ""); // file remains — the trap shape
167
+ // Kill the producer: it is the helper's child; find it via ps parentage.
168
+ const { execSync } = await import("node:child_process");
169
+ // pgrep -P is portable (BSD + GNU); `ps --ppid` is GNU-only and dies on macOS.
170
+ // pgrep exits 1 on no-match, which for "list the children" simply means none.
171
+ let kidsOut = "";
172
+ try { kidsOut = execSync(`pgrep -P ${child.pid}`).toString(); } catch { kidsOut = ""; }
173
+ const kids = kidsOut.trim().split("\n").map((s) => Number(s.trim())).filter(Boolean);
174
+ assert.ok(kids.length >= 1, "helper should own a producer child");
175
+ process.kill(kids[0], "SIGKILL");
176
+ const [code] = await once(child, "exit");
177
+ assert.equal(code, 4);
178
+ assert.match(child.stdoutText, /producer-child-died/);
179
+ const gasps = daemon.turnStarts().filter((t) => t.params.input[0].text.includes("KIJITO WAKE HELPER NOTICE"));
180
+ assert.equal(gasps.length, 1, "producer death must be visible in-session");
181
+ await daemon.close();
182
+ });
183
+
184
+ test("SIGTERM -> gasp + pidfile cleanup (graceful path of §6 row 5)", async () => {
185
+ const env = mkEnv("sigterm");
186
+ const daemon = new MockDaemon(env.sock);
187
+ await daemon.listen();
188
+ const child = startHelper(env);
189
+ assert.ok(await waitFor(() => child.stdoutText.includes('"event":"armed"')));
190
+ child.kill("SIGTERM");
191
+ const [code] = await once(child, "exit");
192
+ assert.equal(code, 0);
193
+ const gasps = daemon.turnStarts().filter((t) => t.params.input[0].text.includes("KIJITO WAKE HELPER NOTICE"));
194
+ assert.equal(gasps.length, 1);
195
+ assert.equal(fs.existsSync(path.join(env.runtime, "helper-codex.pid")), false, "pidfile must be reaped");
196
+ await daemon.close();
197
+ });
198
+
199
+ // ── F1 regression (argus PR#19 review): a stale "armed" line from a PREVIOUS run in the
200
+ // persistent ndjson must never verify a NEW arm whose child is still attaching (or failing
201
+ // slowly). Pre-fix bytes report "armed pid=<new>" off the old record at the first 300ms
202
+ // tick; fixed bytes scan only post-spawn appended bytes AND bind on the child's own pid.
203
+ test("F1: stale armed line from a previous run never verifies a new arm", async () => {
204
+ const { createServer } = await import("node:net");
205
+ const env = mkEnv("f1stale");
206
+ fs.mkdirSync(env.runtime, { recursive: true });
207
+ // A previous run's armed record: same thread, dead pid, no helper-exit after it (the
208
+ // SIGKILL residue shape the battery documented).
209
+ fs.writeFileSync(path.join(env.runtime, "helper-codex.ndjson"),
210
+ JSON.stringify({ ts: "2026-08-15T00:00:00.000Z", event: "armed", pid: 424242, threadId: "T1", eventsFile: env.events, offset: 0 }) + "\n");
211
+ // A socket that ACCEPTS but never completes the WS handshake: the new child hangs in
212
+ // connect — the exact "still attaching" window the false-arm needs.
213
+ const hang = createServer(() => { /* accept, say nothing */ });
214
+ await new Promise((r) => hang.listen(env.sock, r));
215
+ const wrapper = spawn(process.execPath, [HELPER, "arm",
216
+ "--persona", "codex", "--thread-id", "T1", "--events", env.events,
217
+ "--sock", env.sock, "--runtime", env.runtime], { stdio: ["ignore", "pipe", "pipe"] });
218
+ wrapper.stdoutText = ""; wrapper.stderrText = "";
219
+ wrapper.stdout.on("data", (d) => { wrapper.stdoutText += d; });
220
+ wrapper.stderr.on("data", (d) => { wrapper.stderrText += d; });
221
+ const [code] = await once(wrapper, "exit");
222
+ assert.notEqual(code, 0, `wrapper must not verify: stdout=${wrapper.stdoutText}`);
223
+ assert.ok(!wrapper.stdoutText.includes("armed pid="), `false-arm off stale record: ${wrapper.stdoutText}`);
224
+ hang.close();
225
+ // Reap the hung run child so no orphan outlives the test.
226
+ try {
227
+ const kids = (await import("node:child_process")).execSync(`pgrep -P ${wrapper.pid} 2>/dev/null || true`).toString().trim();
228
+ for (const k of kids.split("\n").map(Number).filter(Boolean)) process.kill(k, "SIGKILL");
229
+ } catch { /* already gone */ }
230
+ try { (await import("node:child_process")).execSync(`pkill -f "run --persona codex --thread-id T1 --events ${env.events}" 2>/dev/null || true`); } catch { /* gone */ }
231
+ });
232
+
233
+ // ── F2 regression (argus PR#19 review): a mail line torn EXACTLY across the 256KB read cap
234
+ // must still deliver, exactly once. Pre-fix bytes advance offset past the torn head and the
235
+ // event silently vanishes; fixed bytes consume only through the last complete line.
236
+ test("F2: line torn across the 256KB read cap delivers exactly once", async () => {
237
+ const CAP = 256 * 1024;
238
+ const env = mkEnv("f2torn");
239
+ const daemon = new MockDaemon(env.sock);
240
+ await daemon.listen();
241
+ const child = startHelper(env);
242
+ assert.ok(await waitFor(() => child.stdoutText.includes('"event":"armed"')), `no armed: ${child.stderrText}`);
243
+ const mail = mailLine(505);
244
+ // Filler of IGNORABLE (wrong-persona) lines sized so the mail line straddles byte CAP:
245
+ // total filler length L with L < CAP < L + mail.length (cut lands 10 bytes into the mail).
246
+ const L = CAP - 10;
247
+ const unit = JSON.stringify({ source: "kijito-inbox", persona: "other", event: "new", id: 1 }) + "\n";
248
+ let filler = unit.repeat(Math.floor((L - 200) / unit.length));
249
+ const padLen = L - filler.length;
250
+ const padObj = `{"source":"kijito-inbox","persona":"other","event":"new","id":1,"pad":"${"x".repeat(Math.max(0, padLen - 72))}"}\n`;
251
+ filler += padObj + " ".repeat(L - filler.length - padObj.length >= 0 ? 0 : 0);
252
+ // Exactness of L matters only to within the mail line's length; assert the straddle holds.
253
+ assert.ok(filler.length < CAP && filler.length + mail.length > CAP,
254
+ `straddle broken: filler=${filler.length} mail=${mail.length} cap=${CAP}`);
255
+ fs.appendFileSync(env.events, filler + mail);
256
+ assert.ok(await waitFor(() => daemon.turnStarts().length >= 1, 8_000), "torn-line event never delivered");
257
+ await new Promise((r) => setTimeout(r, 1_200));
258
+ assert.equal(daemon.turnStarts().length, 1, "must deliver exactly once");
259
+ assert.match(daemon.turnStarts()[0].params.input[0].text, /Message IDs: 505/);
260
+ child.kill("SIGKILL");
261
+ await daemon.close();
262
+ });
263
+
264
+ test("gate-7: a diagnostic-kind event delivers an alert-shaped wake end-to-end", async () => {
265
+ const env = mkEnv("diag");
266
+ const daemon = new MockDaemon(env.sock);
267
+ await daemon.listen();
268
+ const child = startHelper(env);
269
+ assert.ok(await waitFor(() => child.stdoutText.includes('"event":"armed"')), `no armed: ${child.stderrText}`);
270
+ // Shape measured on a live producer 2026-08-15 (g7 fixtures): diagnostic kinds carry ts, no id.
271
+ fs.appendFileSync(env.events, JSON.stringify({
272
+ source: "kijito-inbox", persona: "codex", event: "baseline_skipped",
273
+ ts: "2026-08-15T08:04:46.059708+00:00", wake_class: "diagnostic",
274
+ }) + "\n");
275
+ assert.ok(await waitFor(() => daemon.turnStarts().length === 1), "diagnostic wake not delivered");
276
+ const text = daemon.turnStarts()[0].params.input[0].text;
277
+ assert.ok(text.startsWith(WAKE_PREFIX));
278
+ assert.match(text, /Events: baseline_skipped/);
279
+ assert.match(text, /Diagnostics: baseline_skipped:2026-08-15T08:04:46\.059708\+00:00/);
280
+ assert.match(text, /Message IDs: none/);
281
+ // the helper's own log carries the lifecycle batch key — the successor instrument's grammar
282
+ assert.ok(await waitFor(() => child.stdoutText.includes('"batch":["baseline_skipped:2026-08-15T08:04:46.059708+00:00"]')), "batch key absent from helper log");
283
+ child.kill("SIGKILL");
284
+ await daemon.close();
285
+ });