@llblab/pi-actors 0.41.1 → 0.42.1

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 (157) hide show
  1. package/AGENTS.md +11 -6
  2. package/BACKLOG.md +1 -65
  3. package/CHANGELOG.md +25 -0
  4. package/README.md +24 -4
  5. package/dist/index.js +25 -121
  6. package/dist/lib/async-runs.d.ts +37 -5
  7. package/dist/lib/async-runs.js +181 -51
  8. package/dist/lib/automatic-review-runtime.d.ts +18 -0
  9. package/dist/lib/automatic-review-runtime.js +96 -0
  10. package/dist/lib/command-templates.js +1 -1
  11. package/dist/lib/draft-consolidation-transaction.d.ts +65 -0
  12. package/dist/lib/draft-consolidation-transaction.js +610 -0
  13. package/dist/lib/draft-consolidation.d.ts +35 -0
  14. package/dist/lib/draft-consolidation.js +126 -0
  15. package/dist/lib/draft-review.d.ts +56 -0
  16. package/dist/lib/draft-review.js +254 -0
  17. package/dist/lib/draft-sleep.d.ts +65 -0
  18. package/dist/lib/draft-sleep.js +468 -0
  19. package/dist/lib/file-state.d.ts +8 -1
  20. package/dist/lib/file-state.js +115 -18
  21. package/dist/lib/inspector-actions.d.ts +16 -0
  22. package/dist/lib/inspector-actions.js +57 -0
  23. package/dist/lib/inspector-command.d.ts +7 -0
  24. package/dist/lib/inspector-command.js +37 -0
  25. package/dist/lib/inspector-overlay.d.ts +23 -3
  26. package/dist/lib/inspector-overlay.js +327 -70
  27. package/dist/lib/inspector.d.ts +9 -0
  28. package/dist/lib/inspector.js +49 -1
  29. package/dist/lib/observability.d.ts +13 -0
  30. package/dist/lib/observability.js +160 -6
  31. package/dist/lib/paths.d.ts +7 -0
  32. package/dist/lib/paths.js +28 -0
  33. package/dist/lib/recipes-discovery.js +32 -24
  34. package/dist/lib/recipes-usage.d.ts +18 -6
  35. package/dist/lib/recipes-usage.js +445 -34
  36. package/dist/lib/review-control.d.ts +14 -0
  37. package/dist/lib/review-control.js +111 -0
  38. package/dist/lib/review-diagnostics.d.ts +11 -0
  39. package/dist/lib/review-diagnostics.js +148 -0
  40. package/dist/lib/review-projection.d.ts +14 -0
  41. package/dist/lib/review-projection.js +170 -0
  42. package/dist/lib/run-ui-runtime.d.ts +18 -0
  43. package/dist/lib/run-ui-runtime.js +123 -0
  44. package/dist/lib/runs-artifacts.d.ts +1 -1
  45. package/dist/lib/runs-artifacts.js +1 -1
  46. package/dist/lib/runs-control.d.ts +10 -2
  47. package/dist/lib/runs-control.js +37 -7
  48. package/dist/lib/runs-identity.d.ts +1 -1
  49. package/dist/lib/runs-identity.js +1 -1
  50. package/dist/lib/runs-index.d.ts +11 -2
  51. package/dist/lib/runs-index.js +46 -23
  52. package/dist/lib/runs-mailbox.d.ts +1 -1
  53. package/dist/lib/runs-mailbox.js +1 -1
  54. package/dist/lib/runs-messages.d.ts +1 -1
  55. package/dist/lib/runs-messages.js +1 -1
  56. package/dist/lib/runs-outbox.d.ts +1 -1
  57. package/dist/lib/runs-outbox.js +1 -1
  58. package/dist/lib/runs-ownership.d.ts +1 -1
  59. package/dist/lib/runs-ownership.js +18 -4
  60. package/dist/lib/runs-parent-teardown.d.ts +51 -0
  61. package/dist/lib/runs-parent-teardown.js +172 -0
  62. package/dist/lib/runs-process.d.ts +1 -1
  63. package/dist/lib/runs-process.js +5 -4
  64. package/dist/lib/runs-retention.d.ts +1 -1
  65. package/dist/lib/runs-retention.js +1 -1
  66. package/dist/lib/runs-start.d.ts +5 -3
  67. package/dist/lib/runs-start.js +7 -48
  68. package/dist/lib/runs-status.d.ts +5 -3
  69. package/dist/lib/runs-status.js +7 -6
  70. package/dist/lib/runtime.d.ts +7 -1
  71. package/dist/lib/runtime.js +32 -18
  72. package/dist/lib/tool-review-lineage-transaction.d.ts +27 -0
  73. package/dist/lib/tool-review-lineage-transaction.js +597 -0
  74. package/dist/lib/tool-review-lineage.d.ts +24 -0
  75. package/dist/lib/tool-review-lineage.js +98 -0
  76. package/dist/lib/tool-review-scheduler.d.ts +80 -0
  77. package/dist/lib/tool-review-scheduler.js +494 -0
  78. package/dist/lib/tool-review-transaction.d.ts +50 -0
  79. package/dist/lib/tool-review-transaction.js +362 -0
  80. package/dist/lib/tool-review.d.ts +56 -0
  81. package/dist/lib/tool-review.js +197 -0
  82. package/dist/lib/tools-inspect.js +28 -4
  83. package/dist/lib/tools-local.js +21 -4
  84. package/dist/lib/tools-message.d.ts +1 -0
  85. package/dist/lib/tools-message.js +29 -17
  86. package/dist/lib/tools-spawn.js +14 -2
  87. package/dist/lib/tools.d.ts +1 -0
  88. package/dist/lib/tools.js +1 -0
  89. package/dist/recipes/draft-review.json +24 -0
  90. package/dist/recipes/tool-review.json +24 -0
  91. package/dist/scripts/async-runner.mjs +9 -9
  92. package/dist/scripts/build-dist.mjs +6 -1
  93. package/dist/scripts/conformance.mjs +6 -1
  94. package/dist/scripts/recipe-utils.mjs +3 -3
  95. package/dist/scripts/release-gates.mjs +165 -0
  96. package/dist/skills/actors/SKILL.md +9 -8
  97. package/dist/skills/swarm/SKILL.md +1 -1
  98. package/docs/actor-inspector.md +20 -11
  99. package/docs/async-runs.md +16 -2
  100. package/docs/recipe-library.md +7 -3
  101. package/docs/template-recipes.md +4 -11
  102. package/docs/tool-registry.md +13 -4
  103. package/index.ts +27 -142
  104. package/lib/async-runs.ts +283 -67
  105. package/lib/automatic-review-runtime.ts +135 -0
  106. package/lib/command-templates.ts +1 -1
  107. package/lib/draft-consolidation-transaction.ts +821 -0
  108. package/lib/draft-consolidation.ts +181 -0
  109. package/lib/draft-review.ts +325 -0
  110. package/lib/draft-sleep.ts +576 -0
  111. package/lib/file-state.ts +143 -19
  112. package/lib/inspector-actions.ts +79 -0
  113. package/lib/inspector-command.ts +54 -0
  114. package/lib/inspector-overlay.ts +377 -91
  115. package/lib/inspector.ts +73 -1
  116. package/lib/observability.ts +194 -5
  117. package/lib/paths.ts +43 -0
  118. package/lib/recipes-discovery.ts +34 -26
  119. package/lib/recipes-usage.ts +569 -40
  120. package/lib/review-control.ts +137 -0
  121. package/lib/review-diagnostics.ts +164 -0
  122. package/lib/review-projection.ts +200 -0
  123. package/lib/run-ui-runtime.ts +153 -0
  124. package/lib/runs-artifacts.ts +1 -1
  125. package/lib/runs-control.ts +69 -6
  126. package/lib/runs-identity.ts +1 -1
  127. package/lib/runs-index.ts +57 -21
  128. package/lib/runs-mailbox.ts +1 -1
  129. package/lib/runs-messages.ts +1 -1
  130. package/lib/runs-outbox.ts +1 -1
  131. package/lib/runs-ownership.ts +23 -4
  132. package/lib/runs-parent-teardown.ts +257 -0
  133. package/lib/runs-process.ts +5 -4
  134. package/lib/runs-retention.ts +1 -1
  135. package/lib/runs-start.ts +13 -68
  136. package/lib/runs-status.ts +17 -8
  137. package/lib/runtime.ts +34 -17
  138. package/lib/tool-review-lineage-transaction.ts +881 -0
  139. package/lib/tool-review-lineage.ts +145 -0
  140. package/lib/tool-review-scheduler.ts +635 -0
  141. package/lib/tool-review-transaction.ts +563 -0
  142. package/lib/tool-review.ts +270 -0
  143. package/lib/tools-inspect.ts +35 -4
  144. package/lib/tools-local.ts +29 -4
  145. package/lib/tools-message.ts +45 -30
  146. package/lib/tools-spawn.ts +22 -2
  147. package/lib/tools.ts +5 -0
  148. package/package.json +5 -3
  149. package/recipes/draft-review.json +24 -0
  150. package/recipes/tool-review.json +24 -0
  151. package/scripts/async-runner.mjs +9 -9
  152. package/scripts/build-dist.mjs +6 -1
  153. package/scripts/conformance.mjs +6 -1
  154. package/scripts/recipe-utils.mjs +3 -3
  155. package/scripts/release-gates.mjs +165 -0
  156. package/skills/actors/SKILL.md +9 -8
  157. package/skills/swarm/SKILL.md +1 -1
@@ -19,10 +19,14 @@ export function readActorInspectorRuns(stateRoot, ownerId) {
19
19
  const stateDir = path.join(stateRoot, entry.name);
20
20
  if (!matchesOwner(stateDir, ownerId))
21
21
  return [];
22
+ const runMeta = readJsonFileResilient(path.join(stateDir, "run.json"), {}).value;
22
23
  const progress = readJsonFileResilient(path.join(stateDir, "progress.json"), {}).value;
23
24
  const result = readJsonFileResilient(path.join(stateDir, "result.json"), {}).value;
24
25
  return [{
25
26
  run: entry.name,
27
+ ...(typeof runMeta.run_instance_id === "string"
28
+ ? { runInstanceId: runMeta.run_instance_id }
29
+ : {}),
26
30
  status: String(progress.phase ?? (Object.keys(result).length ? "terminal" : "unknown")),
27
31
  ...(typeof progress.updatedAt === "string"
28
32
  ? { updatedAt: progress.updatedAt }
@@ -42,6 +46,50 @@ export function readActorInspectorRuns(stateRoot, ownerId) {
42
46
  return [];
43
47
  }
44
48
  }
49
+ export function readActorInspectorRecipe(stateDir) {
50
+ const result = readJsonFileResilient(path.join(stateDir, "run.json"), {});
51
+ const meta = result.value;
52
+ const contexts = Array.isArray(meta.recipe_context_records)
53
+ ? meta.recipe_context_records
54
+ .filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry)))
55
+ : [];
56
+ const primary = contexts.find((entry) => entry.depth === 0) ?? contexts[0];
57
+ const definition = primary?.recipe && typeof primary.recipe === "object" &&
58
+ !Array.isArray(primary.recipe)
59
+ ? primary.recipe
60
+ : undefined;
61
+ const redacted = (value) => SessionEvidence.redactSessionEvidenceValue(value);
62
+ const redactedRecord = (value) => redacted(Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined)));
63
+ return {
64
+ composition: contexts.map((entry) => redactedRecord({
65
+ ...(typeof entry.alias === "string" ? { alias: entry.alias } : {}),
66
+ depth: entry.depth,
67
+ file: entry.file,
68
+ import_path: entry.import_path,
69
+ name: entry.name,
70
+ })),
71
+ ...(definition
72
+ ? { definition: redactedRecord(definition) }
73
+ : {}),
74
+ diagnostics: result.diagnostics.map((diagnostic) => diagnostic.message),
75
+ identity: redactedRecord({
76
+ run: meta.run,
77
+ recipe: meta.recipe,
78
+ recipe_file: meta.recipe_file,
79
+ launch_source: meta.launch_source,
80
+ }),
81
+ launch: redactedRecord({
82
+ cwd: meta.cwd,
83
+ template: meta.template,
84
+ values: meta.values,
85
+ model_policy: meta.model_policy,
86
+ mailbox: meta.mailbox,
87
+ artifacts: meta.artifacts,
88
+ notification_policy: meta.notification_policy,
89
+ retire_when: meta.retire_when,
90
+ }),
91
+ };
92
+ }
45
93
  function ownedSessionPath(stateDir, sessionFile) {
46
94
  if (path.isAbsolute(sessionFile))
47
95
  return undefined;
@@ -78,7 +126,7 @@ export function readActorInspectorTurns(stateDir) {
78
126
  withFileTypes: true,
79
127
  })
80
128
  .filter((file) => file.isFile() && file.name.endsWith(".jsonl"))
81
- .map((file) => path.join("sessions", entry.name, file.name))
129
+ .map((file) => path.posix.join("sessions", entry.name, file.name))
82
130
  .filter((file) => !recordedFiles.has(file));
83
131
  return files.length > 0
84
132
  ? [{ id: entry.name, session_files: files, stage: "subagent" }]
@@ -15,12 +15,15 @@ export interface RunObservation {
15
15
  failures?: number;
16
16
  ownerId?: string;
17
17
  artifacts?: Record<string, string>;
18
+ launchCorrelation?: Record<string, string>;
18
19
  launchSource?: AsyncRuns.AsyncRunLaunchSource;
19
20
  modelPolicy?: Record<string, unknown>;
21
+ notificationPolicy?: "normal" | "silent";
20
22
  recipeFile?: string;
21
23
  terminalHandled?: boolean;
22
24
  retireWhen?: string;
23
25
  run: string;
26
+ semanticResult?: RunTerminalSemanticResult;
24
27
  tool?: string;
25
28
  stateDir?: string;
26
29
  status: RunObservedStatus;
@@ -130,12 +133,22 @@ export interface RunTransition {
130
133
  run: string;
131
134
  stateDir?: string;
132
135
  artifacts?: Record<string, string>;
136
+ launchCorrelation?: Record<string, string>;
133
137
  launchSource?: AsyncRuns.AsyncRunLaunchSource;
134
138
  modelPolicy?: Record<string, unknown>;
135
139
  recipeFile?: string;
136
140
  terminalHandled?: boolean;
137
141
  to: RunObservedStatus;
138
142
  tool?: string;
143
+ semanticResult?: RunTerminalSemanticResult;
144
+ }
145
+ export interface RunTerminalSemanticResult {
146
+ body?: string;
147
+ correlationId?: string;
148
+ metadata: Record<string, unknown>;
149
+ summary: string;
150
+ synthesized: boolean;
151
+ type: string;
139
152
  }
140
153
  export interface RunOutboxEvent {
141
154
  body?: unknown;
@@ -3,7 +3,7 @@
3
3
  * Zones: async runtime, ambient UI, diagnostics
4
4
  * Owns ambient summaries, terminal events, and run outbox delivery for detached command-template runs
5
5
  */
6
- import { existsSync, readdirSync, readFileSync, watch, } from "node:fs";
6
+ import { closeSync, existsSync, fstatSync, openSync, readdirSync, readFileSync, readSync, watch, } from "node:fs";
7
7
  import { basename, dirname, isAbsolute, join, relative, resolve, } from "node:path";
8
8
  import * as AsyncRuns from "./async-runs.js";
9
9
  import * as Paths from "./paths.js";
@@ -54,6 +54,13 @@ export function deliverRunTransitionNotifications(transitions, sink, inFlight =
54
54
  AsyncRuns.markRunTerminalNotificationHandled(transition.stateDir, transition.to);
55
55
  }
56
56
  }
57
+ catch (error) {
58
+ if (transition.stateDir) {
59
+ AsyncRuns.recordRunTerminalDeliveryFailure(transition.stateDir, transition.to, error);
60
+ }
61
+ const message = error instanceof Error ? error.message : String(error);
62
+ sink.notify(`Actor terminal delivery failed for run:${transition.run}: ${message.replaceAll(/\s+/g, " ").slice(0, 240)}`, "error");
63
+ }
57
64
  finally {
58
65
  inFlight.delete(key);
59
66
  }
@@ -282,6 +289,121 @@ function scanRunStateDirs(stateRoot, depth = 0, seen = new Set()) {
282
289
  }
283
290
  return result;
284
291
  }
292
+ const TERMINAL_RESULT_BYTES = 8 * 1024;
293
+ const TERMINAL_RESULT_CHARS = 4_000;
294
+ function readBoundedStart(path) {
295
+ if (!existsSync(path))
296
+ return "";
297
+ const fd = openSync(path, "r");
298
+ try {
299
+ const size = Math.min(fstatSync(fd).size, TERMINAL_RESULT_BYTES);
300
+ const buffer = Buffer.alloc(size);
301
+ const bytes = readSync(fd, buffer, 0, size, 0);
302
+ const text = buffer.subarray(0, bytes).toString("utf8").trim();
303
+ return text.length > TERMINAL_RESULT_CHARS
304
+ ? `${text.slice(0, TERMINAL_RESULT_CHARS - 1)}…`
305
+ : text;
306
+ }
307
+ finally {
308
+ closeSync(fd);
309
+ }
310
+ }
311
+ function semanticBodyFromStdout(stateDir) {
312
+ const text = readBoundedStart(join(stateDir, "stdout.log"));
313
+ if (!text)
314
+ return undefined;
315
+ const withoutMarker = text.replace(/^ACTOR_REVIEW_RESULT\s*(?:\r?\n)?/, "").trim();
316
+ return withoutMarker || undefined;
317
+ }
318
+ function terminalSemanticResult(status, stateDir, observedStatus) {
319
+ const correlation = status.launch_correlation &&
320
+ typeof status.launch_correlation === "object" &&
321
+ !Array.isArray(status.launch_correlation)
322
+ ? status.launch_correlation
323
+ : {};
324
+ const correlationId = typeof correlation.correlation_id === "string"
325
+ ? correlation.correlation_id
326
+ : typeof correlation.tool_call_id === "string"
327
+ ? correlation.tool_call_id
328
+ : undefined;
329
+ const mailbox = status.mailbox &&
330
+ typeof status.mailbox === "object" &&
331
+ !Array.isArray(status.mailbox)
332
+ ? status.mailbox
333
+ : {};
334
+ const emits = Array.isArray(mailbox.emits)
335
+ ? mailbox.emits.filter((item) => typeof item === "string")
336
+ : [];
337
+ const outbox = readJsonlFileResilient(join(stateDir, "outbox.jsonl")).records;
338
+ const explicit = outbox.findLast((record) => {
339
+ const type = String(record.type ?? record.event ?? "");
340
+ return emits.includes(type) && !["command.done", "run.done", "run.failed"].includes(type);
341
+ });
342
+ if (explicit) {
343
+ const type = String(explicit.type ?? explicit.event);
344
+ return {
345
+ ...(explicit.body === undefined
346
+ ? {} : { body: formatSemanticBody(explicit.body) }),
347
+ ...(typeof explicit.correlation_id === "string"
348
+ ? { correlationId: explicit.correlation_id } : correlationId ? { correlationId } : {}),
349
+ metadata: {
350
+ ...(explicit.metadata &&
351
+ typeof explicit.metadata === "object" &&
352
+ !Array.isArray(explicit.metadata)
353
+ ? explicit.metadata : {}),
354
+ ...(status.transport_context &&
355
+ typeof status.transport_context === "object" &&
356
+ !Array.isArray(status.transport_context)
357
+ ? {
358
+ transport_context: status.transport_context,
359
+ } : {}),
360
+ run: String(status.run ?? ""),
361
+ status: observedStatus,
362
+ },
363
+ summary: String(explicit.summary ?? type),
364
+ synthesized: false,
365
+ type,
366
+ };
367
+ }
368
+ const reviewCompleted = observedStatus === "done" && emits.includes("review.completed");
369
+ const result = status.result &&
370
+ typeof status.result === "object" &&
371
+ !Array.isArray(status.result)
372
+ ? status.result
373
+ : {};
374
+ const type = reviewCompleted
375
+ ? "review.completed"
376
+ : observedStatus === "done" ? "run.done" : "run.failed";
377
+ const stdoutBody = observedStatus === "done"
378
+ ? semanticBodyFromStdout(stateDir) : undefined;
379
+ return {
380
+ ...(stdoutBody
381
+ ? { body: stdoutBody }
382
+ : typeof result.error === "string" ? { body: result.error } : {}),
383
+ ...(correlationId ? { correlationId } : {}),
384
+ metadata: {
385
+ ...(status.transport_context &&
386
+ typeof status.transport_context === "object" &&
387
+ !Array.isArray(status.transport_context)
388
+ ? {
389
+ transport_context: status.transport_context,
390
+ } : {}),
391
+ run: String(status.run ?? ""), status: observedStatus,
392
+ },
393
+ summary: reviewCompleted ? "Review completed." :
394
+ observedStatus === "done" ? "Run completed." : `Run ${observedStatus}.`,
395
+ synthesized: true,
396
+ type,
397
+ };
398
+ }
399
+ function formatSemanticBody(body) {
400
+ const rendered = typeof body === "string" ? body : JSON.stringify(body);
401
+ const text = typeof rendered === "string" ? rendered : String(body);
402
+ const compact = text.trim();
403
+ return compact.length > TERMINAL_RESULT_CHARS
404
+ ? `${compact.slice(0, TERMINAL_RESULT_CHARS - 1)}…`
405
+ : compact;
406
+ }
285
407
  function observeRun(stateDir) {
286
408
  try {
287
409
  const status = AsyncRuns.getRunStatus(stateDir);
@@ -289,9 +411,15 @@ function observeRun(stateDir) {
289
411
  const run = typeof status.run === "string" ? status.run : undefined;
290
412
  if (!run)
291
413
  return undefined;
414
+ const observedStatus = status.status;
292
415
  return {
293
416
  activeSubagents: toNumber(progress.activeSubagents),
294
417
  completed: toNumber(progress.completed),
418
+ ...(status.launch_correlation &&
419
+ typeof status.launch_correlation === "object" &&
420
+ !Array.isArray(status.launch_correlation)
421
+ ? { launchCorrelation: status.launch_correlation }
422
+ : {}),
295
423
  failures: Array.isArray(progress.failures)
296
424
  ? progress.failures.length
297
425
  : undefined,
@@ -311,6 +439,9 @@ function observeRun(stateDir) {
311
439
  !Array.isArray(status.model_policy)
312
440
  ? { modelPolicy: status.model_policy }
313
441
  : {}),
442
+ ...(status.notification_policy === "silent"
443
+ ? { notificationPolicy: "silent" }
444
+ : {}),
314
445
  ...(typeof status.recipe_file === "string"
315
446
  ? { recipeFile: status.recipe_file }
316
447
  : {}),
@@ -318,9 +449,12 @@ function observeRun(stateDir) {
318
449
  ...(typeof status.retire_when === "string"
319
450
  ? { retireWhen: status.retire_when }
320
451
  : {}),
452
+ ...(TERMINAL.has(observedStatus)
453
+ ? { semanticResult: terminalSemanticResult(status, stateDir, observedStatus) }
454
+ : {}),
321
455
  run,
322
456
  stateDir,
323
- status: status.status,
457
+ status: observedStatus,
324
458
  ...(typeof status.tool === "string" ? { tool: status.tool } : {}),
325
459
  updatedAt: getUpdatedAt(status),
326
460
  };
@@ -615,15 +749,19 @@ export function detectRunTransitions(previous, summary) {
615
749
  for (const run of summary.runs) {
616
750
  const key = runObservationKey(run);
617
751
  const old = previous.get(key);
618
- if (!run.terminalHandled && TERMINAL.has(run.status)) {
752
+ if (run.notificationPolicy !== "silent" &&
753
+ !run.terminalHandled &&
754
+ TERMINAL.has(run.status)) {
619
755
  transitions.push({
620
756
  from: old ?? "running",
621
757
  run: run.run,
622
758
  ...(run.stateDir ? { stateDir: run.stateDir } : {}),
623
759
  ...(run.artifacts ? { artifacts: run.artifacts } : {}),
760
+ ...(run.launchCorrelation ? { launchCorrelation: run.launchCorrelation } : {}),
624
761
  ...(run.launchSource ? { launchSource: run.launchSource } : {}),
625
762
  ...(run.modelPolicy ? { modelPolicy: run.modelPolicy } : {}),
626
763
  ...(run.recipeFile ? { recipeFile: run.recipeFile } : {}),
764
+ ...(run.semanticResult ? { semanticResult: run.semanticResult } : {}),
627
765
  ...(run.terminalHandled ? { terminalHandled: true } : {}),
628
766
  to: run.status,
629
767
  ...(run.tool ? { tool: run.tool } : {}),
@@ -707,6 +845,11 @@ export function detectRunOutboxEvents(previousLineCounts, summary, seenEventIds
707
845
  const key = run.stateDir ?? run.run;
708
846
  const records = readOutboxRecords(run);
709
847
  const previousCount = previousLineCounts.get(key) ?? 0;
848
+ if (run.notificationPolicy === "silent") {
849
+ previousLineCounts.set(key, records.length);
850
+ seenEventIds.set(key, seenEventIds.get(key) ?? new Set());
851
+ continue;
852
+ }
710
853
  const start = Math.min(previousCount, records.length);
711
854
  const seen = seenEventIds.get(key) ?? new Set();
712
855
  for (let index = start; index < records.length; index += 1) {
@@ -831,7 +974,8 @@ function isUserRecipeFile(file) {
831
974
  return false;
832
975
  const recipeRoot = resolve(Paths.getRecipeRoot());
833
976
  const path = resolve(file);
834
- return path === recipeRoot || path.startsWith(`${recipeRoot}/`);
977
+ const relation = relative(recipeRoot, path);
978
+ return relation === "" || (!relation.startsWith("..") && !isAbsolute(relation));
835
979
  }
836
980
  export function shouldSuggestRecipePersistence(transition) {
837
981
  if (transition.to !== "done")
@@ -877,16 +1021,26 @@ function formatTransitionNextActions(transition) {
877
1021
  ].filter(Boolean);
878
1022
  return `\nNext actions: ${actions.join(" | ")}`;
879
1023
  }
1024
+ function formatTransitionSemanticResult(transition) {
1025
+ const result = transition.semanticResult;
1026
+ if (!result)
1027
+ return "";
1028
+ const correlation = result.correlationId
1029
+ ? `\nCorrelation: ${result.correlationId}` : "";
1030
+ const body = result.body ? `\nResult:\n${result.body}` : "";
1031
+ return `\nSemantic result: ${result.type} — ${result.summary}${correlation}${body}`;
1032
+ }
880
1033
  export function formatRunTransitionMessage(transition) {
881
1034
  const artifacts = formatNamedArtifacts(transition.artifacts);
882
1035
  const runFiles = formatRunFileList(getRunArtifacts(transition));
883
1036
  const persistenceSuggestion = formatRecipePersistenceSuggestion(transition);
884
1037
  const policy = formatTransitionPolicy(transition);
885
1038
  const nextActions = formatTransitionNextActions(transition);
1039
+ const semanticResult = formatTransitionSemanticResult(transition);
886
1040
  if (transition.to === "done")
887
- return `Run ${transition.run} completed successfully.${policy}${artifacts}${runFiles}${nextActions}${persistenceSuggestion}`;
1041
+ return `Run ${transition.run} completed successfully.${semanticResult}${policy}${artifacts}${runFiles}${nextActions}${persistenceSuggestion}`;
888
1042
  if (transition.to === "failed")
889
- return `Run ${transition.run} failed.${policy}${artifacts}${runFiles}${nextActions}`;
1043
+ return `Run ${transition.run} failed.${semanticResult}${policy}${artifacts}${runFiles}${nextActions}`;
890
1044
  if (transition.to === "cancelled")
891
1045
  return `Run ${transition.run} was cancelled.${policy}${nextActions}`;
892
1046
  if (transition.to === "killed")
@@ -4,6 +4,7 @@
4
4
  * Owns agent directory, tools config, recipe root, and actor run state root resolution
5
5
  */
6
6
  export declare function getAgentDir(env?: Record<string, string | undefined>): string;
7
+ export declare function isAutomaticRecipeReviewEnabled(env?: Record<string, string | undefined>): boolean;
7
8
  export interface ExtensionRuntimePaths {
8
9
  configPath: string;
9
10
  runStateRoot: string;
@@ -12,6 +13,12 @@ export interface ExtensionRuntimePaths {
12
13
  export declare function getConfigPath(agentDir?: string): string;
13
14
  export declare function getExtensionTmpDir(agentDir?: string, extensionName?: string): string;
14
15
  export declare function getRunStateRoot(agentDir?: string): string;
16
+ export declare function getDraftSleepRoot(agentDir?: string): string;
17
+ export declare function getDraftSleepStatePath(agentDir?: string): string;
18
+ export declare function getDraftSleepBatchDir(batchId: string, agentDir?: string): string;
19
+ export declare function getToolReviewRoot(agentDir?: string): string;
20
+ export declare function getToolReviewStatePath(agentDir?: string): string;
21
+ export declare function getToolReviewBatchDir(reviewId: string, agentDir?: string): string;
15
22
  export declare function getExtensionRuntimePaths(agentDir?: string): ExtensionRuntimePaths;
16
23
  export declare const EXTENSION_RUNTIME_PATHS: ExtensionRuntimePaths;
17
24
  export declare function getExtensionSkillsDir(extensionUrl: string): string;
package/dist/lib/paths.js CHANGED
@@ -12,6 +12,10 @@ export function getAgentDir(env = process.env) {
12
12
  ? resolve(env.PI_CODING_AGENT_DIR)
13
13
  : join(homedir(), ".pi", "agent");
14
14
  }
15
+ export function isAutomaticRecipeReviewEnabled(env = process.env) {
16
+ const value = env.PI_ACTORS_AUTOMATIC_REVIEW?.trim().toLowerCase();
17
+ return value === undefined || !["0", "false", "off"].includes(value);
18
+ }
15
19
  export function getConfigPath(agentDir = getAgentDir()) {
16
20
  return join(agentDir, "tool-registry.json");
17
21
  }
@@ -21,6 +25,30 @@ export function getExtensionTmpDir(agentDir = getAgentDir(), extensionName = "pi
21
25
  export function getRunStateRoot(agentDir = getAgentDir()) {
22
26
  return join(getExtensionTmpDir(agentDir), "runs");
23
27
  }
28
+ export function getDraftSleepRoot(agentDir = getAgentDir()) {
29
+ return join(getExtensionTmpDir(agentDir), "draft-sleep");
30
+ }
31
+ export function getDraftSleepStatePath(agentDir = getAgentDir()) {
32
+ return join(getDraftSleepRoot(agentDir), "state.json");
33
+ }
34
+ export function getDraftSleepBatchDir(batchId, agentDir = getAgentDir()) {
35
+ if (!/^[a-f0-9-]{36}$/u.test(batchId)) {
36
+ throw new Error("Invalid draft sleep batch id.");
37
+ }
38
+ return join(getDraftSleepRoot(agentDir), "batches", batchId);
39
+ }
40
+ export function getToolReviewRoot(agentDir = getAgentDir()) {
41
+ return join(getExtensionTmpDir(agentDir), "tool-review");
42
+ }
43
+ export function getToolReviewStatePath(agentDir = getAgentDir()) {
44
+ return join(getToolReviewRoot(agentDir), "state.json");
45
+ }
46
+ export function getToolReviewBatchDir(reviewId, agentDir = getAgentDir()) {
47
+ if (!/^[a-f0-9-]{36}$/u.test(reviewId)) {
48
+ throw new Error("Invalid tool review id.");
49
+ }
50
+ return join(getToolReviewRoot(agentDir), "batches", reviewId);
51
+ }
24
52
  export function getExtensionRuntimePaths(agentDir = getAgentDir()) {
25
53
  return {
26
54
  configPath: getConfigPath(agentDir),
@@ -528,30 +528,38 @@ function templatePreview(value) {
528
528
  }
529
529
  export function listDraftRecipes(root) {
530
530
  return listRecipeFiles(root).map((path) => {
531
- const id = RecipesReferences.getRecipeIdFromPath(path);
532
- const bytes = readFileSync(path);
533
- const stat = statSync(path);
534
- const config = RecipesReferences.readRawRecipeConfig(path);
535
- const resolved = RecipesReferences.readResolvedRecipeConfig(path);
536
- const diagnostics = getRecipeConfigDiagnostics(path, resolved);
537
- const sourceRun = String(config?.description ?? "").match(/spawn run ([^\s]+)/)?.[1];
538
- const preview = templatePreview(config?.template);
539
- const riskLabels = getRecipeRiskLabels(resolved);
540
- return {
541
- id,
542
- path,
543
- sha256: createHash("sha256").update(bytes).digest("hex"),
544
- size: bytes.byteLength,
545
- created_at: stat.birthtime.toISOString(),
546
- modified_at: stat.mtime.toISOString(),
547
- valid: Boolean(resolved),
548
- diagnostics,
549
- ...(riskLabels.length ? { risk_labels: riskLabels } : {}),
550
- ...(config?.description ? { description: config.description } : {}),
551
- ...(sourceRun ? { source_run: sourceRun } : {}),
552
- ...(config?.async !== undefined ? { async: config.async } : {}),
553
- ...(preview ? { template_preview: preview } : {}),
554
- };
531
+ for (let attempt = 0; attempt < 3; attempt += 1) {
532
+ const id = RecipesReferences.getRecipeIdFromPath(path);
533
+ const bytes = readFileSync(path);
534
+ const fingerprint = createHash("sha256").update(bytes).digest("hex");
535
+ const stat = statSync(path);
536
+ const config = RecipesReferences.readRawRecipeConfig(path);
537
+ const resolved = RecipesReferences.readResolvedRecipeConfig(path);
538
+ const diagnostics = getRecipeConfigDiagnostics(path, resolved);
539
+ const after = readFileSync(path);
540
+ if (createHash("sha256").update(after).digest("hex") !== fingerprint) {
541
+ continue;
542
+ }
543
+ const sourceRun = String(config?.description ?? "").match(/spawn run ([^\s]+)/)?.[1];
544
+ const preview = templatePreview(config?.template);
545
+ const riskLabels = getRecipeRiskLabels(resolved);
546
+ return {
547
+ id,
548
+ path,
549
+ sha256: fingerprint,
550
+ size: bytes.byteLength,
551
+ created_at: stat.birthtime.toISOString(),
552
+ modified_at: stat.mtime.toISOString(),
553
+ valid: Boolean(resolved),
554
+ diagnostics,
555
+ ...(riskLabels.length ? { risk_labels: riskLabels } : {}),
556
+ ...(config?.description ? { description: config.description } : {}),
557
+ ...(sourceRun ? { source_run: sourceRun } : {}),
558
+ ...(config?.async !== undefined ? { async: config.async } : {}),
559
+ ...(preview ? { template_preview: preview } : {}),
560
+ };
561
+ }
562
+ throw new Error(`Draft changed repeatedly during inventory: ${path}`);
555
563
  });
556
564
  }
557
565
  export function summarizeDiscovery(result) {
@@ -1,9 +1,21 @@
1
1
  /**
2
- * Recipe usage metadata helpers
3
- * Zones: recipe telemetry, muscle-memory cleanup evidence
4
- * Owns lightweight launch counters for user-owned recipe files
2
+ * Recipe usage and name-based lineage metadata.
3
+ * Zones: recipe lineage, launch telemetry, revision history, migration
4
+ * Owns priority-compatible recipe names and lifetime/revision counters for user recipes.
5
5
  */
6
6
  export type RecipeLaunchKind = "direct" | "spawn" | "tool";
7
- export declare function getRecipeUsagePath(path: string): string;
8
- export declare function readRecipeUsage(path: string): Record<string, unknown> | undefined;
9
- export declare function recordRecipeLaunch(path: string, now?: Date, kind?: RecipeLaunchKind): boolean;
7
+ export declare function getRecipeUsageIndexPath(recipeRoot: string): string;
8
+ export declare function getRecipeUsageLedgerPath(lineageName: string, recipeRoot: string): string;
9
+ export declare const RECIPE_REVISION_SNAPSHOT_LIMIT = 32;
10
+ export declare function getRecipeRevisionSnapshotPath(lineageName: string, revision: number, recipeRoot: string): string;
11
+ export declare function readRecipeUsage(path: string, recipeRoot?: string): Record<string, unknown> | undefined;
12
+ export declare function ensureRecipeLineage(path: string, now?: Date, recipeRoot?: string): boolean;
13
+ export declare function isCurrentRecipeRevisionReviewed(path: string, recipeRoot?: string): boolean;
14
+ export interface RecipeLaunchRecordOptions {
15
+ onMutationLockContention?(): void;
16
+ }
17
+ export declare function recordRecipeLaunch(path: string, now?: Date, kind?: RecipeLaunchKind, recipeRoot?: string, options?: RecipeLaunchRecordOptions): boolean;
18
+ export declare function recordRecipeReview(path: string, reviewEpoch: string, now?: Date, recipeRoot?: string): boolean;
19
+ export declare function recordRecipeRollback(path: string, rollbackRevision: number, now?: Date, recipeRoot?: string): boolean;
20
+ export declare function retireRecipeUsage(path: string, recipeRoot?: string): boolean;
21
+ export declare function moveRecipeUsage(fromPath: string, toPath: string, recipeRoot?: string): boolean;