@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
package/lib/inspector.ts CHANGED
@@ -74,6 +74,7 @@ export interface ActorInspectorPreviewReadOptions {
74
74
 
75
75
  export interface ActorInspectorRunItem {
76
76
  run: string;
77
+ runInstanceId?: string;
77
78
  status: string;
78
79
  updatedAt?: string;
79
80
  }
@@ -89,6 +90,10 @@ export function readActorInspectorRuns(
89
90
  .flatMap((entry) => {
90
91
  const stateDir = path.join(stateRoot, entry.name);
91
92
  if (!matchesOwner(stateDir, ownerId)) return [];
93
+ const runMeta = readJsonFileResilient<Record<string, unknown>>(
94
+ path.join(stateDir, "run.json"),
95
+ {},
96
+ ).value;
92
97
  const progress = readJsonFileResilient<Record<string, unknown>>(
93
98
  path.join(stateDir, "progress.json"),
94
99
  {},
@@ -99,6 +104,9 @@ export function readActorInspectorRuns(
99
104
  ).value;
100
105
  return [{
101
106
  run: entry.name,
107
+ ...(typeof runMeta.run_instance_id === "string"
108
+ ? { runInstanceId: runMeta.run_instance_id }
109
+ : {}),
102
110
  status: String(progress.phase ?? (Object.keys(result).length ? "terminal" : "unknown")),
103
111
  ...(typeof progress.updatedAt === "string"
104
112
  ? { updatedAt: progress.updatedAt }
@@ -119,6 +127,70 @@ export function readActorInspectorRuns(
119
127
  }
120
128
  }
121
129
 
130
+ export interface ActorInspectorRecipeView {
131
+ composition: Array<Record<string, unknown>>;
132
+ definition?: Record<string, unknown>;
133
+ diagnostics: string[];
134
+ identity: Record<string, unknown>;
135
+ launch: Record<string, unknown>;
136
+ }
137
+
138
+ export function readActorInspectorRecipe(
139
+ stateDir: string,
140
+ ): ActorInspectorRecipeView {
141
+ const result = readJsonFileResilient<Record<string, unknown>>(
142
+ path.join(stateDir, "run.json"),
143
+ {},
144
+ );
145
+ const meta = result.value;
146
+ const contexts = Array.isArray(meta.recipe_context_records)
147
+ ? meta.recipe_context_records
148
+ .filter((entry): entry is Record<string, unknown> =>
149
+ Boolean(entry && typeof entry === "object" && !Array.isArray(entry)),
150
+ )
151
+ : [];
152
+ const primary = contexts.find((entry) => entry.depth === 0) ?? contexts[0];
153
+ const definition = primary?.recipe && typeof primary.recipe === "object" &&
154
+ !Array.isArray(primary.recipe)
155
+ ? primary.recipe as Record<string, unknown>
156
+ : undefined;
157
+ const redacted = (value: unknown): unknown =>
158
+ SessionEvidence.redactSessionEvidenceValue(value);
159
+ const redactedRecord = (value: Record<string, unknown>): Record<string, unknown> =>
160
+ redacted(Object.fromEntries(
161
+ Object.entries(value).filter(([, entry]) => entry !== undefined),
162
+ )) as Record<string, unknown>;
163
+ return {
164
+ composition: contexts.map((entry) => redactedRecord({
165
+ ...(typeof entry.alias === "string" ? { alias: entry.alias } : {}),
166
+ depth: entry.depth,
167
+ file: entry.file,
168
+ import_path: entry.import_path,
169
+ name: entry.name,
170
+ })),
171
+ ...(definition
172
+ ? { definition: redactedRecord(definition) }
173
+ : {}),
174
+ diagnostics: result.diagnostics.map((diagnostic) => diagnostic.message),
175
+ identity: redactedRecord({
176
+ run: meta.run,
177
+ recipe: meta.recipe,
178
+ recipe_file: meta.recipe_file,
179
+ launch_source: meta.launch_source,
180
+ }),
181
+ launch: redactedRecord({
182
+ cwd: meta.cwd,
183
+ template: meta.template,
184
+ values: meta.values,
185
+ model_policy: meta.model_policy,
186
+ mailbox: meta.mailbox,
187
+ artifacts: meta.artifacts,
188
+ notification_policy: meta.notification_policy,
189
+ retire_when: meta.retire_when,
190
+ }),
191
+ };
192
+ }
193
+
122
194
  export interface ActorInspectorTurnItem extends SessionEvidence.SessionEvidenceTurn {
123
195
  commandId: string;
124
196
  diagnostics: string[];
@@ -176,7 +248,7 @@ export function readActorInspectorTurns(stateDir: string): ActorInspectorTurnIte
176
248
  withFileTypes: true,
177
249
  })
178
250
  .filter((file) => file.isFile() && file.name.endsWith(".jsonl"))
179
- .map((file) => path.join("sessions", entry.name, file.name))
251
+ .map((file) => path.posix.join("sessions", entry.name, file.name))
180
252
  .filter((file) => !recordedFiles.has(file));
181
253
  return files.length > 0
182
254
  ? [{ id: entry.name, session_files: files, stage: "subagent" }]
@@ -5,9 +5,13 @@
5
5
  */
6
6
 
7
7
  import {
8
+ closeSync,
8
9
  existsSync,
10
+ fstatSync,
11
+ openSync,
9
12
  readdirSync,
10
13
  readFileSync,
14
+ readSync,
11
15
  watch,
12
16
  type FSWatcher,
13
17
  } from "node:fs";
@@ -41,12 +45,15 @@ export interface RunObservation {
41
45
  failures?: number;
42
46
  ownerId?: string;
43
47
  artifacts?: Record<string, string>;
48
+ launchCorrelation?: Record<string, string>;
44
49
  launchSource?: AsyncRuns.AsyncRunLaunchSource;
45
50
  modelPolicy?: Record<string, unknown>;
51
+ notificationPolicy?: "normal" | "silent";
46
52
  recipeFile?: string;
47
53
  terminalHandled?: boolean;
48
54
  retireWhen?: string;
49
55
  run: string;
56
+ semanticResult?: RunTerminalSemanticResult;
50
57
  tool?: string;
51
58
  stateDir?: string;
52
59
  status: RunObservedStatus;
@@ -161,6 +168,19 @@ export function deliverRunTransitionNotifications(
161
168
  transition.to,
162
169
  );
163
170
  }
171
+ } catch (error) {
172
+ if (transition.stateDir) {
173
+ AsyncRuns.recordRunTerminalDeliveryFailure(
174
+ transition.stateDir,
175
+ transition.to,
176
+ error,
177
+ );
178
+ }
179
+ const message = error instanceof Error ? error.message : String(error);
180
+ sink.notify(
181
+ `Actor terminal delivery failed for run:${transition.run}: ${message.replaceAll(/\s+/g, " ").slice(0, 240)}`,
182
+ "error",
183
+ );
164
184
  } finally {
165
185
  inFlight.delete(key);
166
186
  }
@@ -419,12 +439,23 @@ export interface RunTransition {
419
439
  run: string;
420
440
  stateDir?: string;
421
441
  artifacts?: Record<string, string>;
442
+ launchCorrelation?: Record<string, string>;
422
443
  launchSource?: AsyncRuns.AsyncRunLaunchSource;
423
444
  modelPolicy?: Record<string, unknown>;
424
445
  recipeFile?: string;
425
446
  terminalHandled?: boolean;
426
447
  to: RunObservedStatus;
427
448
  tool?: string;
449
+ semanticResult?: RunTerminalSemanticResult;
450
+ }
451
+
452
+ export interface RunTerminalSemanticResult {
453
+ body?: string;
454
+ correlationId?: string;
455
+ metadata: Record<string, unknown>;
456
+ summary: string;
457
+ synthesized: boolean;
458
+ type: string;
428
459
  }
429
460
 
430
461
  export interface RunOutboxEvent {
@@ -503,15 +534,145 @@ function scanRunStateDirs(
503
534
  return result;
504
535
  }
505
536
 
537
+ const TERMINAL_RESULT_BYTES = 8 * 1024;
538
+ const TERMINAL_RESULT_CHARS = 4_000;
539
+
540
+ function readBoundedStart(path: string): string {
541
+ if (!existsSync(path)) return "";
542
+ const fd = openSync(path, "r");
543
+ try {
544
+ const size = Math.min(fstatSync(fd).size, TERMINAL_RESULT_BYTES);
545
+ const buffer = Buffer.alloc(size);
546
+ const bytes = readSync(fd, buffer, 0, size, 0);
547
+ const text = buffer.subarray(0, bytes).toString("utf8").trim();
548
+ return text.length > TERMINAL_RESULT_CHARS
549
+ ? `${text.slice(0, TERMINAL_RESULT_CHARS - 1)}…`
550
+ : text;
551
+ } finally {
552
+ closeSync(fd);
553
+ }
554
+ }
555
+
556
+ function semanticBodyFromStdout(stateDir: string): string | undefined {
557
+ const text = readBoundedStart(join(stateDir, "stdout.log"));
558
+ if (!text) return undefined;
559
+ const withoutMarker = text.replace(/^ACTOR_REVIEW_RESULT\s*(?:\r?\n)?/, "").trim();
560
+ return withoutMarker || undefined;
561
+ }
562
+
563
+ function terminalSemanticResult(
564
+ status: Record<string, unknown>,
565
+ stateDir: string,
566
+ observedStatus: RunObservedStatus,
567
+ ): RunTerminalSemanticResult {
568
+ const correlation = status.launch_correlation &&
569
+ typeof status.launch_correlation === "object" &&
570
+ !Array.isArray(status.launch_correlation)
571
+ ? status.launch_correlation as Record<string, unknown>
572
+ : {};
573
+ const correlationId =
574
+ typeof correlation.correlation_id === "string"
575
+ ? correlation.correlation_id
576
+ : typeof correlation.tool_call_id === "string"
577
+ ? correlation.tool_call_id
578
+ : undefined;
579
+ const mailbox = status.mailbox &&
580
+ typeof status.mailbox === "object" &&
581
+ !Array.isArray(status.mailbox)
582
+ ? status.mailbox as Record<string, unknown>
583
+ : {};
584
+ const emits = Array.isArray(mailbox.emits)
585
+ ? mailbox.emits.filter((item): item is string => typeof item === "string")
586
+ : [];
587
+ const outbox = readJsonlFileResilient<Record<string, unknown>>(
588
+ join(stateDir, "outbox.jsonl"),
589
+ ).records;
590
+ const explicit = outbox.findLast((record) => {
591
+ const type = String(record.type ?? record.event ?? "");
592
+ return emits.includes(type) && !["command.done", "run.done", "run.failed"].includes(type);
593
+ });
594
+ if (explicit) {
595
+ const type = String(explicit.type ?? explicit.event);
596
+ return {
597
+ ...(explicit.body === undefined
598
+ ? {} : { body: formatSemanticBody(explicit.body) }),
599
+ ...(typeof explicit.correlation_id === "string"
600
+ ? { correlationId: explicit.correlation_id } : correlationId ? { correlationId } : {}),
601
+ metadata: {
602
+ ...(explicit.metadata &&
603
+ typeof explicit.metadata === "object" &&
604
+ !Array.isArray(explicit.metadata)
605
+ ? explicit.metadata as Record<string, unknown> : {}),
606
+ ...(status.transport_context &&
607
+ typeof status.transport_context === "object" &&
608
+ !Array.isArray(status.transport_context)
609
+ ? {
610
+ transport_context: status.transport_context as Record<string, unknown>,
611
+ } : {}),
612
+ run: String(status.run ?? ""),
613
+ status: observedStatus,
614
+ },
615
+ summary: String(explicit.summary ?? type),
616
+ synthesized: false,
617
+ type,
618
+ };
619
+ }
620
+ const reviewCompleted = observedStatus === "done" && emits.includes("review.completed");
621
+ const result = status.result &&
622
+ typeof status.result === "object" &&
623
+ !Array.isArray(status.result)
624
+ ? status.result as Record<string, unknown>
625
+ : {};
626
+ const type = reviewCompleted
627
+ ? "review.completed"
628
+ : observedStatus === "done" ? "run.done" : "run.failed";
629
+ const stdoutBody = observedStatus === "done"
630
+ ? semanticBodyFromStdout(stateDir) : undefined;
631
+ return {
632
+ ...(stdoutBody
633
+ ? { body: stdoutBody }
634
+ : typeof result.error === "string" ? { body: result.error } : {}),
635
+ ...(correlationId ? { correlationId } : {}),
636
+ metadata: {
637
+ ...(status.transport_context &&
638
+ typeof status.transport_context === "object" &&
639
+ !Array.isArray(status.transport_context)
640
+ ? {
641
+ transport_context: status.transport_context as Record<string, unknown>,
642
+ } : {}),
643
+ run: String(status.run ?? ""), status: observedStatus,
644
+ },
645
+ summary: reviewCompleted ? "Review completed." :
646
+ observedStatus === "done" ? "Run completed." : `Run ${observedStatus}.`,
647
+ synthesized: true,
648
+ type,
649
+ };
650
+ }
651
+
652
+ function formatSemanticBody(body: unknown): string {
653
+ const rendered = typeof body === "string" ? body : JSON.stringify(body);
654
+ const text = typeof rendered === "string" ? rendered : String(body);
655
+ const compact = text.trim();
656
+ return compact.length > TERMINAL_RESULT_CHARS
657
+ ? `${compact.slice(0, TERMINAL_RESULT_CHARS - 1)}…`
658
+ : compact;
659
+ }
660
+
506
661
  function observeRun(stateDir: string): RunObservation | undefined {
507
662
  try {
508
663
  const status = AsyncRuns.getRunStatus(stateDir);
509
664
  const progress = getProgress(status);
510
665
  const run = typeof status.run === "string" ? status.run : undefined;
511
666
  if (!run) return undefined;
667
+ const observedStatus = status.status as RunObservedStatus;
512
668
  return {
513
669
  activeSubagents: toNumber(progress.activeSubagents),
514
670
  completed: toNumber(progress.completed),
671
+ ...(status.launch_correlation &&
672
+ typeof status.launch_correlation === "object" &&
673
+ !Array.isArray(status.launch_correlation)
674
+ ? { launchCorrelation: status.launch_correlation as Record<string, string> }
675
+ : {}),
515
676
  failures: Array.isArray(progress.failures)
516
677
  ? progress.failures.length
517
678
  : undefined,
@@ -531,6 +692,9 @@ function observeRun(stateDir: string): RunObservation | undefined {
531
692
  !Array.isArray(status.model_policy)
532
693
  ? { modelPolicy: status.model_policy as Record<string, unknown> }
533
694
  : {}),
695
+ ...(status.notification_policy === "silent"
696
+ ? { notificationPolicy: "silent" as const }
697
+ : {}),
534
698
  ...(typeof status.recipe_file === "string"
535
699
  ? { recipeFile: status.recipe_file }
536
700
  : {}),
@@ -538,9 +702,12 @@ function observeRun(stateDir: string): RunObservation | undefined {
538
702
  ...(typeof status.retire_when === "string"
539
703
  ? { retireWhen: status.retire_when }
540
704
  : {}),
705
+ ...(TERMINAL.has(observedStatus)
706
+ ? { semanticResult: terminalSemanticResult(status, stateDir, observedStatus) }
707
+ : {}),
541
708
  run,
542
709
  stateDir,
543
- status: status.status as RunObservedStatus,
710
+ status: observedStatus,
544
711
  ...(typeof status.tool === "string" ? { tool: status.tool } : {}),
545
712
  updatedAt: getUpdatedAt(status),
546
713
  };
@@ -919,15 +1086,21 @@ export function detectRunTransitions(
919
1086
  for (const run of summary.runs) {
920
1087
  const key = runObservationKey(run);
921
1088
  const old = previous.get(key);
922
- if (!run.terminalHandled && TERMINAL.has(run.status)) {
1089
+ if (
1090
+ run.notificationPolicy !== "silent" &&
1091
+ !run.terminalHandled &&
1092
+ TERMINAL.has(run.status)
1093
+ ) {
923
1094
  transitions.push({
924
1095
  from: old ?? "running",
925
1096
  run: run.run,
926
1097
  ...(run.stateDir ? { stateDir: run.stateDir } : {}),
927
1098
  ...(run.artifacts ? { artifacts: run.artifacts } : {}),
1099
+ ...(run.launchCorrelation ? { launchCorrelation: run.launchCorrelation } : {}),
928
1100
  ...(run.launchSource ? { launchSource: run.launchSource } : {}),
929
1101
  ...(run.modelPolicy ? { modelPolicy: run.modelPolicy } : {}),
930
1102
  ...(run.recipeFile ? { recipeFile: run.recipeFile } : {}),
1103
+ ...(run.semanticResult ? { semanticResult: run.semanticResult } : {}),
931
1104
  ...(run.terminalHandled ? { terminalHandled: true } : {}),
932
1105
  to: run.status,
933
1106
  ...(run.tool ? { tool: run.tool } : {}),
@@ -1037,6 +1210,11 @@ export function detectRunOutboxEvents(
1037
1210
  const key = run.stateDir ?? run.run;
1038
1211
  const records = readOutboxRecords(run);
1039
1212
  const previousCount = previousLineCounts.get(key) ?? 0;
1213
+ if (run.notificationPolicy === "silent") {
1214
+ previousLineCounts.set(key, records.length);
1215
+ seenEventIds.set(key, seenEventIds.get(key) ?? new Set<string>());
1216
+ continue;
1217
+ }
1040
1218
  const start = Math.min(previousCount, records.length);
1041
1219
  const seen = seenEventIds.get(key) ?? new Set<string>();
1042
1220
  for (let index = start; index < records.length; index += 1) {
@@ -1182,7 +1360,8 @@ function isUserRecipeFile(file: string | undefined): boolean {
1182
1360
  if (!file) return false;
1183
1361
  const recipeRoot = resolve(Paths.getRecipeRoot());
1184
1362
  const path = resolve(file);
1185
- return path === recipeRoot || path.startsWith(`${recipeRoot}/`);
1363
+ const relation = relative(recipeRoot, path);
1364
+ return relation === "" || (!relation.startsWith("..") && !isAbsolute(relation));
1186
1365
  }
1187
1366
 
1188
1367
  export function shouldSuggestRecipePersistence(
@@ -1232,16 +1411,26 @@ function formatTransitionNextActions(transition: RunTransition): string {
1232
1411
  return `\nNext actions: ${actions.join(" | ")}`;
1233
1412
  }
1234
1413
 
1414
+ function formatTransitionSemanticResult(transition: RunTransition): string {
1415
+ const result = transition.semanticResult;
1416
+ if (!result) return "";
1417
+ const correlation = result.correlationId
1418
+ ? `\nCorrelation: ${result.correlationId}` : "";
1419
+ const body = result.body ? `\nResult:\n${result.body}` : "";
1420
+ return `\nSemantic result: ${result.type} — ${result.summary}${correlation}${body}`;
1421
+ }
1422
+
1235
1423
  export function formatRunTransitionMessage(transition: RunTransition): string {
1236
1424
  const artifacts = formatNamedArtifacts(transition.artifacts);
1237
1425
  const runFiles = formatRunFileList(getRunArtifacts(transition));
1238
1426
  const persistenceSuggestion = formatRecipePersistenceSuggestion(transition);
1239
1427
  const policy = formatTransitionPolicy(transition);
1240
1428
  const nextActions = formatTransitionNextActions(transition);
1429
+ const semanticResult = formatTransitionSemanticResult(transition);
1241
1430
  if (transition.to === "done")
1242
- return `Run ${transition.run} completed successfully.${policy}${artifacts}${runFiles}${nextActions}${persistenceSuggestion}`;
1431
+ return `Run ${transition.run} completed successfully.${semanticResult}${policy}${artifacts}${runFiles}${nextActions}${persistenceSuggestion}`;
1243
1432
  if (transition.to === "failed")
1244
- return `Run ${transition.run} failed.${policy}${artifacts}${runFiles}${nextActions}`;
1433
+ return `Run ${transition.run} failed.${semanticResult}${policy}${artifacts}${runFiles}${nextActions}`;
1245
1434
  if (transition.to === "cancelled")
1246
1435
  return `Run ${transition.run} was cancelled.${policy}${nextActions}`;
1247
1436
  if (transition.to === "killed")
package/lib/paths.ts CHANGED
@@ -17,6 +17,13 @@ export function getAgentDir(
17
17
  : join(homedir(), ".pi", "agent");
18
18
  }
19
19
 
20
+ export function isAutomaticRecipeReviewEnabled(
21
+ env: Record<string, string | undefined> = process.env,
22
+ ): boolean {
23
+ const value = env.PI_ACTORS_AUTOMATIC_REVIEW?.trim().toLowerCase();
24
+ return value === undefined || !["0", "false", "off"].includes(value);
25
+ }
26
+
20
27
  export interface ExtensionRuntimePaths {
21
28
  configPath: string;
22
29
  runStateRoot: string;
@@ -38,6 +45,42 @@ export function getRunStateRoot(agentDir = getAgentDir()): string {
38
45
  return join(getExtensionTmpDir(agentDir), "runs");
39
46
  }
40
47
 
48
+ export function getDraftSleepRoot(agentDir = getAgentDir()): string {
49
+ return join(getExtensionTmpDir(agentDir), "draft-sleep");
50
+ }
51
+
52
+ export function getDraftSleepStatePath(agentDir = getAgentDir()): string {
53
+ return join(getDraftSleepRoot(agentDir), "state.json");
54
+ }
55
+
56
+ export function getDraftSleepBatchDir(
57
+ batchId: string,
58
+ agentDir = getAgentDir(),
59
+ ): string {
60
+ if (!/^[a-f0-9-]{36}$/u.test(batchId)) {
61
+ throw new Error("Invalid draft sleep batch id.");
62
+ }
63
+ return join(getDraftSleepRoot(agentDir), "batches", batchId);
64
+ }
65
+
66
+ export function getToolReviewRoot(agentDir = getAgentDir()): string {
67
+ return join(getExtensionTmpDir(agentDir), "tool-review");
68
+ }
69
+
70
+ export function getToolReviewStatePath(agentDir = getAgentDir()): string {
71
+ return join(getToolReviewRoot(agentDir), "state.json");
72
+ }
73
+
74
+ export function getToolReviewBatchDir(
75
+ reviewId: string,
76
+ agentDir = getAgentDir(),
77
+ ): string {
78
+ if (!/^[a-f0-9-]{36}$/u.test(reviewId)) {
79
+ throw new Error("Invalid tool review id.");
80
+ }
81
+ return join(getToolReviewRoot(agentDir), "batches", reviewId);
82
+ }
83
+
41
84
  export function getExtensionRuntimePaths(
42
85
  agentDir = getAgentDir(),
43
86
  ): ExtensionRuntimePaths {
@@ -703,32 +703,40 @@ function templatePreview(value: unknown): string | undefined {
703
703
 
704
704
  export function listDraftRecipes(root: string): Array<Record<string, unknown>> {
705
705
  return listRecipeFiles(root).map((path) => {
706
- const id = RecipesReferences.getRecipeIdFromPath(path);
707
- const bytes = readFileSync(path);
708
- const stat = statSync(path);
709
- const config = RecipesReferences.readRawRecipeConfig(path);
710
- const resolved = RecipesReferences.readResolvedRecipeConfig(path);
711
- const diagnostics = getRecipeConfigDiagnostics(path, resolved);
712
- const sourceRun = String(config?.description ?? "").match(
713
- /spawn run ([^\s]+)/,
714
- )?.[1];
715
- const preview = templatePreview(config?.template);
716
- const riskLabels = getRecipeRiskLabels(resolved);
717
- return {
718
- id,
719
- path,
720
- sha256: createHash("sha256").update(bytes).digest("hex"),
721
- size: bytes.byteLength,
722
- created_at: stat.birthtime.toISOString(),
723
- modified_at: stat.mtime.toISOString(),
724
- valid: Boolean(resolved),
725
- diagnostics,
726
- ...(riskLabels.length ? { risk_labels: riskLabels } : {}),
727
- ...(config?.description ? { description: config.description } : {}),
728
- ...(sourceRun ? { source_run: sourceRun } : {}),
729
- ...(config?.async !== undefined ? { async: config.async } : {}),
730
- ...(preview ? { template_preview: preview } : {}),
731
- };
706
+ for (let attempt = 0; attempt < 3; attempt += 1) {
707
+ const id = RecipesReferences.getRecipeIdFromPath(path);
708
+ const bytes = readFileSync(path);
709
+ const fingerprint = createHash("sha256").update(bytes).digest("hex");
710
+ const stat = statSync(path);
711
+ const config = RecipesReferences.readRawRecipeConfig(path);
712
+ const resolved = RecipesReferences.readResolvedRecipeConfig(path);
713
+ const diagnostics = getRecipeConfigDiagnostics(path, resolved);
714
+ const after = readFileSync(path);
715
+ if (createHash("sha256").update(after).digest("hex") !== fingerprint) {
716
+ continue;
717
+ }
718
+ const sourceRun = String(config?.description ?? "").match(
719
+ /spawn run ([^\s]+)/,
720
+ )?.[1];
721
+ const preview = templatePreview(config?.template);
722
+ const riskLabels = getRecipeRiskLabels(resolved);
723
+ return {
724
+ id,
725
+ path,
726
+ sha256: fingerprint,
727
+ size: bytes.byteLength,
728
+ created_at: stat.birthtime.toISOString(),
729
+ modified_at: stat.mtime.toISOString(),
730
+ valid: Boolean(resolved),
731
+ diagnostics,
732
+ ...(riskLabels.length ? { risk_labels: riskLabels } : {}),
733
+ ...(config?.description ? { description: config.description } : {}),
734
+ ...(sourceRun ? { source_run: sourceRun } : {}),
735
+ ...(config?.async !== undefined ? { async: config.async } : {}),
736
+ ...(preview ? { template_preview: preview } : {}),
737
+ };
738
+ }
739
+ throw new Error(`Draft changed repeatedly during inventory: ${path}`);
732
740
  });
733
741
  }
734
742