deadbyte-mcp 0.15.22 → 0.15.23

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 (32) hide show
  1. package/MANIFEST.SHA256 +31 -15
  2. package/PROVENANCE.json +10 -10
  3. package/README.txt +18 -7
  4. package/RELEASE.json +15 -9
  5. package/SBOM.cdx.json +3 -3
  6. package/TEST-MATRIX.json +14 -4
  7. package/package.json +3 -2
  8. package/scripts/activate-skill-creator-1.1.1.mjs +46 -0
  9. package/scripts/build-advanced-pcb-skill.mjs +208 -0
  10. package/scripts/diagnose-skill-tree.mjs +8 -0
  11. package/scripts/npm-release-r48.mjs +252 -0
  12. package/scripts/pcb-skill-src-SKILL.md.txt +67 -0
  13. package/scripts/pcb-skill-src-bringup-evidence.md.txt +22 -0
  14. package/scripts/pcb-skill-src-constraint-model.md.txt +17 -0
  15. package/scripts/pcb-skill-src-emc-mechanical-dfm.md.txt +17 -0
  16. package/scripts/pcb-skill-src-high-speed-layout.md.txt +27 -0
  17. package/scripts/pcb-skill-src-kicad-native.md.txt +30 -0
  18. package/scripts/pcb-skill-src-kicad_pcb_probe.py.txt +159 -0
  19. package/scripts/pcb-skill-src-pcb_constraint_gate.py.txt +76 -0
  20. package/scripts/pcb-skill-src-power-integrity.md.txt +15 -0
  21. package/scripts/prepare-skill-creator-1.1.1.mjs +113 -0
  22. package/scripts/test-skill-creator-semver-matrix.mjs +30 -0
  23. package/scripts/test-skill-creator-semver.mjs +22 -0
  24. package/src/deadbyte-cli.mjs +70 -15
  25. package/src/remote-live-log.mjs +189 -35
  26. package/src/tool-audit.mjs +1 -1
  27. package/src/version.mjs +1 -1
  28. package/test/full-max-catalog-r48.test.mjs +1 -1
  29. package/test/r48-release-contract.test.mjs +20 -14
  30. package/test/remote-live-log-r37.test.mjs +106 -7
  31. package/test/remote-session-cli.test.mjs +40 -0
  32. package/test/tool-audit.test.mjs +17 -0
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env python3
2
+ import json, sys
3
+
4
+ def load(path):
5
+ with open(path, "r", encoding="utf-8") as f:
6
+ return json.load(f)
7
+
8
+ def main():
9
+ if len(sys.argv) != 3:
10
+ raise SystemExit("usage: pcb_constraint_gate.py board-probe.json board-contract.json")
11
+ probe = load(sys.argv[1])
12
+ contract = load(sys.argv[2])
13
+ if probe.get("schema") != "deadbyte.kicad-pcb-probe.v1":
14
+ raise SystemExit("unsupported probe schema")
15
+
16
+ checks = []
17
+ def add(rule, ok, observed, required):
18
+ checks.append({"rule": rule, "ok": bool(ok), "observed": observed, "required": required})
19
+
20
+ if "required_layers" in contract:
21
+ observed = set(x.get("name") for x in probe.get("layers", []))
22
+ req = list(contract["required_layers"])
23
+ add("required_layers", all(x in observed for x in req), sorted(observed), req)
24
+
25
+ if "min_copper_layers" in contract:
26
+ observed = probe.get("copper_layer_count")
27
+ req = int(contract["min_copper_layers"])
28
+ add("min_copper_layers", isinstance(observed, int) and observed >= req, observed, req)
29
+
30
+ for key, probe_key in [
31
+ ("min_track_width_mm", "track_width_mm"),
32
+ ("min_via_diameter_mm", "via_diameter_mm"),
33
+ ("min_via_drill_mm", "via_drill_mm"),
34
+ ]:
35
+ if key in contract:
36
+ observed = (probe.get(probe_key) or {}).get("min")
37
+ req = float(contract[key])
38
+ add(key, observed is not None and float(observed) >= req, observed, req)
39
+
40
+ if "required_nets" in contract:
41
+ observed = set(probe.get("nets", []))
42
+ req = list(contract["required_nets"])
43
+ add("required_nets", all(x in observed for x in req), sorted(observed), req)
44
+
45
+ for key, count_key in [
46
+ ("min_footprints", "footprints"),
47
+ ("min_segments", "segments"),
48
+ ("min_vias", "vias"),
49
+ ("min_zones", "zones"),
50
+ ]:
51
+ if key in contract:
52
+ observed = (probe.get("counts") or {}).get(count_key)
53
+ req = int(contract[key])
54
+ add(key, isinstance(observed, int) and observed >= req, observed, req)
55
+
56
+ allowed = {
57
+ "required_layers", "min_copper_layers", "min_track_width_mm",
58
+ "min_via_diameter_mm", "min_via_drill_mm", "required_nets",
59
+ "min_footprints", "min_segments", "min_vias", "min_zones"
60
+ }
61
+ unknown = sorted(set(contract) - allowed)
62
+ if unknown:
63
+ checks.append({"rule": "unknown_contract_keys", "ok": False, "observed": unknown, "required": "no unsupported keys"})
64
+
65
+ passed = all(x["ok"] for x in checks)
66
+ out = {
67
+ "schema": "deadbyte.pcb-constraint-gate.v1",
68
+ "status": "PASS" if passed else "FAIL",
69
+ "probe_sha256": probe.get("sha256"),
70
+ "checks": checks
71
+ }
72
+ print(json.dumps(out, indent=2, sort_keys=True))
73
+ raise SystemExit(0 if passed else 2)
74
+
75
+ if __name__ == "__main__":
76
+ main()
@@ -0,0 +1,15 @@
1
+ # Power Integrity and Thermal
2
+
3
+ Power integrity begins with current loops, not capacitor count.
4
+
5
+ For each regulator trace the input current path, keep the high di/dt input loop compact, bound switch-node copper, place bootstrap/gate-drive parts by topology, protect the feedback sense path, close the output-current loop, and provide a credible thermal path.
6
+
7
+ For decoupling separate bulk-energy storage from local high-frequency charge. Account for capacitor mounting inductance, vias, planes, and package inductance. Printed capacitance alone does not prove useful impedance at frequency.
8
+
9
+ For planes and pours verify neck-downs at pads, vias, connectors, and thermal spokes; identify slots caused by keepouts and antipads; preserve return continuity; and check both DC drop and transient current paths.
10
+
11
+ For sensitive analog control where digital and switching currents return. Do not split ground reflexively. Placement, loop control, filtering, and current-path ownership matter more than labels.
12
+
13
+ Thermal claims require dissipation, package thermal path, copper geometry, ambient, airflow, and enclosure assumptions. Missing inputs mean thermal margin UNKNOWN.
14
+
15
+ Bench evidence should include rail voltage, ripple/noise with correct probing, startup sequencing, relevant load transient behavior, and regulator temperature under representative load.
@@ -0,0 +1,113 @@
1
+ import { readFile, writeFile, mkdir, rm, cp } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import os from "node:os";
4
+ import { promisify } from "node:util";
5
+ import { execFile } from "node:child_process";
6
+ import { createHash, createPrivateKey, createPublicKey, sign } from "node:crypto";
7
+ import { createSkillPlane } from "../src/skill-plane-r44.mjs";
8
+
9
+ const ex = promisify(execFile);
10
+ const home = os.homedir();
11
+ const stateDir = path.join(home, ".deadbyte-mcp");
12
+ const skillsRoot = path.join(stateDir, "skills");
13
+ const active = path.join(skillsRoot, "skill-creator");
14
+ const expectedOldManifest = "f34b3953db176c8ae9a5e08e7702b39f12b60b013edcd2f4d63a6268af9ba432";
15
+ const oldEnv = JSON.parse(await readFile(path.join(active, "skill.json"), "utf8"));
16
+ if (oldEnv.manifest_sha256 !== expectedOldManifest) {
17
+ throw new Error("unexpected active skill-creator manifest: " + oldEnv.manifest_sha256);
18
+ }
19
+ if (oldEnv.body.version !== "1.1.0") throw new Error("expected active skill-creator 1.1.0");
20
+
21
+ const history = path.join(stateDir, "skill-history", "skill-creator", "1.1.0-" + expectedOldManifest);
22
+ await mkdir(path.dirname(history), {recursive:true});
23
+ try {
24
+ const prior = JSON.parse(await readFile(path.join(history, "skill.json"), "utf8"));
25
+ if (prior.manifest_sha256 !== expectedOldManifest) throw new Error("history path collision");
26
+ } catch (e) {
27
+ if (e.code !== "ENOENT") throw e;
28
+ await cp(active, history, {recursive:true, errorOnExist:true});
29
+ }
30
+ const archived = JSON.parse(await readFile(path.join(history, "skill.json"), "utf8"));
31
+ if (archived.manifest_sha256 !== expectedOldManifest) throw new Error("archive manifest mismatch");
32
+
33
+ const stageParent = path.join(stateDir, "skill-staging", "skill-creator-1.1.1");
34
+ const stage = path.join(stageParent, "skill-creator");
35
+ await rm(stageParent, {recursive:true, force:true});
36
+ await mkdir(stageParent, {recursive:true});
37
+ await cp(active, stage, {recursive:true});
38
+ await rm(path.join(stage, "references", "skills_cache.json"), {force:true});
39
+
40
+ const createPath = path.join(stage, "scripts", "create-skill.mjs");
41
+ const oldCreate = await readFile(createPath, "utf8");
42
+ const oldRegex = "const VERSION=/^[0-9]+\\\\.[0-9]+\\\\.[0-9]+(?:-[A-Za-z0-9.-]+)?$/;";
43
+ const newRegex = "const VERSION=/^[0-9]+\\.[0-9]+\\.[0-9]+(?:-[A-Za-z0-9.-]+)?$/;";
44
+ if (!oldCreate.includes(oldRegex)) throw new Error("expected defective VERSION regex not found");
45
+ if (oldCreate.split(oldRegex).length !== 2) throw new Error("VERSION regex occurrence count != 1");
46
+ await writeFile(createPath, oldCreate.replace(oldRegex, newRegex), "utf8");
47
+
48
+ const skillPath = path.join(stage, "SKILL.md");
49
+ const oldSkill = await readFile(skillPath, "utf8");
50
+ const versionLine = "version: 1.1.0";
51
+ if (!oldSkill.includes(versionLine) || oldSkill.split(versionLine).length !== 2) {
52
+ throw new Error("SKILL.md version occurrence count != 1");
53
+ }
54
+ await writeFile(skillPath, oldSkill.replace(versionLine, "version: 1.1.1"), "utf8");
55
+
56
+ const sha = b => createHash("sha256").update(b).digest("hex");
57
+ const canonical = v => Array.isArray(v) ? v.map(canonical) : v && typeof v === "object"
58
+ ? Object.fromEntries(Object.keys(v).sort((a,b)=>a.localeCompare(b,"en")).map(k=>[k,canonical(v[k])]))
59
+ : v;
60
+
61
+ const body = structuredClone(oldEnv.body);
62
+ body.version = "1.1.1";
63
+ body.skill_md_sha256 = sha(await readFile(skillPath));
64
+ body.provenance = {
65
+ ...body.provenance,
66
+ source_revision: "r47.1-semver-regex-repair-2026-09-27"
67
+ };
68
+ for (const resource of body.resources) {
69
+ resource.sha256 = sha(await readFile(path.join(stage, ...resource.path.split("/"))));
70
+ }
71
+ const bodyBytes = Buffer.from(JSON.stringify(canonical(body)), "utf8");
72
+ const body_sha256 = sha(bodyBytes);
73
+ const priv = createPrivateKey(await readFile(path.join(stateDir, "trust", "ed25519-private.pem")));
74
+ const pub = createPublicKey(priv);
75
+ const keyId = sha(pub.export({type:"spki", format:"der"}));
76
+ const attestation = {
77
+ algorithm: "ed25519",
78
+ domain: "DEADBYTE-SKILL-MANIFEST-V1",
79
+ key_id: keyId,
80
+ signature_base64: sign(null, Buffer.concat([Buffer.from("DEADBYTE-SKILL-MANIFEST-V1\0", "utf8"), bodyBytes]), priv).toString("base64")
81
+ };
82
+ const unsigned = {body, body_sha256, attestation};
83
+ const envelope = {
84
+ ...unsigned,
85
+ manifest_sha256: sha(Buffer.from(JSON.stringify(canonical(unsigned)), "utf8"))
86
+ };
87
+ await writeFile(path.join(stage, "skill.json"), JSON.stringify(envelope, null, 2), "utf8");
88
+
89
+ const validator = await ex(process.execPath, [path.join(stage, "scripts", "validate-skill.mjs"), stage], {
90
+ timeout: 30000,
91
+ maxBuffer: 1024 * 1024
92
+ });
93
+
94
+ const plane = await createSkillPlane({
95
+ root: stageParent,
96
+ verifyKeyPath: path.join(stateDir, "trust", "ed25519-public.pem")
97
+ });
98
+ const loaded = await plane.load({name:"skill-creator"});
99
+ if (!loaded.verified || loaded.version !== "1.1.1") throw new Error("staging Skill Plane verification failed");
100
+
101
+ console.log(JSON.stringify({
102
+ status:"STAGED",
103
+ active_manifest:oldEnv.manifest_sha256,
104
+ archived_path:history,
105
+ archived_manifest:archived.manifest_sha256,
106
+ stage_parent:stageParent,
107
+ stage_skill:stage,
108
+ candidate_manifest:envelope.manifest_sha256,
109
+ candidate_body_sha256:body_sha256,
110
+ verify_key_id:keyId,
111
+ validator:validator.stdout.trim(),
112
+ skill_plane_verified:loaded.verified
113
+ }, null, 2));
@@ -0,0 +1,30 @@
1
+ import { mkdtemp, rm } from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { promisify } from "node:util";
5
+ import { execFile } from "node:child_process";
6
+ const ex=promisify(execFile);
7
+ const creator=process.argv[2];
8
+ if(!creator) throw new Error("creator path required");
9
+ const valid=["1.0.0","0.0.1","1.1.1","12.34.56","1.0.0-rc.1"];
10
+ const invalid=["1.0","v1.0.0","1.a.0","",String.fromCharCode(49,92,97,48,92,98,48)];
11
+ const results=[];
12
+ for(const version of valid){
13
+ const tmp=await mkdtemp(path.join(os.tmpdir(),"deadbyte-semver-valid-"));
14
+ try{
15
+ await ex(process.execPath,[creator,tmp,"fixture",version],{timeout:30000,maxBuffer:1048576});
16
+ results.push({version,expected:"PASS",observed:"PASS"});
17
+ }finally{await rm(tmp,{recursive:true,force:true});}
18
+ }
19
+ for(const version of invalid){
20
+ const tmp=await mkdtemp(path.join(os.tmpdir(),"deadbyte-semver-invalid-"));
21
+ let rejected=false;
22
+ try{
23
+ await ex(process.execPath,[creator,tmp,"fixture",version],{timeout:30000,maxBuffer:1048576});
24
+ }catch(e){
25
+ if(e.code===1 && String(e.stderr).includes("invalid version")) rejected=true;
26
+ }finally{await rm(tmp,{recursive:true,force:true});}
27
+ if(!rejected) throw new Error("invalid version accepted: "+JSON.stringify(version));
28
+ results.push({version,expected:"FAIL",observed:"FAIL"});
29
+ }
30
+ console.log(JSON.stringify({status:"PASS",cases:results},null,2));
@@ -0,0 +1,22 @@
1
+ import { mkdtemp, rm } from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { promisify } from "node:util";
5
+ import { execFile } from "node:child_process";
6
+
7
+ const ex = promisify(execFile);
8
+ const creator = process.argv[2];
9
+ if (!creator) throw new Error("usage: node test-skill-creator-semver.mjs <create-skill.mjs>");
10
+ const tmp = await mkdtemp(path.join(os.tmpdir(), "deadbyte-skill-creator-semver-"));
11
+ try {
12
+ const r = await ex(process.execPath, [creator, tmp, "semver-regression-fixture", "1.0.0"], {
13
+ timeout: 30000,
14
+ maxBuffer: 1024 * 1024
15
+ });
16
+ if (!r.stdout.includes("semver-regression-fixture")) {
17
+ throw new Error("create-skill returned success without fixture path");
18
+ }
19
+ console.log(JSON.stringify({status:"PASS", version:"1.0.0", stdout:r.stdout.trim()}));
20
+ } finally {
21
+ await rm(tmp, {recursive:true, force:true});
22
+ }
@@ -13,7 +13,7 @@ import { openRemoteResultJournal, parseRemoteResultCursor } from './remote-resul
13
13
  import { DEADBYTE_VERSION } from './version.mjs';
14
14
  import { parseActivePointer } from './release-slot.mjs';
15
15
  import { printDoctor } from './doctor-r45.mjs';
16
- import { createLiveTimelineTracker, formatCompactAuditEvent, formatRemoteLifecycle, formatRemoteToolCall, formatRuntimeDiagnosticLine } from './remote-live-log.mjs';
16
+ import { createLiveTimelineFilter, createLiveTimelineTracker, createLiveTimelineWriter, formatCompactAuditEvent, formatRemoteLifecycle, formatRemoteToolCall, formatRuntimeDiagnosticLine } from './remote-live-log.mjs';
17
17
  import { prepareRemoteConsole, remoteDisplayText } from './remote-console.mjs';
18
18
 
19
19
  const REMOTE_COMMANDS=new Set([
@@ -46,6 +46,8 @@ function printHelp(){
46
46
  ` npx -y ${exact} mcp # stdio MCP server for local clients`,
47
47
  ` npx -y ${exact} remote # unified foreground device session + live timeline`,
48
48
  ` npx -y ${exact} remote --resume <cursor> # replay missed results, then reconnect`,
49
+ ` npx -y ${exact} remote --trace # show trace/span identity without payload detail`,
50
+ ` npx -y ${exact} remote --filter <expr> # presentation filter: tool/category/status/trace`,
49
51
  ` npx -y ${exact} remote status`,
50
52
  ` npx -y ${exact} remote restart`,
51
53
  ` npx -y ${exact} supervisor status`,
@@ -71,6 +73,21 @@ async function readActiveRuntimeBinding(root){
71
73
  try{return parseActivePointer(await readFile(file,'utf8'));}
72
74
  catch(error){if(error?.code==='ENOENT') return null;throw new Error(`active runtime pointer invalid: ${boundedSingleLine(error?.message||error,512)}`);}
73
75
  }
76
+ async function readLiveRootPaths(root){
77
+ const active=await readActiveRuntimeBinding(root);
78
+ if(!active) return Object.freeze({});
79
+ const file=path.join(active.generation_root,'coding-policy.json');
80
+ let raw;
81
+ try{raw=JSON.parse(await readFile(file,'utf8'));}
82
+ catch(error){if(error?.code==='ENOENT') return Object.freeze({});throw new Error(`live root policy invalid: ${boundedSingleLine(error?.message||error,512)}`);}
83
+ if(raw?.schema!=='deadbyte.coding-policy.v1'||!raw.roots||typeof raw.roots!=='object'||Array.isArray(raw.roots)) throw new Error('live root policy schema invalid');
84
+ const roots={};
85
+ for(const [id,spec] of Object.entries(raw.roots)){
86
+ if(!/^[A-Za-z0-9._-]{1,64}$/.test(id)||!spec||typeof spec!=='object'||typeof spec.path!=='string'||!path.isAbsolute(spec.path)) continue;
87
+ roots[id]=path.resolve(spec.path);
88
+ }
89
+ return Object.freeze(roots);
90
+ }
74
91
  async function reportVersionCoherence(root,{displayMode='ascii'}={}){
75
92
  const active=await readActiveRuntimeBinding(root);
76
93
  if(!active||active.version===DEADBYTE_VERSION) return active;
@@ -227,7 +244,22 @@ async function snapshotRemoteLogOffsets(root){
227
244
  }
228
245
  return offsets;
229
246
  }
230
- async function tailRemoteLogs(root,signal,{journal=null,offsets=null,activityTracker=null,verbose=false,displayMode='unicode'}={}){
247
+ function timelineFilterEvent(timeline){
248
+ if(!timeline) return null;
249
+ if(timeline.event_type==='tool.call') return {
250
+ event_type:'tool.request',request_id:timeline.request_id,tool:timeline.tool,args:timeline.arguments??{}
251
+ };
252
+ return {
253
+ event_type:'tool.result',request_id:timeline.request_id,tool:timeline.tool,
254
+ outcome:{
255
+ is_error:timeline.status==='error',
256
+ ...(Number.isFinite(Number(timeline.duration_ms))?{duration_ms:Number(timeline.duration_ms)}:{}),
257
+ ...(timeline.side_effect_state?{side_effect_state:timeline.side_effect_state}:{}),
258
+ ...(timeline.reason?{reason:timeline.reason}:{})
259
+ }
260
+ };
261
+ }
262
+ async function tailRemoteLogs(root,signal,{journal=null,offsets=null,activityTracker=null,timelineFilter=null,timelineWriter=null,verbose=false,displayMode='unicode'}={}){
231
263
  process.stdout.write(verbose
232
264
  ? '[LOG] Verbose live timeline. Full sanitized request detail enabled; Ctrl+C disconnects and revokes authority.\n'
233
265
  : '[LOG] Human live timeline. Transport IN/OUT hidden; full replay/audit detail remains on disk. Ctrl+C disconnects and revokes authority.\n');
@@ -244,10 +276,14 @@ async function tailRemoteLogs(root,signal,{journal=null,offsets=null,activityTra
244
276
  ? await journal.recordCall(timeline)
245
277
  : await journal.recordTerminal(timeline);
246
278
  if(recorded?.duplicate) continue;
279
+ const accepted=timelineFilter&&timeline?timelineFilter.accept(timelineFilterEvent(timeline)):true;
247
280
  const formatted=formatLogLine(file,line,{activityTracker,sequence:recorded?.event?.seq??null,verbose,displayMode});
248
- if(!formatted) continue;
281
+ if(!formatted||!accepted) continue;
249
282
  const output=Array.isArray(formatted)?formatted:[formatted];
250
- for(const item of output) if(item) process.stdout.write(`${item}\n`);
283
+ for(const item of output) if(item){
284
+ if(timelineWriter) timelineWriter.write(item,{event_type:timeline?.event_type??null,request_id:timeline?.request_id??null});
285
+ else process.stdout.write(`${item}\n`);
286
+ }
251
287
  }
252
288
  }
253
289
  try{await sleep(250,null,{signal});}catch(error){if(error?.name!=='AbortError') throw error;}
@@ -341,7 +377,7 @@ async function releaseRemoteSession(ctx,marker,{displayMode='unicode'}={}){
341
377
  const same=current?.schema==='deadbyte.remote-session.v2'&&current.owner_pid===marker.owner_pid&&current.session_nonce===marker.session_nonce;
342
378
  if(same) await rm(remoteSessionMarker(ctx.root),{force:true});
343
379
  }
344
- async function runRemoteSession({resumeCursor=null,verbose=false}={}){
380
+ async function runRemoteSession({resumeCursor=null,verbose=false,trace=false,filterExpression=''}={}){
345
381
  const display=prepareRemoteConsole();
346
382
  const displayMode=display.mode;
347
383
  const ctx=await remoteContext();
@@ -353,7 +389,10 @@ async function runRemoteSession({resumeCursor=null,verbose=false}={}){
353
389
  const resultJournal=await openRemoteResultJournal({
354
390
  filePath:remoteResultJournalPath(ctx.root,marker.result_session_id),sessionId:marker.result_session_id,maxEntries:2048
355
391
  });
356
- const activityTracker=createLiveTimelineTracker({verbose,displayMode});
392
+ const rootPaths=await readLiveRootPaths(ctx.root);
393
+ const activityTracker=createLiveTimelineTracker({verbose,displayMode,rootPaths,trace});
394
+ const timelineFilter=filterExpression?createLiveTimelineFilter(filterExpression):null;
395
+ const timelineWriter=createLiveTimelineWriter({stream:process.stdout,displayMode,rewrite:!verbose});
357
396
  const aborter=new AbortController();
358
397
  const abort=()=>aborter.abort();
359
398
  const watchStdin=!process.stdin.isTTY;
@@ -366,7 +405,7 @@ async function runRemoteSession({resumeCursor=null,verbose=false}={}){
366
405
  process.stdout.write(`${formatRemoteLifecycle('device_start',{displayMode})}\n`);
367
406
  process.stdout.write(`${formatRemoteLifecycle(resumeCursor!==null?'session_restored':'session_new',{displayMode})}\n`);
368
407
  const logOffsets=await snapshotRemoteLogOffsets(ctx.root);
369
- logTask=tailRemoteLogs(ctx.root,aborter.signal,{journal:resultJournal,offsets:logOffsets,activityTracker,verbose,displayMode});
408
+ logTask=tailRemoteLogs(ctx.root,aborter.signal,{journal:resultJournal,offsets:logOffsets,activityTracker,timelineFilter,timelineWriter,verbose,displayMode});
370
409
  process.stdout.write('[BOOT] Starting local + remote runtime...\n');
371
410
  const preflight=invokeController(ctx,'status',{capture:true,allowFailure:true,echoCaptured:false,timeoutMs:REMOTE_STATUS_TIMEOUT_MS});
372
411
  let startText=preflight.stdout||'';
@@ -415,6 +454,7 @@ async function runRemoteSession({resumeCursor=null,verbose=false}={}){
415
454
  const wasAborted=aborter.signal.aborted;
416
455
  aborter.abort();
417
456
  if(logTask) await logTask.catch(()=>{});
457
+ timelineWriter.flush();
418
458
  for(const line of activityTracker.reconcileInterrupted('session_end')) process.stdout.write(`${line}\n`);
419
459
  process.stdout.write(`${activityTracker.summary()}\n`);
420
460
  process.removeListener('SIGINT',abort);process.removeListener('SIGTERM',abort);
@@ -426,14 +466,29 @@ async function runRemoteSession({resumeCursor=null,verbose=false}={}){
426
466
  }
427
467
  }
428
468
  async function runRemote(args){
429
- if(args.length===0){await runRemoteSession();return;}
430
- const verbose=args.includes('--verbose')||args.includes('-v');
431
- const filtered=args.filter(arg=>arg!=='--verbose'&&arg!=='-v');
432
- if(filtered.length===0){await runRemoteSession({verbose});return;}
433
- if(filtered.length===2&&filtered[0]==='--resume'){await runRemoteSession({resumeCursor:filtered[1],verbose});return;}
434
- if(verbose) throw new Error('remote --verbose/-v is valid only for foreground session or --resume <cursor>');
435
- if(filtered.length>1) throw new Error('remote accepts one controller command or --resume <cursor>');
436
- const command=filtered[0];if(!REMOTE_COMMANDS.has(command)) throw new Error(`unsupported remote command '${command}'`);
469
+ let verbose=false,trace=false,filterExpression='',resumeCursor=null;
470
+ const positional=[];
471
+ for(let index=0;index<args.length;index++){
472
+ const arg=args[index];
473
+ if(arg==='--verbose'||arg==='-v'){verbose=true;continue;}
474
+ if(arg==='--trace'){trace=true;continue;}
475
+ if(arg==='--filter'){
476
+ if(filterExpression) throw new Error('remote --filter may be specified once');
477
+ if(index+1>=args.length) throw new Error('remote --filter requires an expression');
478
+ filterExpression=args[++index];continue;
479
+ }
480
+ if(arg==='--resume'){
481
+ if(resumeCursor!==null) throw new Error('remote --resume may be specified once');
482
+ if(index+1>=args.length) throw new Error('remote --resume requires a cursor');
483
+ resumeCursor=args[++index];continue;
484
+ }
485
+ positional.push(arg);
486
+ }
487
+ const presentation=verbose||trace||Boolean(filterExpression)||resumeCursor!==null;
488
+ if(positional.length===0){await runRemoteSession({resumeCursor,verbose,trace,filterExpression});return;}
489
+ if(presentation) throw new Error('remote presentation flags are valid only for foreground session');
490
+ if(positional.length>1) throw new Error('remote accepts one controller command');
491
+ const command=positional[0];if(!REMOTE_COMMANDS.has(command)) throw new Error(`unsupported remote command '${command}'`);
437
492
  const ctx=await remoteContext();
438
493
  await reportVersionCoherence(ctx.root,{displayMode:'ascii'});
439
494
  const result=invokeController(ctx,command,{allowFailure:true});