@llblab/pi-actors 0.43.0 → 0.44.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 (109) hide show
  1. package/AGENTS.md +16 -10
  2. package/CHANGELOG.md +425 -536
  3. package/README.md +13 -11
  4. package/dist/index.js +1 -1
  5. package/dist/lib/async-runs.d.ts +2 -1
  6. package/dist/lib/async-runs.js +24 -34
  7. package/dist/lib/automatic-review-runtime.d.ts +1 -1
  8. package/dist/lib/automatic-review-runtime.js +5 -5
  9. package/dist/lib/command-templates.d.ts +2 -0
  10. package/dist/lib/command-templates.js +38 -4
  11. package/dist/lib/control-projection.d.ts +20 -0
  12. package/dist/lib/control-projection.js +66 -0
  13. package/dist/lib/control.d.ts +3 -0
  14. package/dist/lib/control.js +27 -14
  15. package/dist/lib/draft-sleep.js +3 -3
  16. package/dist/lib/file-state.d.ts +4 -1
  17. package/dist/lib/file-state.js +118 -44
  18. package/dist/lib/inspector-overlay.d.ts +2 -0
  19. package/dist/lib/inspector-overlay.js +124 -69
  20. package/dist/lib/limits.d.ts +15 -3
  21. package/dist/lib/limits.js +15 -3
  22. package/dist/lib/observability.d.ts +4 -2
  23. package/dist/lib/observability.js +43 -36
  24. package/dist/lib/prompts.d.ts +1 -1
  25. package/dist/lib/prompts.js +1 -1
  26. package/dist/lib/recipe-control.js +6 -2
  27. package/dist/lib/review-control.d.ts +1 -1
  28. package/dist/lib/review-control.js +4 -5
  29. package/dist/lib/run-evidence-policy.d.ts +95 -0
  30. package/dist/lib/run-evidence-policy.js +177 -0
  31. package/dist/lib/run-ui-runtime.js +2 -0
  32. package/dist/lib/runs-control-delivery.d.ts +8 -1
  33. package/dist/lib/runs-control-delivery.js +38 -15
  34. package/dist/lib/runs-controls.d.ts +8 -4
  35. package/dist/lib/runs-controls.js +189 -50
  36. package/dist/lib/runs-retention.js +27 -14
  37. package/dist/lib/runs-trace.d.ts +26 -2
  38. package/dist/lib/runs-trace.js +411 -18
  39. package/dist/lib/runtime-identity.d.ts +7 -0
  40. package/dist/lib/runtime-identity.js +35 -0
  41. package/dist/lib/runtime-triage.d.ts +29 -0
  42. package/dist/lib/runtime-triage.js +60 -0
  43. package/dist/lib/tool-review-scheduler.js +7 -7
  44. package/dist/lib/tools-inspect.js +91 -18
  45. package/dist/lib/tools-message.d.ts +1 -2
  46. package/dist/lib/tools-message.js +6 -6
  47. package/dist/lib/tools-response.d.ts +0 -1
  48. package/dist/lib/tools-response.js +0 -9
  49. package/dist/lib/tools.d.ts +1 -1
  50. package/dist/lib/tools.js +1 -1
  51. package/dist/lib/trace-projection.js +107 -41
  52. package/dist/scripts/conformance.mjs +5 -0
  53. package/dist/scripts/locker.mjs +40 -90
  54. package/dist/scripts/music-player.mjs +48 -142
  55. package/dist/scripts/release-gates.mjs +56 -3
  56. package/dist/scripts/validate-recipe.mjs +5 -4
  57. package/dist/skills/actors/SKILL.md +17 -11
  58. package/dist/skills/swarm/SKILL.md +2 -4
  59. package/docs/README.md +1 -4
  60. package/docs/actor-inspector.md +6 -5
  61. package/docs/async-runs.md +11 -9
  62. package/docs/command-templates.md +6 -116
  63. package/docs/recipe-library.md +4 -6
  64. package/docs/releasing.md +28 -0
  65. package/docs/template-recipes.md +1 -1
  66. package/docs/tool-registry.md +2 -2
  67. package/index.ts +1 -1
  68. package/lib/async-runs.ts +26 -50
  69. package/lib/automatic-review-runtime.ts +7 -7
  70. package/lib/command-templates.ts +44 -4
  71. package/lib/control-projection.ts +105 -0
  72. package/lib/control.ts +33 -18
  73. package/lib/draft-sleep.ts +3 -3
  74. package/lib/file-state.ts +91 -63
  75. package/lib/inspector-overlay.ts +108 -61
  76. package/lib/limits.ts +15 -3
  77. package/lib/observability.ts +55 -57
  78. package/lib/prompts.ts +1 -1
  79. package/lib/recipe-control.ts +9 -2
  80. package/lib/review-control.ts +4 -5
  81. package/lib/run-evidence-policy.ts +242 -0
  82. package/lib/run-ui-runtime.ts +2 -0
  83. package/lib/runs-control-delivery.ts +45 -17
  84. package/lib/runs-controls.ts +180 -102
  85. package/lib/runs-retention.ts +28 -20
  86. package/lib/runs-trace.ts +499 -20
  87. package/lib/runtime-identity.ts +39 -0
  88. package/lib/runtime-triage.ts +106 -0
  89. package/lib/tool-review-scheduler.ts +7 -7
  90. package/lib/tools-inspect.ts +94 -20
  91. package/lib/tools-message.ts +7 -8
  92. package/lib/tools-response.ts +0 -12
  93. package/lib/tools.ts +4 -4
  94. package/lib/trace-projection.ts +156 -71
  95. package/package.json +1 -1
  96. package/scripts/conformance.mjs +5 -0
  97. package/scripts/locker.mjs +40 -90
  98. package/scripts/music-player.mjs +48 -142
  99. package/scripts/release-gates.mjs +56 -3
  100. package/scripts/validate-recipe.mjs +5 -4
  101. package/skills/actors/SKILL.md +17 -11
  102. package/skills/swarm/SKILL.md +2 -4
  103. package/dist/lib/runtime-notifier.d.ts +0 -48
  104. package/dist/lib/runtime-notifier.js +0 -138
  105. package/docs/0.43-baseline.md +0 -44
  106. package/docs/actors-deep-reference.md +0 -108
  107. package/docs/component-recipes.md +0 -45
  108. package/docs/task-first-recipes.md +0 -261
  109. package/lib/runtime-notifier.ts +0 -211
@@ -9,13 +9,11 @@
9
9
  */
10
10
 
11
11
  import { spawnSync } from "node:child_process";
12
- import { createHash, randomUUID } from "node:crypto";
12
+ import { createHash } from "node:crypto";
13
13
  import {
14
- appendFileSync,
15
14
  existsSync,
16
15
  mkdirSync,
17
16
  readFileSync,
18
- rmSync,
19
17
  } from "node:fs";
20
18
  import { createServer } from "node:net";
21
19
  import { dirname, join, resolve } from "node:path";
@@ -33,8 +31,13 @@ async function importRuntimeModule(name) {
33
31
  return await import(pathToFileURL(existsSync(compiled) ? compiled : source).href);
34
32
  }
35
33
 
36
- const { acquireFileMutationLock, writeJsonAtomic, writeTextAtomic } =
37
- await importRuntimeModule("file-state");
34
+ const { acquireFileMutationLock, writeJsonAtomic, writeTextAtomic } = await importRuntimeModule("file-state");
35
+ const { readJsonlFileResilient } = await importRuntimeModule("state-readers");
36
+ const LOCKER_JOURNAL_MAX_RECORDS = 512;
37
+ const LOCKER_JOURNAL_MAX_BYTES = 1024 * 1024;
38
+ const { claimRunControlByIdInStateDir, updateRunControlStatusInStateDir } =
39
+ await importRuntimeModule("runs-controls");
40
+ const { appendRunTraceEvent } = await importRuntimeModule("runs-trace");
38
41
 
39
42
  function parseArgs(argv) {
40
43
  const args = { mode: "serve", stateDir: "", leaseMs: 600000, lines: 20 };
@@ -59,8 +62,6 @@ async function runLocker(argv = process.argv.slice(2)) {
59
62
  const queuePath = join(stateDir, "queue.json");
60
63
  const locksPath = join(stateDir, "locks.json");
61
64
  const journalPath = join(stateDir, "journal.jsonl");
62
- const tracePath = join(stateDir, "trace.jsonl");
63
- const controlsPath = join(stateDir, "controls.jsonl");
64
65
  const controlPath = join(stateDir, "control.fifo");
65
66
  let runInstanceId;
66
67
  mkdirSync(stateDir, { recursive: true });
@@ -97,25 +98,32 @@ async function runLocker(argv = process.argv.slice(2)) {
97
98
  }
98
99
 
99
100
  function journal(event, data = {}) {
100
- appendFileSync(
101
- journalPath,
102
- `${JSON.stringify({ event, ts: new Date().toISOString(), ...data })}\n`,
103
- );
101
+ const release = acquireFileMutationLock(journalPath);
102
+ try {
103
+ const records = [...readJsonlFileResilient(journalPath).records,
104
+ { event, ts: new Date().toISOString(), ...data }]
105
+ .slice(-LOCKER_JOURNAL_MAX_RECORDS);
106
+ let content = encodeJournal(records);
107
+ while (records.length && Buffer.byteLength(content) > LOCKER_JOURNAL_MAX_BYTES) {
108
+ records.shift();
109
+ content = encodeJournal(records);
110
+ }
111
+ writeTextAtomic(journalPath, content);
112
+ } finally { release(); }
113
+ }
114
+
115
+ function encodeJournal(records) {
116
+ return records.length ? `${records.map((record) => JSON.stringify(record)).join("\n")}\n` : "";
104
117
  }
105
118
 
106
119
  function emitTrace(kind, summary, data = {}, level = "info") {
107
- appendFileSync(
108
- tracePath,
109
- `${JSON.stringify({
110
- attention: "followup",
111
- data,
112
- id: randomUUID(),
113
- kind,
114
- level,
115
- summary,
116
- ts: new Date().toISOString(),
117
- })}\n`,
118
- );
120
+ appendRunTraceEvent(stateDir, {
121
+ attention: "followup",
122
+ data,
123
+ kind,
124
+ level,
125
+ summary,
126
+ });
119
127
  }
120
128
 
121
129
  function now() {
@@ -153,69 +161,6 @@ async function runLocker(argv = process.argv.slice(2)) {
153
161
  }
154
162
  }
155
163
 
156
- function acquireControlsLock() {
157
- return acquireFileMutationLock(controlsPath);
158
- }
159
-
160
- function readControls() {
161
- if (!existsSync(controlsPath)) return [];
162
- return readFileSync(controlsPath, "utf8")
163
- .split("\n")
164
- .filter((line) => line.trim())
165
- .map((line) => {
166
- try {
167
- return JSON.parse(line);
168
- } catch {
169
- return undefined;
170
- }
171
- })
172
- .filter(Boolean);
173
- }
174
-
175
- function writeControls(controls) {
176
- writeTextAtomic(
177
- controlsPath,
178
- controls.length
179
- ? `${controls.map((control) => JSON.stringify(control)).join("\n")}\n`
180
- : "",
181
- );
182
- }
183
-
184
- function claimControl(id) {
185
- const release = acquireControlsLock();
186
- try {
187
- const controls = readControls();
188
- const control = controls.find((item) => item.id === id);
189
- if (
190
- !control ||
191
- control.run_instance_id !== runInstanceId ||
192
- (control.status !== "queued" && control.status !== "delivered")
193
- ) {
194
- return undefined;
195
- }
196
- control.claimed_at = new Date().toISOString();
197
- control.status = "claimed";
198
- writeControls(controls);
199
- return { ...control };
200
- } finally {
201
- release();
202
- }
203
- }
204
-
205
- function finalizeControl(id, status, error) {
206
- const release = acquireControlsLock();
207
- try {
208
- const controls = readControls();
209
- const control = controls.find((item) => item.id === id);
210
- if (!control || control.status !== "claimed") return;
211
- control.status = status;
212
- control[`${status}_at`] = new Date().toISOString();
213
- if (error) control.error = error;
214
- writeControls(controls);
215
- } finally {
216
- release();
217
- }
218
- }
219
164
 
220
165
  function tailJournal(count) {
221
166
  if (!existsSync(journalPath)) return [];
@@ -403,7 +348,9 @@ async function runLocker(argv = process.argv.slice(2)) {
403
348
  function handleLine(line) {
404
349
  const control = normalizeControl(line);
405
350
  if (!control) return false;
406
- const claimed = claimControl(control.id);
351
+ const claimed = claimRunControlByIdInStateDir(
352
+ stateDir, runInstanceId, control.id,
353
+ );
407
354
  if (!claimed) {
408
355
  emitTrace(
409
356
  "lock.control_rejected",
@@ -415,11 +362,15 @@ async function runLocker(argv = process.argv.slice(2)) {
415
362
  }
416
363
  try {
417
364
  const stopping = handle(claimed) === true;
418
- finalizeControl(claimed.id, "handled");
365
+ updateRunControlStatusInStateDir(
366
+ stateDir, claimed.id, "handled", {}, ["claimed"],
367
+ );
419
368
  return stopping;
420
369
  } catch (error) {
421
370
  const text = error instanceof Error ? error.message : String(error);
422
- finalizeControl(claimed.id, "failed", text);
371
+ updateRunControlStatusInStateDir(
372
+ stateDir, claimed.id, "failed", { error: text }, ["claimed"],
373
+ );
423
374
  journal("lock.error", { error: text });
424
375
  emitTrace("lock.error", text, { error: text }, "error");
425
376
  return false;
@@ -454,7 +405,6 @@ async function runLocker(argv = process.argv.slice(2)) {
454
405
  }
455
406
 
456
407
  async function serveNamedPipe(endpoint) {
457
- rmSync(endpoint.path, { force: true });
458
408
  let resolveStopped;
459
409
  const stopped = new Promise((resolve) => {
460
410
  resolveStopped = resolve;
@@ -13,7 +13,6 @@
13
13
  */
14
14
 
15
15
  import { spawn, spawnSync } from "node:child_process";
16
- import { randomUUID } from "node:crypto";
17
16
  import {
18
17
  accessSync,
19
18
  constants,
@@ -50,8 +49,9 @@ async function importRuntimeModule(name) {
50
49
  return await import(pathToFileURL(existsSync(compiled) ? compiled : source).href);
51
50
  }
52
51
 
53
- const { acquireFileMutationLock, writeTextAtomic } =
54
- await importRuntimeModule("file-state");
52
+ const { appendRunControlInStateDir, claimRunControlByIdInStateDir,
53
+ updateRunControlStatusInStateDir } = await importRuntimeModule("runs-controls");
54
+ const { appendRunTraceEvent } = await importRuntimeModule("runs-trace");
55
55
 
56
56
  const AUDIO_EXTENSIONS = new Set([
57
57
  ".aac",
@@ -407,8 +407,8 @@ function playerCommand(ctx, player, volume, track) {
407
407
  }
408
408
  }
409
409
 
410
- function writeText(path, value, flag = "w") {
411
- writeFileSync(path, value, { encoding: "utf8", flag });
410
+ function writeText(path, value) {
411
+ writeFileSync(path, value, "utf8");
412
412
  }
413
413
 
414
414
  function readText(path) {
@@ -420,18 +420,12 @@ function readText(path) {
420
420
  }
421
421
 
422
422
  function emitPlayerEvent(ctx, kind, summary, data = {}) {
423
- writeText(
424
- ctx.eventFile,
425
- `${JSON.stringify({
426
- data,
427
- id: randomUUID(),
428
- kind,
429
- level: "info",
430
- summary,
431
- ts: new Date().toISOString(),
432
- })}\n`,
433
- "a",
434
- );
423
+ appendRunTraceEvent(ctx.stateDir, {
424
+ data,
425
+ kind,
426
+ level: "info",
427
+ summary,
428
+ });
435
429
  }
436
430
 
437
431
  function emitTrackEvent(ctx, index, count, track, player) {
@@ -589,7 +583,24 @@ async function startControlServer(ctx, wakeControlLoop) {
589
583
  : join(ctx.stateDir, "control.sock");
590
584
  if (process.platform !== "win32") rmSync(path, { force: true });
591
585
  const server = createServer((socket) => {
592
- socket.on("data", wakeControlLoop);
586
+ let content = "";
587
+ socket.setEncoding("utf8");
588
+ socket.on("data", (chunk) => { content += chunk; });
589
+ socket.on("end", () => {
590
+ for (const line of content.split("\n")) {
591
+ if (!line.trim()) continue;
592
+ try {
593
+ const claimed = claimControl(ctx, JSON.parse(line));
594
+ if (!claimed) continue;
595
+ handleControl(ctx, claimed.command);
596
+ finalizeControl(ctx, claimed.id, "handled");
597
+ } catch (error) {
598
+ const id = (() => { try { return JSON.parse(line).id; } catch { return undefined; } })();
599
+ finalizeControl(ctx, id, "failed", error instanceof Error ? error.message : String(error));
600
+ }
601
+ }
602
+ wakeControlLoop();
603
+ });
593
604
  socket.resume();
594
605
  });
595
606
  await new Promise((resolveReady, rejectReady) => {
@@ -611,133 +622,38 @@ async function startControlServer(ctx, wakeControlLoop) {
611
622
  };
612
623
  }
613
624
 
614
- function acquireControlsLock(ctx) {
615
- return acquireFileMutationLock(ctx.controlsFile);
616
- }
617
-
618
- function readControls(ctx) {
619
- if (!exists(ctx.controlsFile)) return [];
620
- return readFileSync(ctx.controlsFile, "utf8")
621
- .split("\n")
622
- .filter((line) => line.trim())
623
- .map((line) => {
624
- try {
625
- return JSON.parse(line);
626
- } catch {
627
- return undefined;
628
- }
629
- })
630
- .filter(Boolean);
631
- }
632
-
633
- function writeControls(ctx, controls) {
634
- writeTextAtomic(
635
- ctx.controlsFile,
636
- controls.length
637
- ? `${controls.map((control) => JSON.stringify(control)).join("\n")}\n`
638
- : "",
639
- );
640
- }
641
-
642
625
  function commandFromControl(control) {
643
626
  if (typeof control.action !== "string") return undefined;
644
627
  const action = control.action.trim();
645
628
  return CONTROL_COMMANDS.has(action) ? action : undefined;
646
629
  }
647
630
 
648
- function runtimeWakeFile(ctx) {
649
- return join(ctx.stateDir, "wake.jsonl");
650
- }
651
-
652
- function notifyControlWake(ctx, reason = "control.queued") {
653
- try {
654
- writeText(
655
- runtimeWakeFile(ctx),
656
- `${JSON.stringify({
657
- actor: `run:${basename(ctx.stateDir)}`,
658
- id: randomUUID(),
659
- metadata: { command: "music-player" },
660
- reason,
661
- state_dir: ctx.stateDir,
662
- ts: new Date().toISOString(),
663
- })}\n`,
664
- "a",
665
- );
666
- } catch {
667
- // Wake records are advisory; the Control journal remains authoritative.
668
- }
669
- }
670
-
671
631
  function appendControl(ctx, action) {
672
- const release = acquireControlsLock(ctx);
673
- try {
674
- const run = readJsonFile(runJsonFile(ctx), {});
675
- const controls = readControls(ctx);
676
- controls.push({
677
- action,
678
- id: randomUUID(),
679
- queued_at: new Date().toISOString(),
680
- run_instance_id: run.run_instance_id,
681
- status: "queued",
682
- });
683
- writeControls(ctx, controls);
684
- } finally {
685
- release();
686
- }
687
- notifyControlWake(ctx);
632
+ const run = readJsonFile(runJsonFile(ctx), {});
633
+ appendRunControlInStateDir(ctx.stateDir, {
634
+ action,
635
+ run_instance_id: run.run_instance_id,
636
+ });
688
637
  }
689
638
 
690
- function claimControls(ctx) {
691
- const release = acquireControlsLock(ctx);
692
- try {
693
- const controls = readControls(ctx);
694
- const commands = [];
695
- let changed = false;
696
- const claimedAt = new Date().toISOString();
697
- for (const control of controls) {
698
- if (control.status !== "queued" && control.status !== "delivered") continue;
699
- const command =
700
- control.run_instance_id === ctx.runInstanceId
701
- ? commandFromControl(control)
702
- : undefined;
703
- if (!command) {
704
- control.failed_at = claimedAt;
705
- control.status = "failed";
706
- control.error = "Unsupported or stale music-player Control";
707
- changed = true;
708
- continue;
709
- }
710
- control.claimed_at = claimedAt;
711
- control.status = "claimed";
712
- commands.push({ command, id: control.id });
713
- changed = true;
714
- }
715
- if (changed) writeControls(ctx, controls);
716
- return commands;
717
- } finally {
718
- release();
639
+ function claimControl(ctx, wire) {
640
+ const control = claimRunControlByIdInStateDir(
641
+ ctx.stateDir, ctx.runInstanceId, wire.id,
642
+ );
643
+ const command = control ? commandFromControl(control) : undefined;
644
+ if (!command && control) {
645
+ updateRunControlStatusInStateDir(ctx.stateDir, control.id, "failed", {
646
+ error: "Unsupported music-player Control",
647
+ }, ["claimed"]);
719
648
  }
649
+ return command && control ? { command, id: control.id } : undefined;
720
650
  }
721
651
 
722
652
  function finalizeControl(ctx, id, status, error) {
723
653
  if (!id) return;
724
- const release = acquireControlsLock(ctx);
725
- try {
726
- const controls = readControls(ctx);
727
- const timestamp = new Date().toISOString();
728
- let changed = false;
729
- for (const control of controls) {
730
- if (control.id !== id || control.status !== "claimed") continue;
731
- control.status = status;
732
- if (status === "handled") control.handled_at = timestamp;
733
- else control.failed_at = timestamp;
734
- if (error) control.error = error;
735
- changed = true;
736
- }
737
- if (changed) writeControls(ctx, controls);
738
- } finally {
739
- release();
740
- }
654
+ updateRunControlStatusInStateDir(
655
+ ctx.stateDir, id, status, error ? { error } : {}, ["claimed"],
656
+ );
741
657
  }
742
658
 
743
659
  function controlsSignature(ctx) {
@@ -759,8 +675,7 @@ function startControlLoop(ctx) {
759
675
  const name = file ? String(file) : "";
760
676
  if (
761
677
  !name ||
762
- name === basename(ctx.controlsFile) ||
763
- name === basename(runtimeWakeFile(ctx))
678
+ name === basename(ctx.controlsFile)
764
679
  ) {
765
680
  dirty = true;
766
681
  }
@@ -779,14 +694,6 @@ function startControlLoop(ctx) {
779
694
  const signature = controlsSignature(ctx);
780
695
  if (dirty || signature !== lastSignature) {
781
696
  dirty = false;
782
- for (const { command, id } of claimControls(ctx)) {
783
- try {
784
- handleControl(ctx, command);
785
- finalizeControl(ctx, id, "handled");
786
- } catch (error) {
787
- finalizeControl(ctx, id, "failed", error.message);
788
- }
789
- }
790
697
  lastSignature = controlsSignature(ctx);
791
698
  continue;
792
699
  }
@@ -859,7 +766,6 @@ async function playMain(args) {
859
766
  const ctx = {
860
767
  commandFile: join(stateDir, "command.txt"),
861
768
  current: undefined,
862
- eventFile: join(stateDir, "trace.jsonl"),
863
769
  controlsFile: join(stateDir, "controls.jsonl"),
864
770
  pidFile: join(stateDir, "current.pid"),
865
771
  playerControlFile: join(stateDir, "player-control.txt"),
@@ -145,13 +145,66 @@ try {
145
145
  }
146
146
  console.log("[release] removed-surface and legacy-fallback allowlists checked");
147
147
 
148
- const baselineShippedLines = 35_077;
148
+ const directTraceWrite = /(?:appendFileSync|writeFileSync|writeText(?:Atomic)?)\s*\(\s*[A-Za-z0-9_.]*?(?:trace|event)(?:Path|File)/iu;
149
+ for (const path of files.filter((candidate) =>
150
+ (candidate.startsWith("lib/") && candidate.endsWith(".ts")) ||
151
+ (candidate.startsWith("scripts/") && candidate.endsWith(".mjs"))
152
+ )) {
153
+ if (path === "lib/runs-trace.ts") continue;
154
+ check(!directTraceWrite.test(stagedText(path) ?? ""), `direct Trace writer outside canonical authority: ${path}`);
155
+ }
156
+ const canonicalTrace = stagedText("lib/runs-trace.ts") ?? "";
157
+ check(/withFileMutationLock\(path/u.test(canonicalTrace), "canonical Trace append lacks mutation lock");
158
+ for (const path of [
159
+ "scripts/async-runner.mjs",
160
+ "scripts/locker.mjs",
161
+ "scripts/music-player.mjs",
162
+ ]) {
163
+ const text = stagedText(path) ?? "";
164
+ check(
165
+ text.includes('importRuntimeModule("runs-trace")') && text.includes("appendRunTraceEvent"),
166
+ `first-party Trace writer bypasses canonical runtime module: ${path}`,
167
+ );
168
+ if (path !== "scripts/async-runner.mjs") check(
169
+ text.includes('importRuntimeModule("runs-controls")') &&
170
+ !/function (?:read|write|claimControls|finalizeControls)\b/u.test(text),
171
+ `first-party Control writer bypasses canonical runtime module: ${path}`,
172
+ );
173
+ }
174
+ console.log("[release] canonical Trace and Control writer residue checked");
175
+
176
+ // owner | class | lock | record bound | byte bound | recovery
177
+ const appendOnlyOwners = new Set([
178
+ "lib/runs-trace.ts", // canonical Run evidence | token lock | 2048 | 4 MiB | compact valid suffix
179
+ "lib/runs-retention.ts", // shared kernel evidence | token lock | 256 | 1 MiB | skip malformed
180
+ "scripts/locker.mjs", // first-party actor state | token lock | 512 | 1 MiB | skip malformed
181
+ "lib/command-templates.ts", // complete execution capture | command owner | n/a | artifact policy | preserve
182
+ "scripts/async-runner.mjs", // captures/prompts | Run owner | n/a | artifact policy | preserve
183
+ "scripts/recipe-utils.mjs", // user-declared artifact | caller owner | n/a | caller policy | preserve
184
+ "scripts/release-gates.mjs", // build/test-only source inventory | n/a | n/a | n/a | self-match
185
+ ]);
186
+ const appendOnlyPattern = /\bappendFileSync\b|\b(?:writeFileSync|writeTextAtomic)\s*\([^)]*\{\s*flag:\s*["']a["']/su;
187
+ for (const path of files.filter((candidate) => /^(?:lib\/.*\.ts|scripts\/.*\.mjs)$/u.test(candidate)))
188
+ if (appendOnlyPattern.test(stagedText(path) ?? ""))
189
+ check(appendOnlyOwners.has(path), `unregistered shipped append-only writer: ${path}`);
190
+ for (const path of appendOnlyOwners) check(fileSet.has(path), `stale append-only owner inventory: ${path}`);
191
+ console.log(`[release] append-only owner inventory checked: ${appendOnlyOwners.size} source owners`);
192
+
193
+ check(
194
+ (stagedText("scripts/validate-recipe.mjs") ?? "").includes(
195
+ "qaReport.diagnostics.length === 0 && qaReport.warnings.length === 0",
196
+ ),
197
+ "Recipe QA warnings are not release-blocking",
198
+ );
199
+ console.log("[release] zero-warning Recipe QA gate checked");
200
+
201
+ const maximumShippedLines = 29_598;
149
202
  const shippedPath = /^(?:lib\/|scripts\/|recipes\/|docs\/|skills\/)/u;
150
203
  const shippedLines = files
151
204
  .filter((path) => shippedPath.test(path))
152
205
  .reduce((total, path) => total + (((stagedText(path) ?? "").match(/\n/gu) ?? []).length + 1), 0);
153
- check(shippedLines < baselineShippedLines, `shipped lines ${shippedLines} are not below baseline ${baselineShippedLines}`);
154
- console.log(`[release] shipped lines ${shippedLines} < frozen baseline ${baselineShippedLines}`);
206
+ check(shippedLines <= maximumShippedLines, `shipped lines ${shippedLines} exceed release maximum ${maximumShippedLines}`);
207
+ console.log(`[release] shipped lines ${shippedLines} <= release maximum ${maximumShippedLines}`);
155
208
 
156
209
  const sources = files.filter((path) => path === "index.ts" || (path.startsWith("lib/") && path.endsWith(".ts")));
157
210
  const sourceSet = new Set(sources);
@@ -29,7 +29,7 @@ export function validateRecipeUsage() {
29
29
  return `Usage:
30
30
  validate-recipe.mjs <recipe-file-or-dir> [--all] [--qa] [--summary]
31
31
 
32
- Validates one template recipe file, or all *.json/*.md files in a directory when --all is set. Add --qa for packaged-recipe quality checks. Add --summary for compact CLI output.`;
32
+ Validates one template recipe file, or all *.json/*.md files in a directory when --all is set. Add --qa for packaged-recipe quality checks, where diagnostics and warnings fail validation. Add --summary for compact CLI output.`;
33
33
  }
34
34
 
35
35
  function expandPath(value) {
@@ -120,8 +120,6 @@ function validateHelperPaths(file, config) {
120
120
  function qaDiagnostics(file, config) {
121
121
  const diagnostics = [];
122
122
  const warnings = [];
123
- if (typeof config.description !== "string" || !config.description.trim())
124
- warnings.push("description: missing or empty");
125
123
  if (config.mailbox !== undefined)
126
124
  diagnostics.push("recipe.mailbox was removed; use control actions and Trace events");
127
125
  diagnostics.push(...validateArtifactDeclarations(config));
@@ -138,7 +136,7 @@ function qaDiagnostics(file, config) {
138
136
  }
139
137
 
140
138
  function qaOk(qaReport) {
141
- return qaReport.diagnostics.length === 0;
139
+ return qaReport.diagnostics.length === 0 && qaReport.warnings.length === 0;
142
140
  }
143
141
 
144
142
  function validateFile(file, qa = false) {
@@ -225,6 +223,9 @@ function summarizeReport(report) {
225
223
  ...(result.qa?.diagnostics?.length
226
224
  ? { diagnostics: result.qa.diagnostics }
227
225
  : {}),
226
+ ...(result.qa?.warnings?.length
227
+ ? { warnings: result.qa.warnings }
228
+ : {}),
228
229
  })),
229
230
  };
230
231
  }
@@ -1,8 +1,6 @@
1
1
  ---
2
2
  name: actors
3
3
  description: Required practical guide for non-trivial pi-actors use and Run-kernel work. Read before using or changing spawn, message, inspect, Runs, tools, Recipes, command templates, Control, Trace, artifacts, or lifecycle mechanics.
4
- metadata:
5
- version: 0.43.0
6
4
  ---
7
5
 
8
6
  # Actors (pi-actors)
@@ -38,22 +36,30 @@ Prefer maintained packaged Recipes over ad hoc wrappers. Keep model, thinking, m
38
36
  Trace records bounded structured observations in `trace.jsonl`:
39
37
 
40
38
  ```json
41
- {"id":"…","ts":"…","kind":"progress.update","summary":"…","data":{},"level":"info","attention":"notify"}
39
+ {
40
+ "id": "…",
41
+ "ts": "…",
42
+ "kind": "progress.update",
43
+ "summary": "…",
44
+ "data": {},
45
+ "level": "info",
46
+ "attention": "notify"
47
+ }
42
48
  ```
43
49
 
44
- Trace never carries sender, recipient, route, reply, or message-envelope fields. Use `attention: "notify"` for visible notification and `attention: "followup"` only when the coordinator must receive semantic follow-up context. Prefer artifacts or complete execution captures for large evidence.
50
+ Trace never carries sender, recipient, route, reply, or message-envelope fields. It is a bounded retained suffix: the canonical lock appends within 2,048 events and 4 MiB or atomically keeps the newest suffix plus one warning-only `runtime.trace_compacted` marker. That marker means older history was discarded; terminal/result/execution/artifact evidence stays independently authoritative. `inspect view=trace` reports completeness. Equal timestamps use same-source physical order, fixed source rank, then stable id without exposing an ordinal or claiming cross-source causality. Attention is a wake hint, not a queue: persist durable state or an artifact first, use `notify` for visible status, and reserve `followup` for needed coordinator context. Compaction may discard old hints.
45
51
 
46
52
  ## Control
47
53
 
48
54
  The public Control request is exact:
49
55
 
50
56
  ```json
51
- {"target":"run:<id>","action":"pause","input":{},"verbose":false}
57
+ { "target": "run:<id>", "action": "pause", "input": {}, "verbose": false }
52
58
  ```
53
59
 
54
- Controls persist in `controls.jsonl` before delivery. Token-owned locks serialize atomic journal replacements. Service endpoints publish readiness in `control-endpoint.json` with the immutable startup `run_instance_id`; only FIFO and named-pipe endpoints transport Controls. Expected-status-fenced transitions remain monotonic when a fast consumer completes before sender delivery evidence. FIFO documents must fit the portable 512-byte atomic-write bound, partial writes fail, and controlled FIFO readers remain gap-free across writers. Delivery revalidates owner, generation, running state, and process identity under the lifecycle lock.
60
+ Valid Controls persist in `controls.jsonl` before delivery; invalid envelopes remain outside it. One token-owned lock rejects a 65th pending Control or 1 MiB rewrite before admission, fails closed on malformed or stale-generation evidence, and atomically admits one queued record. Exact-id claims/finalization preserve a 128-terminal tail, expected-state fencing, and 4 KiB errors. Admitted nonterminal Controls never expire automatically. `inspect view=control` reports capacity, saturation, stale work, bytes, and diagnostics. Endpoints carry immutable startup `run_instance_id`; FIFO and named pipe share limits of 64 action characters, 380 serialized input bytes, and 512 newline-terminated wire bytes. Partial writes fail. Put larger data in an artifact and send only its reference. Delivery revalidates owner, generation, state, and process identity.
55
61
 
56
- `kill` remains a runtime lifecycle action. Use an actor-local action such as `stop` only when the Recipe declares and implements it.
62
+ `kill` remains the runtime recovery path for a stuck saturated Run: it bypasses actor-local Control capacity and adds no synthetic Control. Use actor-local `stop` only when declared and implemented. Restart clears generation-local evidence; archive preserves the bounded terminal tree, while prune preserves only requested artifacts.
57
63
 
58
64
  ## Run State and Safety
59
65
 
@@ -66,6 +72,8 @@ Run state lives under `~/.pi/agent/tmp/pi-actors/runs/<run>/`. Important evidenc
66
72
  - `execution.json`: command/session provenance and bounded complete-capture references.
67
73
  - `result.json`, logs, and declared artifacts.
68
74
 
75
+ Trace/Control quotas do not constrain user-declared artifacts, repositories, media sources, complete captures, or actor-owned workload state. No public noun, tool, target, or view is added by bounded retention.
76
+
69
77
  Never bypass owner filtering, immutable generation fencing, process-identity verification, path containment, redaction, terminal reconciliation, or shutdown kill behavior. Do not edit active Run state to force a result.
70
78
 
71
79
  ## Operating Pattern
@@ -73,8 +81,8 @@ Never bypass owner filtering, immutable generation fencing, process-identity ver
73
81
  1. Inspect the Recipe before launch when its contract or policy matters.
74
82
  2. Spawn with explicit values and retain the returned `run:<id>`.
75
83
  3. Let short Runs finish; avoid polling.
76
- 4. Inspect Trace when evidence or attention requires it.
77
- 5. Send only declared actor-local Controls.
84
+ 4. Inspect Trace when evidence or attention requires it; its summary states whether retained history is complete.
85
+ 5. Inspect Control capacity before diagnosing stale work or saturation, then send only declared actor-local Controls.
78
86
  6. Use runtime kill/cancel behavior for lifecycle termination.
79
87
  7. Inspect artifacts and execution evidence for final validation.
80
88
 
@@ -90,9 +98,7 @@ If work may outlive the current turn, needs steering, produces artifacts, fans o
90
98
 
91
99
  ## Deep References
92
100
 
93
- - [Actors deep reference](../../docs/actors-deep-reference.md)
94
101
  - [Recipe library](../../docs/recipe-library.md)
95
102
  - [Async Runs](../../docs/async-runs.md)
96
- - [Baseline and preservation gates](../../docs/0.43-baseline.md)
97
103
 
98
104
  Read repository source and tests for exact contracts when changing pi-actors itself. Update this skill whenever durable Run mechanics change.
@@ -1,8 +1,6 @@
1
1
  ---
2
2
  name: swarm
3
3
  description: Subagent and actor orchestration with scoped locks, fanout, and quorum consensus. Use before launching multiple parallel actors or subagents for independent implementation, artifact generation, review, delegated audit, coordinated execution, or any workflow that needs autonomous coordinator decomposition and integration.
4
- metadata:
5
- version: 0.43.0
6
4
  ---
7
5
 
8
6
  # Swarm
@@ -267,9 +265,9 @@ Report white spots, contradictions, evidence, and risks.
267
265
 
268
266
  Detached execution is an adapter concern, not a portable Swarm-script requirement. When the host offers Runs, launch the composed Recipe, return its id, and rely on terminal follow-up rather than blocking or polling.
269
267
 
270
- `Progress contract`: expose bounded structured Trace, logs, artifacts, status, timestamps, and final result evidence. Read these through Run inspection rather than scraping process output.
268
+ `Progress contract`: expose bounded structured Trace, logs, artifacts, status, timestamps, and final result evidence. Treat Trace as a retained suffix and attention as a wake hint; write durable task cards, checkpoints, and large evidence to artifacts before signaling attention. Read completeness and Control capacity through existing Run inspection rather than scraping output. Artifact size/lifecycle remains separate from Trace/Control quotas.
271
269
 
272
- `Resumable checkpoint goal`: a controlled agent-backed Run may preserve context and accept a declared Control. When the host cannot preserve context, write a handoff artifact and launch a clean-context Run while marking the context loss explicitly.
270
+ `Resumable checkpoint goal`: a controlled agent-backed Run may preserve context and accept a declared Control. Control saturation rejects before admission and admitted work does not expire; lifecycle recovery remains host-owned. When the host cannot preserve context, write a handoff artifact and launch a clean-context Run while marking the context loss explicitly.
273
271
 
274
272
  `Cancellation boundary`: terminate only an owned active generation whose process identity the runtime can prove. Stale pid reuse must fail closed.
275
273