@naswerks/engineering-loop 0.7.0 → 0.8.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 (37) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +125 -0
  4. package/README.md +12 -7
  5. package/bin/verify-staged.mjs +136 -23
  6. package/bin/witness-acts.mjs +64 -0
  7. package/bin/witness-bodycheck.mjs +78 -0
  8. package/bin/witness-edits.mjs +60 -0
  9. package/bin/witness-extract-cmd.mjs +57 -0
  10. package/bin/witness-extract-seed.mjs +65 -0
  11. package/bin/witness-file-ledger.mjs +57 -0
  12. package/bin/witness-gaps.mjs +57 -0
  13. package/bin/witness-latency.mjs +55 -0
  14. package/bin/witness-lib.mjs +124 -0
  15. package/bin/witness-plane-window.mjs +57 -0
  16. package/bin/witness-procs.mjs +71 -0
  17. package/bin/witness-spawn-task.mjs +61 -0
  18. package/bin/witness-tx.mjs +60 -0
  19. package/bin/witness-watch.mjs +405 -0
  20. package/bin/witness-writes.mjs +64 -0
  21. package/package.json +1 -1
  22. package/references/docs-workflow.md +44 -24
  23. package/references/engineering-loop.md +41 -24
  24. package/skills/docs-audit-full/SKILL.md +2 -1
  25. package/skills/docs-process/SKILL.md +13 -7
  26. package/skills/docs-write/SKILL.md +1 -1
  27. package/skills/init/scripts/doctor.mjs +17 -5
  28. package/skills/init/scripts/owned.mjs +10 -0
  29. package/skills/spec-build/SKILL.md +8 -4
  30. package/skills/spec-coordinator/SKILL.md +125 -66
  31. package/skills/spec-ignite/SKILL.md +25 -21
  32. package/skills/spec-pipeline/SKILL.md +128 -80
  33. package/skills/spec-retro/SKILL.md +9 -4
  34. package/skills/spec-review/SKILL.md +22 -10
  35. package/skills/spec-seat/SKILL.md +63 -31
  36. package/skills/spec-witness/SKILL.md +267 -212
  37. package/templates/docs/readme-research.md +2 -2
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env node
2
+ // One tool call's command, written to a file byte for byte, with its line count and every non-ASCII
3
+ // character named: a refused command is often explained by one character a terminal never shows.
4
+ // Writes only <outFile>.
5
+ //
6
+ // node witness-extract-cmd.mjs <file.jsonl> <toolUseId> <outFile>
7
+ // node witness-extract-cmd.mjs --self-test
8
+
9
+ import { readFileSync, rmSync, writeFileSync } from 'node:fs';
10
+ import { join } from 'node:path';
11
+ import { contentOf, entries, isMain, positionals, report, scratch, toolUse, writeTranscript } from './witness-lib.mjs';
12
+
13
+ export async function extractCmd(file, id, outFile) {
14
+ for await (const { entry } of entries(file)) {
15
+ for (const c of contentOf(entry)) {
16
+ if (c.type !== 'tool_use' || c.id !== id) continue;
17
+ const command = String(c.input?.command ?? '');
18
+ writeFileSync(outFile, command);
19
+ const lines = command.split('\n');
20
+ const out = [`${lines.length} line(s), ${Buffer.byteLength(command)} bytes -> ${outFile}`];
21
+ lines.forEach((line, i) => {
22
+ const odd = [...line].filter((ch) => ch.charCodeAt(0) > 127);
23
+ if (odd.length) out.push(`non-ascii on line ${i + 1}: ${odd.map((ch) => `U+${ch.codePointAt(0).toString(16).toUpperCase().padStart(4, '0')}`).join(' ')}`);
24
+ });
25
+ return out;
26
+ }
27
+ }
28
+ return [`no tool call ${id} in ${file}`];
29
+ }
30
+
31
+ async function selfTest() {
32
+ const dir = scratch('witness-extract-cmd');
33
+ try {
34
+ const command = 'cat >> tldr.md <<\'EOF\'\nC:\\work\\path and a dash — here\nEOF';
35
+ const file = writeTranscript(dir, 'seat.jsonl', [
36
+ toolUse(1, 'toolu_one', 'Bash', { command: 'git status' }),
37
+ toolUse(2, 'toolu_two', 'Bash', { command }),
38
+ ]);
39
+ const out = join(dir, 'command.txt');
40
+ const lines = await extractCmd(file, 'toolu_two', out);
41
+ return report('witness-extract-cmd', [
42
+ { name: 'the command is written byte for byte, backslashes included', ok: readFileSync(out, 'utf8') === command, detail: JSON.stringify(readFileSync(out, 'utf8')) },
43
+ { name: 'its non-ASCII character is named with its line', ok: lines.some((l) => l === 'non-ascii on line 2: U+2014'), detail: lines.join(' | ') },
44
+ { name: 'CONTROL: an id not in the transcript says so', ok: (await extractCmd(file, 'toolu_none', out))[0].startsWith('no tool call'), detail: '' },
45
+ ]);
46
+ } finally {
47
+ rmSync(dir, { recursive: true, force: true });
48
+ }
49
+ }
50
+
51
+ if (isMain(import.meta.url)) {
52
+ const argv = process.argv.slice(2);
53
+ if (argv.includes('--self-test')) process.exit(await selfTest());
54
+ const [file, id, outFile] = positionals(argv);
55
+ if (!file || !id || !outFile) { console.log('usage: witness-extract-cmd.mjs <file.jsonl> <toolUseId> <outFile>'); process.exit(2); }
56
+ for (const line of await extractCmd(file, id, outFile)) console.log(line);
57
+ }
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env node
2
+ // A session's seed (its first user message) and its prompt snapshots, saved verbatim. The seed is what the
3
+ // session was told; the policy text a control plane appends to the system prompt lives in the
4
+ // `prompt_snapshot` attachments, not in the seed. Writes <outPrefix>-seed-as-kicked.txt and
5
+ // <outPrefix>-prompt-snapshots.txt.
6
+ //
7
+ // node witness-extract-seed.mjs <file.jsonl> <outPrefix>
8
+ // node witness-extract-seed.mjs --self-test
9
+
10
+ import { readFileSync, rmSync, writeFileSync } from 'node:fs';
11
+ import { join } from 'node:path';
12
+ import { at, entries, isMain, positionals, report, scratch, userText, writeTranscript } from './witness-lib.mjs';
13
+
14
+ // A user message shorter than this is a command or a meta line, never a seed.
15
+ const SEED_FLOOR = 200;
16
+
17
+ export async function extractSeed(file, outPrefix) {
18
+ let seed = null;
19
+ const snapshots = [];
20
+ for await (const { entry } of entries(file)) {
21
+ if (seed === null && entry.message?.role === 'user' && !entry.isMeta) {
22
+ const c = entry.message.content;
23
+ const text = typeof c === 'string' ? c : Array.isArray(c) ? c.filter((x) => x.type === 'text').map((x) => x.text).join('\n') : '';
24
+ if (text.length > SEED_FLOOR) seed = `${entry.timestamp}\n${text}`;
25
+ }
26
+ if (entry.type === 'attachment' && /prompt_snapshot/.test(JSON.stringify(entry.attachment?.type ?? ''))) {
27
+ snapshots.push(`${entry.timestamp}\n${JSON.stringify(entry.attachment, null, 1)}`);
28
+ }
29
+ }
30
+ writeFileSync(`${outPrefix}-seed-as-kicked.txt`, seed ?? '(no seed found)');
31
+ writeFileSync(`${outPrefix}-prompt-snapshots.txt`, snapshots.join('\n\n=====\n\n') || '(no prompt_snapshot attachments)');
32
+ return [`seed ${seed === null ? 0 : seed.length} chars; prompt snapshots ${snapshots.length}`];
33
+ }
34
+
35
+ async function selfTest() {
36
+ const dir = scratch('witness-extract-seed');
37
+ try {
38
+ const seed = `You are the COORDINATOR. ${'Read the brief and follow it. '.repeat(10)}`;
39
+ const file = writeTranscript(dir, 'seat.jsonl', [
40
+ userText(0, '<command-name>/clear</command-name>'),
41
+ userText(1, seed),
42
+ { type: 'attachment', timestamp: at(1), attachment: { type: 'prompt_snapshot', text: 'the policy suffix' } },
43
+ userText(9, `a later human message ${'x'.repeat(SEED_FLOOR)}`),
44
+ ]);
45
+ const prefix = join(dir, 'coordinator');
46
+ await extractSeed(file, prefix);
47
+ const saved = readFileSync(`${prefix}-seed-as-kicked.txt`, 'utf8');
48
+ const snapshots = readFileSync(`${prefix}-prompt-snapshots.txt`, 'utf8');
49
+ return report('witness-extract-seed', [
50
+ { name: 'the seed is the first user message long enough to be one, saved verbatim', ok: saved.endsWith(`\n${seed}`), detail: saved.slice(0, 80) },
51
+ { name: 'the prompt snapshot is saved', ok: snapshots.includes('the policy suffix'), detail: snapshots.slice(0, 80) },
52
+ { name: 'CONTROL: a short command line before it and a later message are not the seed', ok: !saved.includes('/clear') && !saved.includes('a later human message'), detail: saved.slice(0, 80) },
53
+ ]);
54
+ } finally {
55
+ rmSync(dir, { recursive: true, force: true });
56
+ }
57
+ }
58
+
59
+ if (isMain(import.meta.url)) {
60
+ const argv = process.argv.slice(2);
61
+ if (argv.includes('--self-test')) process.exit(await selfTest());
62
+ const [file, outPrefix] = positionals(argv);
63
+ if (!file || !outPrefix) { console.log('usage: witness-extract-seed.mjs <file.jsonl> <outPrefix>'); process.exit(2); }
64
+ for (const line of await extractSeed(file, outPrefix)) console.log(line);
65
+ }
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env node
2
+ // Files the witness ledger into the run's sessions folder, on the human's word: the ledger's body
3
+ // unedited, under the header the repository's § Doc metadata (repository-owned) prescribes. The header is
4
+ // the config's `ledgerHeader` (a string, or a list of lines) with `{date}`, `{pipeline}` and `{topic}`
5
+ // filled in; the ledger is `ledger.md` in the witness folder, or --ledger <path>. It refuses to overwrite.
6
+ //
7
+ // node witness-file-ledger.mjs <destPath> [--config <path>] [--ledger <path>]
8
+ // -> e.g. docs/research/{topic}/sessions/witness-ledger-{date}.md
9
+ // node witness-file-ledger.mjs --self-test
10
+
11
+ import { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
12
+ import { join } from 'node:path';
13
+ import { flag, isMain, loadConfig, positionals, report, scratch } from './witness-lib.mjs';
14
+
15
+ export function fileLedger(dest, config, ledgerPath, today = new Date().toISOString().slice(0, 10)) {
16
+ if (existsSync(dest)) return { ok: false, out: [`EXISTS, refusing to overwrite: ${dest}`] };
17
+ if (config.ledgerHeader === undefined) {
18
+ return { ok: false, out: ['no ledgerHeader in the config - the filed ledger needs the header the repository\'s § Doc metadata (repository-owned) prescribes'] };
19
+ }
20
+ const template = Array.isArray(config.ledgerHeader) ? config.ledgerHeader.join('\n') : String(config.ledgerHeader);
21
+ const header = template.replaceAll('{date}', today).replaceAll('{pipeline}', String(config.pipeline ?? '')).replaceAll('{topic}', String(config.topic ?? ''));
22
+ const body = readFileSync(ledgerPath, 'utf8').replace(/\r/g, '');
23
+ writeFileSync(dest, `${header}\n${body}`);
24
+ return { ok: true, out: [`wrote ${header.length + 1 + body.length} chars to ${dest}; body ${body.length} chars = the ledger, unedited`] };
25
+ }
26
+
27
+ function selfTest() {
28
+ const dir = scratch('witness-file-ledger');
29
+ try {
30
+ const ledger = join(dir, 'ledger.md');
31
+ writeFileSync(ledger, '# Witness ledger\n\nW1 the kick, by its act.\n');
32
+ const config = { pipeline: '42', topic: 'demo', ledgerHeader: ['---', 'title: "Witness ledger, pipeline {pipeline} ({date})"', 'status: deprecated', '---', ''] };
33
+ const dest = join(dir, 'witness-ledger.md');
34
+ const first = fileLedger(dest, config, ledger, 'the-date');
35
+ const filed = readFileSync(dest, 'utf8');
36
+ const second = fileLedger(dest, config, ledger, 'the-date');
37
+ const headless = fileLedger(join(dir, 'other.md'), { ...config, ledgerHeader: undefined }, ledger);
38
+ return report('witness-file-ledger', [
39
+ { name: 'the ledger is filed under the configured header, its body unedited', ok: first.ok && filed === '---\ntitle: "Witness ledger, pipeline 42 (the-date)"\nstatus: deprecated\n---\n\n# Witness ledger\n\nW1 the kick, by its act.\n', detail: JSON.stringify(filed) },
40
+ { name: 'CONTROL: a second filing refuses to overwrite', ok: !second.ok && second.out[0].startsWith('EXISTS'), detail: second.out[0] },
41
+ { name: 'CONTROL: a config with no header refuses rather than inventing one', ok: !headless.ok && !existsSync(join(dir, 'other.md')), detail: headless.out[0] },
42
+ ]);
43
+ } finally {
44
+ rmSync(dir, { recursive: true, force: true });
45
+ }
46
+ }
47
+
48
+ if (isMain(import.meta.url)) {
49
+ const argv = process.argv.slice(2);
50
+ if (argv.includes('--self-test')) process.exit(selfTest());
51
+ const [dest] = positionals(argv, ['--config', '--ledger']);
52
+ if (!dest) { console.log('usage: witness-file-ledger.mjs <destPath> [--config <path>] [--ledger <path>]'); process.exit(2); }
53
+ const config = loadConfig(argv, { required: true });
54
+ const { ok, out } = fileLedger(dest, config, flag(argv, '--ledger') ?? join(config.dir, 'ledger.md'));
55
+ for (const line of out) console.log(line);
56
+ process.exit(ok ? 0 : 1);
57
+ }
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env node
2
+ // Idle gaps between a transcript's consecutive entries, its first and last timestamps, and the entries a
3
+ // pattern matches. A transcript with no entry since T has not acted since T. Read-only.
4
+ //
5
+ // node witness-gaps.mjs <file.jsonl> <minGapMinutes> [pattern]
6
+ // node witness-gaps.mjs --self-test
7
+
8
+ import { rmSync } from 'node:fs';
9
+ import { assistantText, entries, isMain, positionals, report, scratch, userText, writeTranscript } from './witness-lib.mjs';
10
+
11
+ export async function gaps(file, min = 10, pattern = null) {
12
+ const re = pattern ? new RegExp(pattern) : null;
13
+ const out = [];
14
+ let prev = null;
15
+ let first = null;
16
+ for await (const { entry } of entries(file)) {
17
+ const ts = entry.timestamp;
18
+ if (!ts) continue;
19
+ first ??= ts;
20
+ const minutes = prev ? (Date.parse(ts) - Date.parse(prev)) / 60000 : 0;
21
+ if (prev && minutes >= min) out.push(`GAP ${minutes.toFixed(0)} min: ${prev} -> ${ts}`);
22
+ if (re) {
23
+ const hit = JSON.stringify(entry.message?.content ?? entry.content ?? '').match(re);
24
+ if (hit) out.push(`MATCH ${ts} ${hit[0].slice(0, 160)}`);
25
+ }
26
+ prev = ts;
27
+ }
28
+ out.push(`first ${first} last ${prev}`);
29
+ return out;
30
+ }
31
+
32
+ async function selfTest() {
33
+ const dir = scratch('witness-gaps');
34
+ try {
35
+ const file = writeTranscript(dir, 'seat.jsonl', [
36
+ userText(0, 'the seed'),
37
+ assistantText(5, 'working'),
38
+ assistantText(45, 'back after the limit reset'),
39
+ ]);
40
+ const lines = await gaps(file, 10, 'limit reset');
41
+ return report('witness-gaps', [
42
+ { name: 'a forty-minute gap is a line', ok: lines.some((l) => l.startsWith('GAP 40 min')), detail: lines.join(' | ') },
43
+ { name: 'CONTROL: a five-minute gap under the floor is not', ok: !lines.some((l) => l.startsWith('GAP 5 ')), detail: lines.join(' | ') },
44
+ { name: 'the pattern and the first and last timestamps are reported', ok: lines.some((l) => l.startsWith('MATCH')) && lines.at(-1).startsWith('first '), detail: lines.join(' | ') },
45
+ ]);
46
+ } finally {
47
+ rmSync(dir, { recursive: true, force: true });
48
+ }
49
+ }
50
+
51
+ if (isMain(import.meta.url)) {
52
+ const argv = process.argv.slice(2);
53
+ if (argv.includes('--self-test')) process.exit(await selfTest());
54
+ const [file, min, pattern] = positionals(argv);
55
+ if (!file) { console.log('usage: witness-gaps.mjs <file.jsonl> <minGapMinutes> [pattern]'); process.exit(2); }
56
+ for (const line of await gaps(file, Number(min ?? 10), pattern ?? null)) console.log(line);
57
+ }
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env node
2
+ // Every tool call with its wait, from the call to its result: a long wait on a gated call is a park or an
3
+ // approval, and a long wait on a plane call is the plane. Read-only.
4
+ //
5
+ // node witness-latency.mjs <file.jsonl> <sinceIso> [minSeconds]
6
+ // node witness-latency.mjs --self-test
7
+
8
+ import { rmSync } from 'node:fs';
9
+ import { at, clip, contentOf, entries, isMain, positionals, report, resultText, scratch, toolResult, toolUse, writeTranscript } from './witness-lib.mjs';
10
+
11
+ export async function latency(file, since, min = 0) {
12
+ const uses = new Map();
13
+ const out = [];
14
+ for await (const { entry } of entries(file)) {
15
+ if ((entry.timestamp ?? '') < since) continue;
16
+ for (const c of contentOf(entry)) {
17
+ if (c.type === 'tool_use') uses.set(c.id, { ts: entry.timestamp, name: c.name, input: JSON.stringify(c.input) });
18
+ else if (c.type === 'tool_result' && uses.has(c.tool_use_id)) {
19
+ const use = uses.get(c.tool_use_id);
20
+ uses.delete(c.tool_use_id);
21
+ const lag = (Date.parse(entry.timestamp) - Date.parse(use.ts)) / 1000;
22
+ if (lag < min) continue;
23
+ out.push(`${lag.toFixed(0).padStart(5)}s ${use.ts.slice(11, 19)}Z ${use.name} ${clip(use.input, 150)} => ${c.is_error ? 'ERROR ' : ''}${clip(resultText(c), 140)}`);
24
+ }
25
+ }
26
+ }
27
+ return out;
28
+ }
29
+
30
+ async function selfTest() {
31
+ const dir = scratch('witness-latency');
32
+ try {
33
+ const file = writeTranscript(dir, 'seat.jsonl', [
34
+ toolUse(1, 'a', 'Bash', { command: 'git status' }),
35
+ toolResult(1.05, 'a', 'clean'),
36
+ toolUse(2, 'b', 'Bash', { command: 'a command a human had to approve' }),
37
+ toolResult(4, 'b', 'approved'),
38
+ ]);
39
+ const lines = await latency(file, at(0), 60);
40
+ return report('witness-latency', [
41
+ { name: 'a two-minute wait is a line', ok: lines.length === 1 && lines[0].trim().startsWith('120s') && lines[0].includes('approved'), detail: lines.join(' | ') },
42
+ { name: 'CONTROL: a three-second call under the floor is not', ok: !lines.some((l) => l.includes('git status')), detail: lines.join(' | ') },
43
+ ]);
44
+ } finally {
45
+ rmSync(dir, { recursive: true, force: true });
46
+ }
47
+ }
48
+
49
+ if (isMain(import.meta.url)) {
50
+ const argv = process.argv.slice(2);
51
+ if (argv.includes('--self-test')) process.exit(await selfTest());
52
+ const [file, since, min] = positionals(argv);
53
+ if (!file || !since) { console.log('usage: witness-latency.mjs <file.jsonl> <sinceIso> [minSeconds]'); process.exit(2); }
54
+ for (const line of await latency(file, since, Number(min ?? 0))) console.log(line);
55
+ }
@@ -0,0 +1,124 @@
1
+ // Shared by the witness tools in this folder: the config, a transcript read one line at a time, the
2
+ // plane's tool prefix, and the self-test helpers. Nothing here writes outside the paths a tool is given.
3
+ //
4
+ // The config is `witness.config.json` in the witness folder (or `--config <path>`). The folder is the
5
+ // witness's durable folder: the watcher keeps its state, its event log and its captures there, and the
6
+ // filed ledger is read from it. Every run-specific fact lives in the config, never in a tool.
7
+
8
+ import { createReadStream, existsSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
9
+ import { createInterface } from 'node:readline';
10
+ import { tmpdir } from 'node:os';
11
+ import { dirname, join, resolve } from 'node:path';
12
+ import { pathToFileURL } from 'node:url';
13
+
14
+ export const DEFAULTS = {
15
+ sourceDirs: ['src', 'tests'],
16
+ generatedPaths: [],
17
+ noisePaths: [],
18
+ watchedCommands: [],
19
+ branches: [],
20
+ logScan: [],
21
+ extraLogDirs: [],
22
+ ownerContainers: [],
23
+ reaperContainerPrefix: 'testcontainers-ryuk',
24
+ pollSeconds: 20,
25
+ probes: { processes: true, docker: true },
26
+ };
27
+
28
+ // The value after `name` in argv, or `fallback`.
29
+ export function flag(argv, name, fallback = undefined) {
30
+ const at = argv.indexOf(name);
31
+ return at >= 0 && at + 1 < argv.length ? argv[at + 1] : fallback;
32
+ }
33
+
34
+ // argv without the flags in `valued` (each with its value) and without bare `--` switches.
35
+ export function positionals(argv, valued = []) {
36
+ const out = [];
37
+ for (let i = 0; i < argv.length; i++) {
38
+ if (valued.includes(argv[i])) { i++; continue; }
39
+ if (argv[i].startsWith('--')) continue;
40
+ out.push(argv[i]);
41
+ }
42
+ return out;
43
+ }
44
+
45
+ export function loadConfig(argv, { required = false } = {}) {
46
+ const path = resolve(flag(argv, '--config', 'witness.config.json'));
47
+ if (!existsSync(path)) {
48
+ if (required) throw new Error(`no witness config at ${path} - pass --config <path>`);
49
+ return { ...DEFAULTS, dir: process.cwd(), path: null };
50
+ }
51
+ const raw = JSON.parse(readFileSync(path, 'utf8'));
52
+ return { ...DEFAULTS, ...raw, probes: { ...DEFAULTS.probes, ...(raw.probes ?? {}) }, dir: dirname(path), path };
53
+ }
54
+
55
+ // The tool-name prefix of the plane's MCP server: `--plane <server>`, else the config's `planeServer`.
56
+ export function planePrefix(argv, config) {
57
+ const server = flag(argv, '--plane') ?? config?.planeServer;
58
+ if (!server) throw new Error('name the control plane\'s MCP server: --plane <server>, or planeServer in the config');
59
+ return `mcp__${server}__`;
60
+ }
61
+
62
+ // The plane's read verbs. A read that succeeded is not an act; a read that failed is worth a line.
63
+ export const PLANE_READS = /^(read_blackboard|seat_read|seat_acts|seat_siblings|seat_usage|seat_parks|seat_content|seat_findings|seat_promises|seat_timeline|needs_you)$/;
64
+
65
+ export const clip = (value, n) => {
66
+ const t = String(value ?? '').replace(/\s+/g, ' ').trim();
67
+ return t.length > n ? `${t.slice(0, n)}...` : t;
68
+ };
69
+
70
+ // A transcript entry's content blocks, whether the message holds a string or a list.
71
+ export function contentOf(entry) {
72
+ const message = entry?.message;
73
+ if (!message) return [];
74
+ return Array.isArray(message.content) ? message.content : [{ type: 'text', text: message.content }];
75
+ }
76
+
77
+ export function resultText(block) {
78
+ return Array.isArray(block.content) ? block.content.map((x) => x.text ?? '').join(' ') : String(block.content ?? '');
79
+ }
80
+
81
+ // Every entry of a transcript, one line at a time from `fromByte`, so a transcript of tens of megabytes is
82
+ // never held whole. Lines that do not parse are skipped.
83
+ export async function* entries(file, fromByte = 0) {
84
+ const lines = createInterface({ input: createReadStream(file, { start: fromByte, encoding: 'utf8' }), crlfDelay: Infinity });
85
+ let line = 0;
86
+ for await (const raw of lines) {
87
+ line++;
88
+ if (!raw.trim()) continue;
89
+ let entry;
90
+ try { entry = JSON.parse(raw); } catch { continue; }
91
+ yield { line, entry };
92
+ }
93
+ }
94
+
95
+ export const isMain = (url) => Boolean(process.argv[1]) && pathToFileURL(resolve(process.argv[1])).href === url;
96
+
97
+ // --- self-test helpers ------------------------------------------------------------------------------
98
+
99
+ export const scratch = (prefix) => mkdtempSync(join(tmpdir(), `${prefix}-`));
100
+
101
+ // A timestamp `minutes` after a fixed base, so a fixture names no date of its own.
102
+ export const at = (minutes) => new Date(Date.UTC(2030, 0, 1) + minutes * 60000).toISOString();
103
+
104
+ export const userText = (minutes, text, extra = {}) => ({ type: 'user', timestamp: at(minutes), message: { role: 'user', content: text }, ...extra });
105
+ export const assistantText = (minutes, text) => ({ type: 'assistant', timestamp: at(minutes), message: { role: 'assistant', content: [{ type: 'text', text }] } });
106
+ export const toolUse = (minutes, id, name, input) => ({ type: 'assistant', timestamp: at(minutes), message: { role: 'assistant', content: [{ type: 'tool_use', id, name, input }] } });
107
+ export const toolResult = (minutes, id, text, isError = false) => ({ type: 'user', timestamp: at(minutes), message: { role: 'user', content: [{ type: 'tool_result', tool_use_id: id, content: [{ type: 'text', text }], is_error: isError }] } });
108
+
109
+ export function writeTranscript(dir, name, events) {
110
+ const path = join(dir, name);
111
+ writeFileSync(path, events.map((e) => JSON.stringify(e)).join('\n') + '\n');
112
+ return path;
113
+ }
114
+
115
+ // Prints one line per case and the verdict; returns the exit code. A case is { name, ok, detail }.
116
+ export function report(tool, cases) {
117
+ let failed = 0;
118
+ for (const c of cases) {
119
+ if (!c.ok) failed++;
120
+ console.log(` [${c.ok ? 'PASS' : 'FAIL'}] ${c.name}${c.ok ? '' : ` - ${c.detail ?? ''}`}`);
121
+ }
122
+ console.log(failed === 0 ? `SELF-TEST PASSED: ${tool}` : `SELF-TEST FAILED: ${tool}: ${failed} case(s)`);
123
+ return failed === 0 ? 0 : 1;
124
+ }
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env node
2
+ // Every plane call in a time window, reads included, with its full result: what a session asked the plane
3
+ // and what the plane answered, for the minutes a claim is about. Read-only.
4
+ //
5
+ // node witness-plane-window.mjs <file.jsonl> <fromIso> <toIso> [maxChars] [--plane <server>] [--config <path>]
6
+ // node witness-plane-window.mjs --self-test
7
+
8
+ import { rmSync } from 'node:fs';
9
+ import { contentOf, entries, isMain, loadConfig, planePrefix, positionals, report, resultText, scratch, toolResult, toolUse, writeTranscript, at } from './witness-lib.mjs';
10
+
11
+ export async function window(file, prefix, from, to, max = 700) {
12
+ const uses = new Map();
13
+ const out = [];
14
+ for await (const { entry } of entries(file)) {
15
+ for (const c of contentOf(entry)) {
16
+ if (c.type === 'tool_use' && typeof c.name === 'string' && c.name.startsWith(prefix)) {
17
+ uses.set(c.id, { ts: entry.timestamp ?? '', verb: c.name.slice(prefix.length), input: c.input ?? {} });
18
+ } else if (c.type === 'tool_result' && uses.has(c.tool_use_id)) {
19
+ const use = uses.get(c.tool_use_id);
20
+ uses.delete(c.tool_use_id);
21
+ if (use.ts < from || use.ts > to) continue;
22
+ const body = resultText(c).replace(/\s+/g, ' ');
23
+ out.push(`${use.ts} -> ${entry.timestamp} ${use.verb} ${JSON.stringify(use.input).slice(0, 80)} => ${c.is_error ? 'ERROR ' : ''}${body.slice(0, max)}`);
24
+ }
25
+ }
26
+ }
27
+ return out;
28
+ }
29
+
30
+ async function selfTest() {
31
+ const dir = scratch('witness-plane-window');
32
+ try {
33
+ const file = writeTranscript(dir, 'coordinator.jsonl', [
34
+ toolUse(1, 'a', 'mcp__plane__seat_read', { seatId: 3 }),
35
+ toolResult(1, 'a', 'seat 3: building'),
36
+ toolUse(10, 'b', 'mcp__plane__callout', { seatId: 3, ruling: 'go' }),
37
+ toolResult(11, 'b', 'RULING RECORDED, NOT DELIVERED'),
38
+ toolUse(30, 'c', 'mcp__plane__seat_read', { seatId: 3 }),
39
+ toolResult(30, 'c', 'seat 3: waiting'),
40
+ ]);
41
+ const lines = await window(file, 'mcp__plane__', at(5), at(20));
42
+ return report('witness-plane-window', [
43
+ { name: 'a call inside the window is a line, with its result', ok: lines.length === 1 && lines[0].includes('callout') && lines[0].includes('RECORDED, NOT DELIVERED'), detail: lines.join(' | ') },
44
+ { name: 'CONTROL: calls before and after the window are not', ok: !lines.some((l) => l.includes('seat_read')), detail: lines.join(' | ') },
45
+ ]);
46
+ } finally {
47
+ rmSync(dir, { recursive: true, force: true });
48
+ }
49
+ }
50
+
51
+ if (isMain(import.meta.url)) {
52
+ const argv = process.argv.slice(2);
53
+ if (argv.includes('--self-test')) process.exit(await selfTest());
54
+ const [file, from, to, max] = positionals(argv, ['--plane', '--config']);
55
+ if (!file || !from || !to) { console.log('usage: witness-plane-window.mjs <file.jsonl> <fromIso> <toIso> [maxChars] [--plane <server>]'); process.exit(2); }
56
+ for (const line of await window(file, planePrefix(argv, loadConfig(argv)), from, to, Number(max ?? 700))) console.log(`${line}\n`);
57
+ }
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env node
2
+ // The processes doing a run's work, by the worktree they run in, and the runner's own processes. A quiet
3
+ // compile is a busy process, and no runner where there was one is a restart, which ends every live
4
+ // session. Windows reads the process table through PowerShell's CIM query, anything else through `ps`.
5
+ // Read-only.
6
+ //
7
+ // node witness-procs.mjs [--key <worktree-folder-name>] [--pattern <regex>] [--config <path>]
8
+ // -> one line per process: `RUNNER|<pid>` for the runner, `<pid>|<name>` for a match
9
+ // node witness-procs.mjs --self-test
10
+
11
+ import { execFileSync } from 'node:child_process';
12
+ import { basename } from 'node:path';
13
+ import { flag, isMain, loadConfig, report } from './witness-lib.mjs';
14
+
15
+ // Every process with its id, its name, its command line and its executable path.
16
+ export function listProcesses() {
17
+ if (process.platform === 'win32') {
18
+ const json = execFileSync('powershell', ['-NoProfile', '-NonInteractive', '-Command',
19
+ 'Get-CimInstance Win32_Process | Select-Object ProcessId,Name,CommandLine,ExecutablePath | ConvertTo-Json -Compress'],
20
+ { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024, stdio: ['ignore', 'pipe', 'ignore'] });
21
+ const rows = JSON.parse(json);
22
+ return (Array.isArray(rows) ? rows : [rows]).map((r) => ({ pid: r.ProcessId, name: String(r.Name ?? ''), cmd: String(r.CommandLine ?? ''), exe: String(r.ExecutablePath ?? '') }));
23
+ }
24
+ const out = execFileSync('ps', ['-eo', 'pid=,comm=,args='], { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
25
+ return out.split('\n').filter((l) => l.trim()).map((l) => {
26
+ const m = l.trim().match(/^(\d+)\s+(\S+)\s+(.*)$/);
27
+ return m ? { pid: Number(m[1]), name: m[2], cmd: m[3], exe: '' } : null;
28
+ }).filter(Boolean);
29
+ }
30
+
31
+ // The runner is a node process whose command line names periscope; a match is a process of the
32
+ // configured pattern running in the worktree named by `key`. This tool never reports itself.
33
+ export function classify(processes, { key = '', pattern = null } = {}) {
34
+ const re = pattern ? new RegExp(pattern, 'i') : null;
35
+ const out = [];
36
+ for (const p of processes) {
37
+ const text = `${p.cmd} ${p.exe}`;
38
+ if (/witness-procs/.test(text)) continue;
39
+ if (/^node(\.exe)?$/i.test(p.name) && /periscope/i.test(p.cmd)) { out.push(`RUNNER|${p.pid}`); continue; }
40
+ if (key && re && text.includes(key) && re.test(text)) out.push(`${p.pid}|${p.name}`);
41
+ }
42
+ return out.sort();
43
+ }
44
+
45
+ function selfTest() {
46
+ const synthetic = [
47
+ { pid: 10, name: 'node.exe', cmd: 'node C:/npm/node_modules/@scope/periscope/dist/bin/periscope.js serve', exe: '' },
48
+ { pid: 11, name: 'dotnet', cmd: 'dotnet test --project tests/App.Tests', exe: 'C:/work/pipeline-7/x' },
49
+ { pid: 12, name: 'dotnet', cmd: 'dotnet test --project tests/App.Tests', exe: 'C:/work/pipeline-9/x' },
50
+ { pid: 13, name: 'node.exe', cmd: 'node vitest run C:/work/pipeline-7', exe: '' },
51
+ { pid: 14, name: 'node.exe', cmd: 'node witness-procs.mjs --key pipeline-7', exe: '' },
52
+ ];
53
+ const lines = classify(synthetic, { key: 'pipeline-7', pattern: 'dotnet.*test|vitest' });
54
+ let live = [];
55
+ let liveError = '';
56
+ try { live = listProcesses(); } catch (e) { liveError = String(e.message ?? e).slice(0, 120); }
57
+ return report('witness-procs', [
58
+ { name: 'the runner and the work in this worktree are reported', ok: JSON.stringify(lines) === JSON.stringify(['11|dotnet', '13|node.exe', 'RUNNER|10']), detail: JSON.stringify(lines) },
59
+ { name: 'CONTROL: the same work in another worktree, and this tool itself, are not', ok: !lines.some((l) => l.startsWith('12|') || l.startsWith('14|')), detail: JSON.stringify(lines) },
60
+ { name: 'the process table is read on this machine, this process in it', ok: live.some((p) => p.pid === process.pid), detail: liveError || `${live.length} processes, ${process.pid} absent` },
61
+ ]);
62
+ }
63
+
64
+ if (isMain(import.meta.url)) {
65
+ const argv = process.argv.slice(2);
66
+ if (argv.includes('--self-test')) process.exit(selfTest());
67
+ const config = loadConfig(argv);
68
+ const key = flag(argv, '--key') ?? (config.worktree ? basename(config.worktree) : '');
69
+ const pattern = flag(argv, '--pattern') ?? config.testProcessPattern ?? null;
70
+ for (const line of classify(listProcesses(), { key, pattern })) console.log(line);
71
+ }
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env node
2
+ // Each `seat_spawn` call's task exactly as the coordinator sent it, one file per spawn, and the plane's
3
+ // answer: what a seat was told, in the words that reached it. Writes only into <outDir>.
4
+ //
5
+ // node witness-spawn-task.mjs <file.jsonl> <outDir> [--plane <server>] [--config <path>]
6
+ // node witness-spawn-task.mjs --self-test
7
+
8
+ import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
9
+ import { join } from 'node:path';
10
+ import { contentOf, entries, isMain, loadConfig, planePrefix, positionals, report, resultText, scratch, toolResult, toolUse, writeTranscript } from './witness-lib.mjs';
11
+
12
+ export async function spawnTasks(file, prefix, outDir) {
13
+ mkdirSync(outDir, { recursive: true });
14
+ const pending = new Set();
15
+ const out = [];
16
+ for await (const { entry } of entries(file)) {
17
+ for (const c of contentOf(entry)) {
18
+ if (c.type === 'tool_use' && c.name === `${prefix}seat_spawn`) {
19
+ pending.add(c.id);
20
+ const input = c.input ?? {};
21
+ const path = join(outDir, `spawn-${input.seatId}-task-as-sent.txt`);
22
+ writeFileSync(path, `${entry.timestamp}\n${JSON.stringify({ ...input, task: undefined })}\n\n${input.task ?? ''}`);
23
+ out.push(`seat ${input.seatId} task ${String(input.task ?? '').length} chars -> ${path}`);
24
+ } else if (c.type === 'tool_result' && pending.has(c.tool_use_id)) {
25
+ pending.delete(c.tool_use_id);
26
+ out.push(` result: ${resultText(c)}`);
27
+ }
28
+ }
29
+ }
30
+ return out;
31
+ }
32
+
33
+ async function selfTest() {
34
+ const dir = scratch('witness-spawn-task');
35
+ try {
36
+ const task = 'You are a BUILD SEAT.\nYour spec: 01-a-slice.md\n\tindented line, kept as sent';
37
+ const file = writeTranscript(dir, 'coordinator.jsonl', [
38
+ toolUse(1, 'a', 'mcp__plane__seat_spawn', { seatId: 12, task }),
39
+ toolResult(2, 'a', 'spawned; act 4321'),
40
+ toolUse(3, 'b', 'mcp__plane__seat_read', { seatId: 12 }),
41
+ toolResult(3, 'b', 'seat 12: planning'),
42
+ ]);
43
+ const lines = await spawnTasks(file, 'mcp__plane__', join(dir, 'out'));
44
+ const saved = readFileSync(join(dir, 'out', 'spawn-12-task-as-sent.txt'), 'utf8');
45
+ return report('witness-spawn-task', [
46
+ { name: 'the task is saved exactly as sent', ok: saved.endsWith(`\n\n${task}`), detail: JSON.stringify(saved.slice(-80)) },
47
+ { name: 'the plane\'s answer is reported', ok: lines.some((l) => l.includes('result: spawned; act 4321')), detail: lines.join(' | ') },
48
+ { name: 'CONTROL: another plane call is not a spawn', ok: lines.length === 2, detail: `${lines.length} lines` },
49
+ ]);
50
+ } finally {
51
+ rmSync(dir, { recursive: true, force: true });
52
+ }
53
+ }
54
+
55
+ if (isMain(import.meta.url)) {
56
+ const argv = process.argv.slice(2);
57
+ if (argv.includes('--self-test')) process.exit(await selfTest());
58
+ const [file, outDir] = positionals(argv, ['--plane', '--config']);
59
+ if (!file || !outDir) { console.log('usage: witness-spawn-task.mjs <file.jsonl> <outDir> [--plane <server>]'); process.exit(2); }
60
+ for (const line of await spawnTasks(file, planePrefix(argv, loadConfig(argv)), outDir)) console.log(line);
61
+ }