@llblab/pi-actors 0.41.0 → 0.42.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 (150) hide show
  1. package/AGENTS.md +13 -8
  2. package/BACKLOG.md +2 -81
  3. package/CHANGELOG.md +22 -0
  4. package/README.md +22 -4
  5. package/dist/index.js +29 -82
  6. package/dist/lib/async-runs.d.ts +25 -5
  7. package/dist/lib/async-runs.js +136 -47
  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/draft-consolidation-transaction.d.ts +65 -0
  11. package/dist/lib/draft-consolidation-transaction.js +610 -0
  12. package/dist/lib/draft-consolidation.d.ts +35 -0
  13. package/dist/lib/draft-consolidation.js +126 -0
  14. package/dist/lib/draft-review.d.ts +56 -0
  15. package/dist/lib/draft-review.js +254 -0
  16. package/dist/lib/draft-sleep.d.ts +65 -0
  17. package/dist/lib/draft-sleep.js +468 -0
  18. package/dist/lib/file-state.d.ts +8 -1
  19. package/dist/lib/file-state.js +115 -18
  20. package/dist/lib/inspector-actions.d.ts +16 -0
  21. package/dist/lib/inspector-actions.js +57 -0
  22. package/dist/lib/inspector-command.d.ts +7 -0
  23. package/dist/lib/inspector-command.js +37 -0
  24. package/dist/lib/inspector-overlay.d.ts +29 -1
  25. package/dist/lib/inspector-overlay.js +537 -90
  26. package/dist/lib/inspector.d.ts +9 -0
  27. package/dist/lib/inspector.js +52 -1
  28. package/dist/lib/observability.d.ts +39 -3
  29. package/dist/lib/observability.js +162 -34
  30. package/dist/lib/paths.d.ts +7 -0
  31. package/dist/lib/paths.js +28 -0
  32. package/dist/lib/recipes-discovery.js +32 -24
  33. package/dist/lib/recipes-usage.d.ts +18 -6
  34. package/dist/lib/recipes-usage.js +445 -34
  35. package/dist/lib/review-control.d.ts +14 -0
  36. package/dist/lib/review-control.js +111 -0
  37. package/dist/lib/review-diagnostics.d.ts +11 -0
  38. package/dist/lib/review-diagnostics.js +148 -0
  39. package/dist/lib/review-projection.d.ts +14 -0
  40. package/dist/lib/review-projection.js +170 -0
  41. package/dist/lib/run-ui-runtime.d.ts +18 -0
  42. package/dist/lib/run-ui-runtime.js +123 -0
  43. package/dist/lib/runs-artifacts.d.ts +1 -1
  44. package/dist/lib/runs-artifacts.js +1 -1
  45. package/dist/lib/runs-control.d.ts +8 -2
  46. package/dist/lib/runs-control.js +23 -6
  47. package/dist/lib/runs-identity.d.ts +1 -1
  48. package/dist/lib/runs-identity.js +1 -1
  49. package/dist/lib/runs-index.d.ts +11 -2
  50. package/dist/lib/runs-index.js +46 -23
  51. package/dist/lib/runs-mailbox.d.ts +1 -1
  52. package/dist/lib/runs-mailbox.js +1 -1
  53. package/dist/lib/runs-messages.d.ts +1 -1
  54. package/dist/lib/runs-messages.js +1 -1
  55. package/dist/lib/runs-outbox.d.ts +1 -1
  56. package/dist/lib/runs-outbox.js +1 -1
  57. package/dist/lib/runs-ownership.d.ts +1 -1
  58. package/dist/lib/runs-ownership.js +1 -1
  59. package/dist/lib/runs-parent-teardown.d.ts +51 -0
  60. package/dist/lib/runs-parent-teardown.js +172 -0
  61. package/dist/lib/runs-process.d.ts +1 -1
  62. package/dist/lib/runs-process.js +1 -1
  63. package/dist/lib/runs-retention.d.ts +1 -1
  64. package/dist/lib/runs-retention.js +1 -1
  65. package/dist/lib/runs-start.d.ts +5 -3
  66. package/dist/lib/runs-start.js +6 -48
  67. package/dist/lib/runs-status.d.ts +5 -3
  68. package/dist/lib/runs-status.js +4 -6
  69. package/dist/lib/runtime.d.ts +7 -1
  70. package/dist/lib/runtime.js +32 -18
  71. package/dist/lib/tool-review-lineage-transaction.d.ts +27 -0
  72. package/dist/lib/tool-review-lineage-transaction.js +597 -0
  73. package/dist/lib/tool-review-lineage.d.ts +24 -0
  74. package/dist/lib/tool-review-lineage.js +98 -0
  75. package/dist/lib/tool-review-scheduler.d.ts +80 -0
  76. package/dist/lib/tool-review-scheduler.js +494 -0
  77. package/dist/lib/tool-review-transaction.d.ts +50 -0
  78. package/dist/lib/tool-review-transaction.js +362 -0
  79. package/dist/lib/tool-review.d.ts +56 -0
  80. package/dist/lib/tool-review.js +197 -0
  81. package/dist/lib/tools-inspect.js +26 -3
  82. package/dist/lib/tools-local.js +4 -2
  83. package/dist/lib/tools-message.d.ts +1 -0
  84. package/dist/lib/tools-message.js +29 -17
  85. package/dist/lib/tools-response.d.ts +1 -1
  86. package/dist/lib/tools-response.js +5 -7
  87. package/dist/lib/tools-spawn.js +4 -1
  88. package/dist/lib/tools.d.ts +1 -0
  89. package/dist/lib/tools.js +1 -0
  90. package/dist/recipes/draft-review.json +24 -0
  91. package/dist/recipes/tool-review.json +24 -0
  92. package/dist/scripts/release-gates.mjs +165 -0
  93. package/dist/skills/actors/SKILL.md +11 -14
  94. package/dist/skills/swarm/SKILL.md +1 -1
  95. package/docs/actor-inspector.md +32 -18
  96. package/docs/async-runs.md +10 -2
  97. package/docs/recipe-library.md +7 -3
  98. package/docs/template-recipes.md +4 -11
  99. package/docs/tool-registry.md +11 -4
  100. package/index.ts +29 -103
  101. package/lib/async-runs.ts +218 -62
  102. package/lib/automatic-review-runtime.ts +135 -0
  103. package/lib/draft-consolidation-transaction.ts +821 -0
  104. package/lib/draft-consolidation.ts +181 -0
  105. package/lib/draft-review.ts +325 -0
  106. package/lib/draft-sleep.ts +576 -0
  107. package/lib/file-state.ts +143 -19
  108. package/lib/inspector-actions.ts +79 -0
  109. package/lib/inspector-command.ts +54 -0
  110. package/lib/inspector-overlay.ts +675 -105
  111. package/lib/inspector.ts +78 -3
  112. package/lib/observability.ts +219 -40
  113. package/lib/paths.ts +43 -0
  114. package/lib/recipes-discovery.ts +34 -26
  115. package/lib/recipes-usage.ts +569 -40
  116. package/lib/review-control.ts +137 -0
  117. package/lib/review-diagnostics.ts +164 -0
  118. package/lib/review-projection.ts +200 -0
  119. package/lib/run-ui-runtime.ts +153 -0
  120. package/lib/runs-artifacts.ts +1 -1
  121. package/lib/runs-control.ts +49 -5
  122. package/lib/runs-identity.ts +1 -1
  123. package/lib/runs-index.ts +57 -21
  124. package/lib/runs-mailbox.ts +1 -1
  125. package/lib/runs-messages.ts +1 -1
  126. package/lib/runs-outbox.ts +1 -1
  127. package/lib/runs-ownership.ts +1 -1
  128. package/lib/runs-parent-teardown.ts +257 -0
  129. package/lib/runs-process.ts +1 -1
  130. package/lib/runs-retention.ts +1 -1
  131. package/lib/runs-start.ts +12 -68
  132. package/lib/runs-status.ts +12 -8
  133. package/lib/runtime.ts +34 -17
  134. package/lib/tool-review-lineage-transaction.ts +881 -0
  135. package/lib/tool-review-lineage.ts +145 -0
  136. package/lib/tool-review-scheduler.ts +635 -0
  137. package/lib/tool-review-transaction.ts +563 -0
  138. package/lib/tool-review.ts +270 -0
  139. package/lib/tools-inspect.ts +33 -3
  140. package/lib/tools-local.ts +8 -2
  141. package/lib/tools-message.ts +45 -30
  142. package/lib/tools-response.ts +5 -6
  143. package/lib/tools-spawn.ts +8 -1
  144. package/lib/tools.ts +5 -0
  145. package/package.json +3 -2
  146. package/recipes/draft-review.json +24 -0
  147. package/recipes/tool-review.json +24 -0
  148. package/scripts/release-gates.mjs +165 -0
  149. package/skills/actors/SKILL.md +11 -14
  150. package/skills/swarm/SKILL.md +1 -1
@@ -55,10 +55,19 @@ export interface ActorInspectorPreviewReadOptions {
55
55
  }
56
56
  export interface ActorInspectorRunItem {
57
57
  run: string;
58
+ runInstanceId?: string;
58
59
  status: string;
59
60
  updatedAt?: string;
60
61
  }
61
62
  export declare function readActorInspectorRuns(stateRoot: string, ownerId: string): ActorInspectorRunItem[];
63
+ export interface ActorInspectorRecipeView {
64
+ composition: Array<Record<string, unknown>>;
65
+ definition?: Record<string, unknown>;
66
+ diagnostics: string[];
67
+ identity: Record<string, unknown>;
68
+ launch: Record<string, unknown>;
69
+ }
70
+ export declare function readActorInspectorRecipe(stateDir: string): ActorInspectorRecipeView;
62
71
  export interface ActorInspectorTurnItem extends SessionEvidence.SessionEvidenceTurn {
63
72
  commandId: string;
64
73
  diagnostics: string[];
@@ -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 }
@@ -31,7 +35,10 @@ export function readActorInspectorRuns(stateRoot, ownerId) {
31
35
  : {}),
32
36
  }];
33
37
  })
34
- .sort((left, right) => String(left.updatedAt ?? "").localeCompare(String(right.updatedAt ?? "")));
38
+ .sort((left, right) => {
39
+ const recency = String(right.updatedAt ?? "").localeCompare(String(left.updatedAt ?? ""));
40
+ return recency || right.run.localeCompare(left.run);
41
+ });
35
42
  }
36
43
  catch (error) {
37
44
  if (error.code === "ENOENT")
@@ -39,6 +46,50 @@ export function readActorInspectorRuns(stateRoot, ownerId) {
39
46
  return [];
40
47
  }
41
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
+ }
42
93
  function ownedSessionPath(stateDir, sessionFile) {
43
94
  if (path.isAbsolute(sessionFile))
44
95
  return undefined;
@@ -3,6 +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 { type FSWatcher } from "node:fs";
6
7
  import * as AsyncRuns from "./async-runs.ts";
7
8
  export type RunObservedStatus = "running" | "done" | "failed" | "exited" | "cancelled" | "killed";
8
9
  export type RunOutboxDelivery = "log" | "notify" | "followup";
@@ -16,6 +17,7 @@ export interface RunObservation {
16
17
  artifacts?: Record<string, string>;
17
18
  launchSource?: AsyncRuns.AsyncRunLaunchSource;
18
19
  modelPolicy?: Record<string, unknown>;
20
+ notificationPolicy?: "normal" | "silent";
19
21
  recipeFile?: string;
20
22
  terminalHandled?: boolean;
21
23
  retireWhen?: string;
@@ -58,9 +60,19 @@ export interface RunUiNotificationSink {
58
60
  }): void;
59
61
  }
60
62
  export declare function createRunUiObservationState(): RunUiObservationState;
61
- export declare function readRunUiSnapshot(state: RunUiObservationState, ownerId: string): RunUiSnapshot;
63
+ export declare function readRunUiSnapshot(state: RunUiObservationState, ownerId: string, options?: {
64
+ includeOutbox?: boolean;
65
+ stateRoot?: string;
66
+ }): RunUiSnapshot;
62
67
  export declare function pruneRunUiObservationState(state: RunUiObservationState, snapshot: Pick<RunUiSnapshot, "summary" | "transitions">): void;
63
- export declare function deliverRunTransitionNotifications(transitions: RunTransition[], sink: RunUiNotificationSink): void;
68
+ export declare function deliverRunTransitionNotifications(transitions: RunTransition[], sink: RunUiNotificationSink, inFlight?: Set<string>): void;
69
+ export declare function reconcileRunTerminalNotifications(input: {
70
+ inFlight?: Set<string>;
71
+ ownerId: string;
72
+ sink: RunUiNotificationSink;
73
+ state: RunUiObservationState;
74
+ stateRoot?: string;
75
+ }): RunUiSnapshot;
64
76
  export declare function deliverRunOutboxNotifications(events: RunOutboxEvent[], sink: RunUiNotificationSink): void;
65
77
  export interface RunRetirementCandidate {
66
78
  activeSubagents: number;
@@ -76,14 +88,38 @@ export interface RunRetirementExecution {
76
88
  run: string;
77
89
  stateDir: string;
78
90
  }
91
+ export type RunStateWatcherDiagnosticCode = "attach_failed" | "error" | "removed" | "rearmed";
92
+ export interface RunStateWatcherDiagnostic {
93
+ code: RunStateWatcherDiagnosticCode;
94
+ id: number;
95
+ message: string;
96
+ path: string;
97
+ scope: "root" | "run";
98
+ ts: string;
99
+ }
79
100
  export interface RunStateWatcher {
80
101
  close(): void;
102
+ getDiagnostics(): RunStateWatcherDiagnostic[];
81
103
  refresh(): void;
82
104
  }
83
105
  export declare function createRunStateWatcher(input: {
84
- stateRoot?: string;
106
+ exists?: (path: string) => boolean;
107
+ listDirectories?: (path: string) => string[];
85
108
  onChange: () => void;
109
+ stateRoot?: string;
110
+ watchPath?: (path: string, onChange: () => void) => FSWatcher;
86
111
  }): RunStateWatcher;
112
+ export interface RunTerminalReconciliationLoop {
113
+ close(): void;
114
+ reconcileNow(): void;
115
+ start(): void;
116
+ }
117
+ export declare function createRunTerminalReconciliationLoop(input: {
118
+ intervalMs?: number;
119
+ onError?: (error: unknown) => void;
120
+ reconcile: () => void;
121
+ refreshWatcher: () => void;
122
+ }): RunTerminalReconciliationLoop;
87
123
  export interface RunRetirementExecutorOptions {
88
124
  attempted?: Set<string>;
89
125
  cancelRun: (candidate: RunRetirementCandidate) => Record<string, unknown>;
@@ -16,11 +16,13 @@ export function createRunUiObservationState() {
16
16
  outboxEventIds: new Map(),
17
17
  };
18
18
  }
19
- export function readRunUiSnapshot(state, ownerId) {
20
- const summary = summarizeRuns(undefined, ownerId);
19
+ export function readRunUiSnapshot(state, ownerId, options = {}) {
20
+ const summary = summarizeRuns(options.stateRoot, ownerId);
21
21
  const status = renderRunStatus(summary, state.frame++);
22
22
  return {
23
- outboxEvents: detectRunOutboxEvents(state.eventLines, summary, state.outboxEventIds),
23
+ outboxEvents: options.includeOutbox === false
24
+ ? []
25
+ : detectRunOutboxEvents(state.eventLines, summary, state.outboxEventIds),
24
26
  status,
25
27
  summary,
26
28
  transitions: detectRunTransitions(state.observed, summary),
@@ -29,25 +31,43 @@ export function readRunUiSnapshot(state, ownerId) {
29
31
  export function pruneRunUiObservationState(state, snapshot) {
30
32
  pruneRunObservationState(state.observed, state.eventLines, snapshot.summary, snapshot.transitions.map((transition) => transition.stateDir ?? transition.run), state.outboxEventIds);
31
33
  }
32
- export function deliverRunTransitionNotifications(transitions, sink) {
34
+ export function deliverRunTransitionNotifications(transitions, sink, inFlight = new Set()) {
33
35
  for (const transition of transitions) {
34
36
  if (!shouldNotifyRunTransition(transition))
35
37
  continue;
36
- const text = formatRunTransitionMessage(transition);
37
- sink.notify(text, getRunTransitionNotificationType(transition));
38
- if (!shouldSendRunTransitionFollowUp(transition))
38
+ const key = transition.stateDir ?? transition.run;
39
+ if (inFlight.has(key))
39
40
  continue;
40
- sink.sendFollowUp({
41
- customType: "pi-actors-run",
42
- content: text,
43
- display: true,
44
- details: transition,
45
- });
46
- if (transition.stateDir) {
47
- AsyncRuns.markRunTerminalNotificationHandled(transition.stateDir, transition.to);
41
+ inFlight.add(key);
42
+ try {
43
+ const text = formatRunTransitionMessage(transition);
44
+ sink.notify(text, getRunTransitionNotificationType(transition));
45
+ if (!shouldSendRunTransitionFollowUp(transition))
46
+ continue;
47
+ sink.sendFollowUp({
48
+ customType: "pi-actors-run",
49
+ content: text,
50
+ display: true,
51
+ details: transition,
52
+ });
53
+ if (transition.stateDir) {
54
+ AsyncRuns.markRunTerminalNotificationHandled(transition.stateDir, transition.to);
55
+ }
56
+ }
57
+ finally {
58
+ inFlight.delete(key);
48
59
  }
49
60
  }
50
61
  }
62
+ export function reconcileRunTerminalNotifications(input) {
63
+ const snapshot = readRunUiSnapshot(input.state, input.ownerId, {
64
+ includeOutbox: false,
65
+ stateRoot: input.stateRoot,
66
+ });
67
+ deliverRunTransitionNotifications(snapshot.transitions, input.sink, input.inFlight);
68
+ pruneRunUiObservationState(input.state, snapshot);
69
+ return snapshot;
70
+ }
51
71
  export function deliverRunOutboxNotifications(events, sink) {
52
72
  for (const event of events) {
53
73
  if (!shouldNotifyRunOutboxEvent(event))
@@ -64,10 +84,58 @@ export function deliverRunOutboxNotifications(events, sink) {
64
84
  });
65
85
  }
66
86
  }
87
+ const RUN_WATCHER_DIAGNOSTIC_LIMIT = 32;
67
88
  export function createRunStateWatcher(input) {
68
89
  const stateRoot = input.stateRoot ?? Paths.getRunStateRoot();
90
+ const pathExists = input.exists ?? existsSync;
91
+ const listDirectories = input.listDirectories ??
92
+ ((path) => readdirSync(path, { withFileTypes: true })
93
+ .filter((entry) => entry.isDirectory())
94
+ .map((entry) => join(path, entry.name)));
95
+ const watchPath = input.watchPath ?? ((path, onChange) => watch(path, onChange));
96
+ let diagnosticId = 0;
97
+ let rootDegraded = false;
69
98
  let stateRootWatcher;
99
+ const degradedRunDirs = new Set();
100
+ const diagnostics = [];
101
+ const lastDiagnosticSignatures = new Map();
70
102
  const runDirWatchers = new Map();
103
+ const record = (code, scope, path, error) => {
104
+ const detail = error instanceof Error ? `: ${error.message}` : "";
105
+ const signature = `${code}${detail}`;
106
+ const signatureKey = `${scope}:${path}`;
107
+ if (lastDiagnosticSignatures.get(signatureKey) === signature)
108
+ return;
109
+ lastDiagnosticSignatures.set(signatureKey, signature);
110
+ const recovery = code === "rearmed"
111
+ ? "; terminal watch acceleration restored"
112
+ : "; terminal reconciliation remains active and watcher rearm will retry";
113
+ diagnostics.push({
114
+ code,
115
+ id: ++diagnosticId,
116
+ message: `Run-state ${scope} watcher ${code.replace("_", " ")} for ${path}${detail}${recovery}`,
117
+ path,
118
+ scope,
119
+ ts: new Date().toISOString(),
120
+ });
121
+ if (diagnostics.length > RUN_WATCHER_DIAGNOSTIC_LIMIT)
122
+ diagnostics.shift();
123
+ };
124
+ const removeRunWatcher = (stateDir, watcher, options = {}) => {
125
+ if (runDirWatchers.get(stateDir) !== watcher)
126
+ return;
127
+ watcher.close();
128
+ runDirWatchers.delete(stateDir);
129
+ if (options.degraded === false) {
130
+ degradedRunDirs.delete(stateDir);
131
+ lastDiagnosticSignatures.delete(`run:${stateDir}`);
132
+ return;
133
+ }
134
+ degradedRunDirs.add(stateDir);
135
+ if (options.error)
136
+ record("error", "run", stateDir, options.error);
137
+ record("removed", "run", stateDir);
138
+ };
71
139
  const close = () => {
72
140
  stateRootWatcher?.close();
73
141
  stateRootWatcher = undefined;
@@ -76,42 +144,92 @@ export function createRunStateWatcher(input) {
76
144
  runDirWatchers.clear();
77
145
  };
78
146
  const watchRunDir = (stateDir) => {
79
- if (runDirWatchers.has(stateDir) || !existsSync(stateDir))
147
+ if (runDirWatchers.has(stateDir) || !pathExists(stateDir))
80
148
  return;
81
149
  try {
82
- const watcher = watch(stateDir, input.onChange);
83
- watcher.on("error", () => {
84
- watcher.close();
85
- runDirWatchers.delete(stateDir);
86
- });
150
+ const watcher = watchPath(stateDir, input.onChange);
151
+ watcher.on("error", (error) => removeRunWatcher(stateDir, watcher, { error }));
87
152
  runDirWatchers.set(stateDir, watcher);
153
+ if (degradedRunDirs.delete(stateDir))
154
+ record("rearmed", "run", stateDir);
88
155
  }
89
- catch {
90
- // Watching is best-effort; explicit inspect remains available.
156
+ catch (error) {
157
+ degradedRunDirs.add(stateDir);
158
+ record("attach_failed", "run", stateDir, error);
91
159
  }
92
160
  };
93
161
  function refresh() {
94
- if (!existsSync(stateRoot))
162
+ if (!pathExists(stateRoot))
95
163
  return;
96
164
  if (!stateRootWatcher) {
97
165
  try {
98
- stateRootWatcher = watch(stateRoot, input.onChange);
99
- stateRootWatcher.on("error", () => {
100
- stateRootWatcher?.close();
166
+ const watcher = watchPath(stateRoot, input.onChange);
167
+ stateRootWatcher = watcher;
168
+ watcher.on("error", (error) => {
169
+ if (stateRootWatcher !== watcher)
170
+ return;
171
+ watcher.close();
101
172
  stateRootWatcher = undefined;
173
+ rootDegraded = true;
174
+ record("error", "root", stateRoot, error);
175
+ record("removed", "root", stateRoot);
102
176
  });
177
+ if (rootDegraded) {
178
+ rootDegraded = false;
179
+ record("rearmed", "root", stateRoot);
180
+ }
103
181
  }
104
- catch {
105
- // Watching is best-effort; explicit inspect remains available.
182
+ catch (error) {
183
+ rootDegraded = true;
184
+ record("attach_failed", "root", stateRoot, error);
106
185
  }
107
186
  }
108
- for (const entry of readdirSync(stateRoot, { withFileTypes: true })) {
109
- if (!entry.isDirectory())
187
+ let stateDirs;
188
+ try {
189
+ stateDirs = listDirectories(stateRoot);
190
+ }
191
+ catch (error) {
192
+ record("attach_failed", "root", stateRoot, error);
193
+ return;
194
+ }
195
+ const present = new Set(stateDirs);
196
+ for (const [stateDir, watcher] of runDirWatchers) {
197
+ if (present.has(stateDir) && pathExists(stateDir))
110
198
  continue;
111
- watchRunDir(`${stateRoot}/${entry.name}`);
199
+ removeRunWatcher(stateDir, watcher, { degraded: false });
112
200
  }
201
+ for (const stateDir of stateDirs)
202
+ watchRunDir(stateDir);
113
203
  }
114
- return { close, refresh };
204
+ return {
205
+ close,
206
+ getDiagnostics: () => [...diagnostics],
207
+ refresh,
208
+ };
209
+ }
210
+ export function createRunTerminalReconciliationLoop(input) {
211
+ const intervalMs = input.intervalMs ?? 10_000;
212
+ let interval;
213
+ const reconcileNow = () => {
214
+ try {
215
+ input.refreshWatcher();
216
+ input.reconcile();
217
+ }
218
+ catch (error) {
219
+ input.onError?.(error);
220
+ }
221
+ };
222
+ const close = () => {
223
+ if (interval)
224
+ clearInterval(interval);
225
+ interval = undefined;
226
+ };
227
+ const start = () => {
228
+ close();
229
+ interval = setInterval(reconcileNow, intervalMs);
230
+ interval.unref?.();
231
+ };
232
+ return { close, reconcileNow, start };
115
233
  }
116
234
  const TERMINAL = new Set([
117
235
  "done",
@@ -193,6 +311,9 @@ function observeRun(stateDir) {
193
311
  !Array.isArray(status.model_policy)
194
312
  ? { modelPolicy: status.model_policy }
195
313
  : {}),
314
+ ...(status.notification_policy === "silent"
315
+ ? { notificationPolicy: "silent" }
316
+ : {}),
196
317
  ...(typeof status.recipe_file === "string"
197
318
  ? { recipeFile: status.recipe_file }
198
319
  : {}),
@@ -497,7 +618,9 @@ export function detectRunTransitions(previous, summary) {
497
618
  for (const run of summary.runs) {
498
619
  const key = runObservationKey(run);
499
620
  const old = previous.get(key);
500
- if (!run.terminalHandled && TERMINAL.has(run.status)) {
621
+ if (run.notificationPolicy !== "silent" &&
622
+ !run.terminalHandled &&
623
+ TERMINAL.has(run.status)) {
501
624
  transitions.push({
502
625
  from: old ?? "running",
503
626
  run: run.run,
@@ -589,6 +712,11 @@ export function detectRunOutboxEvents(previousLineCounts, summary, seenEventIds
589
712
  const key = run.stateDir ?? run.run;
590
713
  const records = readOutboxRecords(run);
591
714
  const previousCount = previousLineCounts.get(key) ?? 0;
715
+ if (run.notificationPolicy === "silent") {
716
+ previousLineCounts.set(key, records.length);
717
+ seenEventIds.set(key, seenEventIds.get(key) ?? new Set());
718
+ continue;
719
+ }
592
720
  const start = Math.min(previousCount, records.length);
593
721
  const seen = seenEventIds.get(key) ?? new Set();
594
722
  for (let index = start; index < records.length; index += 1) {
@@ -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;