@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
@@ -0,0 +1,270 @@
1
+ /**
2
+ * Automatic active-tool portfolio review contract.
3
+ * Zones: thirty-six-tool reviewer input, evolution decisions, mechanical contract gates
4
+ * Owns strict read-only keep/evolve/replace/demote/merge/split review semantics.
5
+ */
6
+
7
+ export const TOOL_REVIEW_THRESHOLD = 36;
8
+
9
+ export interface ToolReviewAssessment {
10
+ adaptability: number;
11
+ futureUsefulness: number;
12
+ lifetimeCalls: number;
13
+ redundancy: number;
14
+ revisionCalls: number;
15
+ safety: string;
16
+ }
17
+
18
+ export interface ToolReviewTool {
19
+ name: string;
20
+ path: string;
21
+ recipe: Record<string, unknown>;
22
+ riskLabels: string[];
23
+ sha256: string;
24
+ usage?: Record<string, unknown>;
25
+ valid: boolean;
26
+ }
27
+
28
+ export interface ToolReviewInput {
29
+ createdAt: string;
30
+ reviewId: string;
31
+ tools: ToolReviewTool[];
32
+ }
33
+
34
+ export interface ToolReviewOutputRecipe {
35
+ name: string;
36
+ recipe: Record<string, unknown>;
37
+ }
38
+
39
+ export type ToolReviewAction =
40
+ | "demote"
41
+ | "evolve"
42
+ | "keep"
43
+ | "merge"
44
+ | "replace"
45
+ | "split";
46
+
47
+ export interface ToolReviewDecision {
48
+ action: ToolReviewAction;
49
+ assessment: ToolReviewAssessment;
50
+ rationale: string;
51
+ recipe?: Record<string, unknown>;
52
+ sha256: string;
53
+ source: string;
54
+ target?: string;
55
+ outputs?: ToolReviewOutputRecipe[];
56
+ }
57
+
58
+ export interface ToolReviewResult {
59
+ createdAt: string;
60
+ decisions: ToolReviewDecision[];
61
+ reviewId: string;
62
+ }
63
+
64
+ export interface ToolReviewValidation {
65
+ errors: string[];
66
+ ok: boolean;
67
+ }
68
+
69
+ const UUID_PATTERN = /^[a-f0-9-]{36}$/u;
70
+ const TOOL_NAME_PATTERN = /^[a-z][a-z0-9_]*$/u;
71
+
72
+ function isRecord(value: unknown): value is Record<string, unknown> {
73
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
74
+ }
75
+
76
+ function canonicalJson(value: unknown): string {
77
+ if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
78
+ if (!isRecord(value)) return JSON.stringify(value);
79
+ return `{${Object.keys(value)
80
+ .sort()
81
+ .map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`)
82
+ .join(",")}}`;
83
+ }
84
+
85
+ function finiteScore(value: unknown): boolean {
86
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1;
87
+ }
88
+
89
+ function calls(value: unknown): number {
90
+ return typeof value === "number" && Number.isInteger(value) && value >= 0
91
+ ? value
92
+ : 0;
93
+ }
94
+
95
+ function validateAssessment(
96
+ assessment: unknown,
97
+ tool: ToolReviewTool,
98
+ errors: string[],
99
+ ): void {
100
+ if (
101
+ !isRecord(assessment) ||
102
+ !finiteScore(assessment.adaptability) ||
103
+ !finiteScore(assessment.futureUsefulness) ||
104
+ !finiteScore(assessment.redundancy) ||
105
+ typeof assessment.safety !== "string" ||
106
+ !assessment.safety.trim() ||
107
+ !Number.isInteger(assessment.lifetimeCalls) ||
108
+ Number(assessment.lifetimeCalls) < 0 ||
109
+ !Number.isInteger(assessment.revisionCalls) ||
110
+ Number(assessment.revisionCalls) < 0
111
+ ) {
112
+ errors.push(`invalid tool assessment: ${tool.name}`);
113
+ return;
114
+ }
115
+ if (assessment.lifetimeCalls !== calls(tool.usage?.lifetime_calls ?? tool.usage?.calls)) {
116
+ errors.push(`tool lifetime calls differ from lineage: ${tool.name}`);
117
+ }
118
+ if (assessment.revisionCalls !== calls(tool.usage?.revision_calls)) {
119
+ errors.push(`tool revision calls differ from lineage: ${tool.name}`);
120
+ }
121
+ }
122
+
123
+ export function validateToolReviewInput(input: ToolReviewInput): ToolReviewValidation {
124
+ const errors: string[] = [];
125
+ if (!UUID_PATTERN.test(input.reviewId)) errors.push("tool reviewId must be a UUID");
126
+ if (!input.createdAt || Number.isNaN(Date.parse(input.createdAt))) {
127
+ errors.push("tool review createdAt must be an ISO timestamp");
128
+ }
129
+ if (!Array.isArray(input.tools) || input.tools.length !== TOOL_REVIEW_THRESHOLD) {
130
+ errors.push(`tool review input must contain exactly ${TOOL_REVIEW_THRESHOLD} tools`);
131
+ }
132
+ const names = new Set<string>();
133
+ for (const tool of input.tools ?? []) {
134
+ if (
135
+ !tool ||
136
+ !TOOL_NAME_PATTERN.test(tool.name) ||
137
+ typeof tool.path !== "string" ||
138
+ typeof tool.sha256 !== "string" ||
139
+ typeof tool.valid !== "boolean" ||
140
+ !isRecord(tool.recipe) ||
141
+ !Array.isArray(tool.riskLabels) ||
142
+ names.has(tool.name)
143
+ ) {
144
+ errors.push(`invalid or duplicate reviewed tool: ${String(tool?.name)}`);
145
+ continue;
146
+ }
147
+ names.add(tool.name);
148
+ }
149
+ return { errors, ok: errors.length === 0 };
150
+ }
151
+
152
+ export function validateToolReviewResult(
153
+ input: ToolReviewInput,
154
+ result: ToolReviewResult,
155
+ ): ToolReviewValidation {
156
+ const errors = [...validateToolReviewInput(input).errors];
157
+ if (result.reviewId !== input.reviewId) errors.push("tool review identity mismatch");
158
+ if (!result.createdAt || Number.isNaN(Date.parse(result.createdAt))) {
159
+ errors.push("tool review result createdAt must be an ISO timestamp");
160
+ }
161
+ if (!Array.isArray(result.decisions)) {
162
+ errors.push("tool review decisions must be an array");
163
+ return { errors, ok: false };
164
+ }
165
+ const inventory = new Map(input.tools.map((tool) => [tool.name, tool]));
166
+ const activeNames = new Set(inventory.keys());
167
+ const seen = new Set<string>();
168
+ const outputNames = new Set<string>();
169
+ const mergeGroups = new Map<string, Array<{ decision: ToolReviewDecision; recipe: string }>>();
170
+ for (const decision of result.decisions) {
171
+ const tool = inventory.get(decision.source);
172
+ if (!tool) {
173
+ errors.push(`unknown reviewed tool: ${decision.source}`);
174
+ continue;
175
+ }
176
+ if (seen.has(decision.source)) {
177
+ errors.push(`duplicate tool decision: ${decision.source}`);
178
+ continue;
179
+ }
180
+ seen.add(decision.source);
181
+ if (decision.sha256 !== tool.sha256) errors.push(`reviewed tool changed: ${tool.name}`);
182
+ if (!decision.rationale?.trim()) errors.push(`missing tool rationale: ${tool.name}`);
183
+ validateAssessment(decision.assessment, tool, errors);
184
+ if (decision.action === "keep" || decision.action === "demote") {
185
+ if (decision.target || decision.recipe || decision.outputs) {
186
+ errors.push(`${decision.action} declares evolution fields: ${tool.name}`);
187
+ }
188
+ continue;
189
+ }
190
+ if (decision.action === "replace" || decision.action === "split") {
191
+ errors.push(`${decision.action} requires explicit operator-authored recipe mutation: ${tool.name}`);
192
+ continue;
193
+ }
194
+ if (decision.action === "evolve") {
195
+ if (!decision.target || !TOOL_NAME_PATTERN.test(decision.target)) {
196
+ errors.push(`evolve requires snake_case target: ${tool.name}`);
197
+ }
198
+ if (decision.recipe !== undefined || decision.outputs !== undefined) {
199
+ errors.push(`evolve must not supply executable recipe content: ${tool.name}`);
200
+ }
201
+ if (
202
+ decision.target &&
203
+ activeNames.has(decision.target) &&
204
+ decision.target !== tool.name
205
+ ) {
206
+ errors.push(`evolve target collides with active tool: ${decision.target}`);
207
+ }
208
+ if (decision.target && outputNames.has(decision.target)) {
209
+ errors.push(`duplicate evolved target: ${decision.target}`);
210
+ } else if (decision.target) outputNames.add(decision.target);
211
+ continue;
212
+ }
213
+ if (decision.action === "merge") {
214
+ if (!decision.target || !TOOL_NAME_PATTERN.test(decision.target)) {
215
+ errors.push(`merge requires snake_case target: ${tool.name}`);
216
+ continue;
217
+ }
218
+ if (decision.recipe !== undefined || decision.outputs !== undefined) {
219
+ errors.push(`merge must not supply executable recipe content: ${tool.name}`);
220
+ }
221
+ const group = mergeGroups.get(decision.target) ?? [];
222
+ group.push({ decision, recipe: canonicalJson(tool.recipe) });
223
+ mergeGroups.set(decision.target, group);
224
+ continue;
225
+ }
226
+ errors.push(`invalid tool review action: ${tool.name}`);
227
+ }
228
+ for (const tool of input.tools) {
229
+ if (!seen.has(tool.name)) errors.push(`missing tool decision: ${tool.name}`);
230
+ }
231
+ for (const [target, group] of mergeGroups) {
232
+ if (group.length < 2) errors.push(`merge target requires at least two sources: ${target}`);
233
+ if (
234
+ activeNames.has(target) &&
235
+ !group.some((entry) => entry.decision.source === target)
236
+ ) {
237
+ errors.push(`merge target collides with unrelated active tool: ${target}`);
238
+ }
239
+ if (new Set(group.map((entry) => entry.recipe)).size !== 1) {
240
+ errors.push(`merge target recipes differ: ${target}`);
241
+ }
242
+ if (outputNames.has(target)) errors.push(`duplicate evolved target: ${target}`);
243
+ outputNames.add(target);
244
+ }
245
+ return { errors, ok: errors.length === 0 };
246
+ }
247
+
248
+ export function parseToolReviewResult(stdout: string): ToolReviewResult {
249
+ const marker = "TOOL_REVIEW_RESULT";
250
+ const markerIndex = stdout.lastIndexOf(marker);
251
+ if (markerIndex < 0) throw new Error("Tool reviewer output marker is missing.");
252
+ const payload = stdout.slice(markerIndex + marker.length).trim();
253
+ if (!payload.startsWith("{") || !payload.endsWith("}")) {
254
+ throw new Error("Tool reviewer output must end with one JSON object.");
255
+ }
256
+ const value = JSON.parse(payload) as unknown;
257
+ if (!isRecord(value)) throw new Error("Tool reviewer result must be an object.");
258
+ return value as unknown as ToolReviewResult;
259
+ }
260
+
261
+ export function createToolReviewPrompt(inputPath: string): string {
262
+ return [
263
+ `Review the attached immutable active-tool portfolio from ${inputPath}.`,
264
+ "Evaluate all 36 tools independently from lifetime/revision usage, adaptability, redundancy, safety, contract quality, and likely future usefulness.",
265
+ "Choose exactly one action per source: keep, evolve, demote, or merge. There is no quota for any action.",
266
+ "evolve may only rename one unchanged captured recipe; demote moves the unchanged recipe to draft memory; merge may only deduplicate at least two canonically identical captured recipes. Never return recipe or outputs fields. replace and split require explicit operator-authored mutation and are unavailable to automatic review.",
267
+ "Do not mutate, register, move, or delete recipes. You are a read-only selector; the deterministic executor derives exact recipe bytes from captured sources and owns every portfolio mutation.",
268
+ "End stdout with TOOL_REVIEW_RESULT on its own line followed by exactly one JSON object with reviewId, createdAt, and decisions.",
269
+ ].join("\n");
270
+ }
@@ -7,12 +7,14 @@
7
7
  import { execFileSync } from "node:child_process";
8
8
  import { existsSync, readFileSync } from "node:fs";
9
9
  import { dirname, join } from "node:path";
10
+ import { fileURLToPath } from "node:url";
10
11
 
11
12
  import * as AsyncRuns from "./async-runs.ts";
12
13
  import * as Limits from "./limits.ts";
13
14
  import * as Messages from "./messages.ts";
14
15
  import * as Paths from "./paths.ts";
15
16
  import * as RecipesDiscovery from "./recipes-discovery.ts";
17
+ import * as ReviewDiagnostics from "./review-diagnostics.ts";
16
18
  import * as Rooms from "./rooms.ts";
17
19
  import * as Schema from "./schema.ts";
18
20
  import * as ToolsAccess from "./tools-access.ts";
@@ -22,6 +24,17 @@ import * as ToolsResponse from "./tools-response.ts";
22
24
  const asRecord = ToolsResponse.asRecord;
23
25
  const maybeJsonText = ToolsResponse.maybeJsonText;
24
26
 
27
+ function compactAutomaticReviewDiagnostics(
28
+ diagnostics: Record<string, unknown>,
29
+ ): string {
30
+ const draft = asRecord(diagnostics.draft_review);
31
+ const tool = asRecord(diagnostics.tool_review);
32
+ return [
33
+ `automatic_reviews draft=${String(draft.phase ?? "idle")} tool=${String(tool.phase ?? "idle")}`,
34
+ `lineages=${String(diagnostics.lineage_count ?? 0)} revision_snapshots=${String(diagnostics.revision_snapshots ?? 0)}`,
35
+ ].join("\n");
36
+ }
37
+
25
38
  function compactRunMessages(messages: AsyncRuns.RunOutboxEvent[]): string {
26
39
  if (messages.length === 0) return "\n(no actor messages)";
27
40
  return `\n${messages
@@ -257,8 +270,9 @@ function getPiActorsRuntimeStatus(): Record<string, unknown> {
257
270
  } catch {
258
271
  git_commit = undefined;
259
272
  }
260
- const entrypoint = new URL(import.meta.url).pathname;
273
+ const entrypoint = fileURLToPath(import.meta.url);
261
274
  return {
275
+ automatic_recipe_review: Paths.isAutomaticRecipeReviewEnabled(),
262
276
  entrypoint,
263
277
  git_commit,
264
278
  mode: entrypoint.includes("/dist/") ? "dist" : "source",
@@ -271,7 +285,7 @@ function getPiActorsRuntimeStatus(): Record<string, unknown> {
271
285
  }
272
286
 
273
287
  function compactPiActorsRuntimeStatus(status: Record<string, unknown>): string {
274
- return `\npi-actors version=${String(status.version)} mode=${String(status.mode)} path=${String(status.package_root)} entrypoint=${String(status.entrypoint)}${status.git_commit ? ` git=${String(status.git_commit)}` : ""}`;
288
+ return `\npi-actors version=${String(status.version)} mode=${String(status.mode)} automatic_review=${String(status.automatic_recipe_review)} path=${String(status.package_root)} entrypoint=${String(status.entrypoint)}${status.git_commit ? ` git=${String(status.git_commit)}` : ""}`;
275
289
  }
276
290
 
277
291
  function isStaleClaim(message: AsyncRuns.RunInboxMessage, now: number): boolean {
@@ -515,12 +529,29 @@ export function createInspectToolDefinition<TContext = unknown>(
515
529
  view !== "status" &&
516
530
  view !== "summary" &&
517
531
  view !== "doctor" &&
518
- view !== "imports"
532
+ view !== "imports" &&
533
+ view !== "reviews"
519
534
  ) {
520
535
  throw new Error(
521
- "inspect recipes supports view=status, view=summary, view=doctor, or view=imports.",
536
+ "inspect recipes supports view=status, view=summary, view=doctor, view=imports, or view=reviews.",
522
537
  );
523
538
  }
539
+ if (view === "reviews") {
540
+ const diagnostics = ReviewDiagnostics.readAutomaticReviewDiagnostics({
541
+ recipeRoot: deps.recipeRoot ?? Paths.getRecipeRoot(),
542
+ });
543
+ return {
544
+ content: [{
545
+ type: "text" as const,
546
+ text: maybeJsonText(
547
+ diagnostics,
548
+ input.verbose === true,
549
+ compactAutomaticReviewDiagnostics(diagnostics),
550
+ ),
551
+ }],
552
+ details: diagnostics,
553
+ };
554
+ }
524
555
  const discovered = RecipesDiscovery.discoverRecipeSources([
525
556
  {
526
557
  root: deps.recipeRoot ?? Paths.getRecipeRoot(),
@@ -143,6 +143,14 @@ export function createRuntimeToolDefinition(
143
143
  paramSchema.run_id = Schema.stringSchema(
144
144
  "Optional run id override for this async template recipe invocation.",
145
145
  );
146
+ if (isAsyncRecipe) {
147
+ paramSchema.correlation_id = Schema.stringSchema(
148
+ "Optional workflow correlation id preserved in terminal follow-up delivery.",
149
+ );
150
+ paramSchema.transport_context = Schema.looseObjectSchema(
151
+ "Optional originating transport route preserved for detached terminal follow-up.",
152
+ );
153
+ }
146
154
  return {
147
155
  name: cfg.name,
148
156
  label: cfg.name,
@@ -155,18 +163,24 @@ export function createRuntimeToolDefinition(
155
163
  )
156
164
  : Prompts.formatRegisteredToolPromptSnippet(cfg.template),
157
165
  async execute(
158
- _toolCallId: string,
166
+ toolCallId: string,
159
167
  params: unknown,
160
168
  signal: AbortSignal | undefined,
161
169
  _onUpdate: unknown,
162
170
  ctx: RuntimeToolContext,
163
171
  ) {
164
172
  try {
165
- if (cfg.sourcePath)
166
- RecipesUsage.recordRecipeLaunch(cfg.sourcePath, new Date(), "tool");
173
+ if (
174
+ cfg.sourcePath &&
175
+ !RecipesUsage.recordRecipeLaunch(cfg.sourcePath, new Date(), "tool")
176
+ ) {
177
+ throw new Error(
178
+ `Recipe launch rejected because its source changed during activation: ${cfg.sourcePath}. Reload recipe tools and retry.`,
179
+ );
180
+ }
167
181
  if (isAsyncRecipe) {
168
182
  const input = params as Record<string, unknown>;
169
- const { run_id, ...values } = input;
183
+ const { correlation_id, run_id, transport_context, ...values } = input;
170
184
  const base = cfg.recipe ? cfg.recipe : { file: String(cfg.template) };
171
185
  const runId =
172
186
  typeof run_id === "string" && run_id.trim()
@@ -176,6 +190,17 @@ export function createRuntimeToolDefinition(
176
190
  {
177
191
  ...base,
178
192
  launch_source: "tool",
193
+ launch_correlation: {
194
+ ...(typeof correlation_id === "string"
195
+ ? { correlation_id } : {}),
196
+ tool_call_id: toolCallId,
197
+ },
198
+ ...(transport_context &&
199
+ typeof transport_context === "object" &&
200
+ !Array.isArray(transport_context)
201
+ ? {
202
+ transport_context: transport_context as Record<string, unknown>,
203
+ } : {}),
179
204
  ownerId: getRunOwnerId(ctx),
180
205
  run_id: runId,
181
206
  tool: cfg.name,
@@ -290,6 +290,10 @@ function compactActorMessageResult(
290
290
 
291
291
  export interface ActorMessageToolDeps<TContext = unknown> {
292
292
  getTool?: (name: string) => any | undefined;
293
+ handleRuntimeMessage?: (
294
+ type: string,
295
+ body: unknown,
296
+ ) => Record<string, unknown>;
293
297
  }
294
298
 
295
299
  export function createActorMessageToolDefinition<TContext = unknown>(
@@ -363,7 +367,14 @@ export function createActorMessageToolDefinition<TContext = unknown>(
363
367
  ]
364
368
  : [];
365
369
  if (message.type === "control.kill") {
366
- result = AsyncRuns.killRun(address.value);
370
+ result = typeof status.run_instance_id === "string"
371
+ ? AsyncRuns.killRun(address.value, {
372
+ ...(typeof status.ownerId === "string"
373
+ ? { ownerId: status.ownerId }
374
+ : {}),
375
+ runInstanceId: status.run_instance_id,
376
+ })
377
+ : { killed: false, reason: "run generation unavailable" };
367
378
  } else if (message.type === "control.archive") {
368
379
  result = AsyncRuns.archiveRun(address.value);
369
380
  } else if (message.type === "control.prune") {
@@ -463,36 +474,40 @@ export function createActorMessageToolDefinition<TContext = unknown>(
463
474
  : {}),
464
475
  };
465
476
  } else if (address.kind === "tool" && address.value) {
466
- const tool = deps.getTool?.(address.value);
467
- if (!tool || typeof tool.execute !== "function") {
468
- throw new Error(
469
- `tool actor not found or not executable: ${address.value}`,
470
- );
471
- }
472
- const toolParams = messageBodyToToolParams(message);
473
- let toolResult: unknown;
474
- try {
475
- toolResult = await tool.execute(
476
- `message:${message.type}`,
477
- toolParams,
478
- _signal,
479
- _onUpdate,
480
- ctx,
481
- );
482
- } catch (error) {
483
- throw formatToolActorFailure(
484
- address.value,
485
- message,
486
- toolParams,
487
- error,
488
- );
477
+ if (address.value === "pi-actors" && deps.handleRuntimeMessage) {
478
+ result = deps.handleRuntimeMessage(message.type, message.body);
479
+ } else {
480
+ const tool = deps.getTool?.(address.value);
481
+ if (!tool || typeof tool.execute !== "function") {
482
+ throw new Error(
483
+ `tool actor not found or not executable: ${address.value}`,
484
+ );
485
+ }
486
+ const toolParams = messageBodyToToolParams(message);
487
+ let toolResult: unknown;
488
+ try {
489
+ toolResult = await tool.execute(
490
+ `message:${message.type}`,
491
+ toolParams,
492
+ _signal,
493
+ _onUpdate,
494
+ ctx,
495
+ );
496
+ } catch (error) {
497
+ throw formatToolActorFailure(
498
+ address.value,
499
+ message,
500
+ toolParams,
501
+ error,
502
+ );
503
+ }
504
+ result = {
505
+ invoked: true,
506
+ sent: true,
507
+ tool: address.value,
508
+ tool_result: toolResult,
509
+ };
489
510
  }
490
- result = {
491
- invoked: true,
492
- sent: true,
493
- tool: address.value,
494
- tool_result: toolResult,
495
- };
496
511
  } else if (address.kind === "coordinator" || address.kind === "session") {
497
512
  if (!message.from) {
498
513
  throw new Error(`message to ${address.kind} requires from=run:<id>.`);
@@ -8,10 +8,12 @@ import { mkdirSync, writeFileSync } from "node:fs";
8
8
  import { join } from "node:path";
9
9
 
10
10
  import * as AsyncRuns from "./async-runs.ts";
11
+ import { withFileMutationLock } from "./file-state.ts";
11
12
  import * as Messages from "./messages.ts";
12
13
  import * as ModelContext from "./model-context.ts";
13
14
  import * as Paths from "./paths.ts";
14
15
  import * as RecipesDiscovery from "./recipes-discovery.ts";
16
+ import * as RecipesUsage from "./recipes-usage.ts";
15
17
  import * as Rooms from "./rooms.ts";
16
18
  import * as Schema from "./schema.ts";
17
19
  import * as ToolsResponse from "./tools-response.ts";
@@ -90,7 +92,12 @@ function writeSpawnDraftRecipe(
90
92
  ...(defaults ? { defaults } : {}),
91
93
  template: input.template,
92
94
  };
93
- writeFileSync(path, `${JSON.stringify(recipe, null, 2)}\n`, { flag: "wx" });
95
+ withFileMutationLock(root, () =>
96
+ withFileMutationLock(path, () =>
97
+ writeFileSync(path, `${JSON.stringify(recipe, null, 2)}\n`, { flag: "wx" }),
98
+ ),
99
+ );
100
+ RecipesUsage.recordRecipeLaunch(path, new Date(), "spawn", Paths.getRecipeRoot());
94
101
  return path;
95
102
  }
96
103
 
@@ -137,6 +144,9 @@ export function createSpawnToolDefinition<
137
144
  as: Schema.stringSchema(
138
145
  "Optional actor address for the spawned run, e.g. run:<id>.",
139
146
  ),
147
+ correlation_id: Schema.stringSchema(
148
+ "Optional workflow correlation id preserved in terminal follow-up delivery.",
149
+ ),
140
150
  file: Schema.stringSchema(
141
151
  "Optional template recipe JSON file. Bare names resolve under ~/.pi/agent/recipes.",
142
152
  ),
@@ -155,6 +165,9 @@ export function createSpawnToolDefinition<
155
165
  values: Schema.looseObjectSchema(
156
166
  "Runtime placeholder values passed to the actor.",
157
167
  ),
168
+ transport_context: Schema.looseObjectSchema(
169
+ "Optional originating transport route preserved for detached terminal follow-up, e.g. Telegram chat_id and thread_id.",
170
+ ),
158
171
  verbose: Schema.booleanSchema(
159
172
  "Return full JSON instead of compact text.",
160
173
  ),
@@ -162,7 +175,7 @@ export function createSpawnToolDefinition<
162
175
  [],
163
176
  ),
164
177
  async execute(
165
- _toolCallId: string,
178
+ toolCallId: string,
166
179
  params: unknown,
167
180
  _signal: AbortSignal | undefined,
168
181
  _onUpdate: unknown,
@@ -189,7 +202,14 @@ export function createSpawnToolDefinition<
189
202
  {
190
203
  file: recipe,
191
204
  launch_source: "spawn",
205
+ launch_correlation: {
206
+ ...(typeof input.correlation_id === "string"
207
+ ? { correlation_id: input.correlation_id } : {}),
208
+ tool_call_id: toolCallId,
209
+ },
192
210
  ownerId: getRunOwnerId(ctx),
211
+ ...(input.transport_context
212
+ ? { transport_context: asRecord(input.transport_context) } : {}),
193
213
  run_id: runId,
194
214
  ...(input.template !== undefined
195
215
  ? {
package/lib/tools.ts CHANGED
@@ -25,6 +25,10 @@ export interface CoreActorToolDefinitionDeps<
25
25
  configPath: string;
26
26
  getActiveTools: () => string[];
27
27
  getRuntimeTool: (name: string) => unknown;
28
+ handleRuntimeMessage?: (
29
+ type: string,
30
+ body: unknown,
31
+ ) => Record<string, unknown>;
28
32
  registryRuntime: Pick<
29
33
  RegisterToolRuntimeDeps<TContext>,
30
34
  "getToolNameBlocker" | "getTools" | "notify" | "registerRuntimeTool"
@@ -71,6 +75,7 @@ export function createCoreActorToolDefinitions<
71
75
  ToolsSpawn.createSpawnToolDefinition<TContext>(),
72
76
  ToolsMessage.createActorMessageToolDefinition<TContext>({
73
77
  getTool: (name) => deps.getRuntimeTool(name),
78
+ handleRuntimeMessage: deps.handleRuntimeMessage,
74
79
  }),
75
80
  ToolsInspect.createInspectToolDefinition<TContext>({
76
81
  getTool: (name) => deps.getRuntimeTool(name),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-actors",
3
- "version": "0.41.1",
3
+ "version": "0.42.1",
4
4
  "private": false,
5
5
  "description": "Local Actor Kernel for Pi",
6
6
  "keywords": [
@@ -27,10 +27,12 @@
27
27
  "scripts": {
28
28
  "check": "node --experimental-strip-types -e \"await import('./index.ts'); console.log('pi-actors: extension import ok')\"",
29
29
  "conformance": "node scripts/conformance.mjs",
30
- "test": "node --experimental-strip-types --test tests/*.test.ts",
30
+ "test": "node --experimental-strip-types --test --test-concurrency=1 tests/*.test.ts",
31
31
  "pack:dry": "npm pack --dry-run",
32
32
  "recipes:qa": "node scripts/validate-recipe.mjs recipes --all --qa --summary",
33
- "validate": "npx tsc --noEmit && npm run recipes:qa && npm run build && npm run check && npm test && npm run pack:dry",
33
+ "release:validate": "npm run validate && node scripts/release-gates.mjs",
34
+ "validate": "npx tsc --noEmit && npm run recipes:qa && npm run build && npm run check && npm test && npm run conformance && npm run pack:dry",
35
+ "audit:dependencies": "npm audit --audit-level=high --omit=peer",
34
36
  "build": "node scripts/build-dist.mjs",
35
37
  "prepack": "npm run build"
36
38
  },
@@ -0,0 +1,24 @@
1
+ {
2
+ "async": true,
3
+ "args": [
4
+ "input_path:path",
5
+ "model:string",
6
+ "thinking:string"
7
+ ],
8
+ "defaults": {
9
+ "model": "{current_model}",
10
+ "thinking": "{current_thinking}"
11
+ },
12
+ "description": "Read-only background reviewer for one immutable automatic draft-memory batch.",
13
+ "mailbox": {
14
+ "accepts": [
15
+ "control.kill"
16
+ ],
17
+ "emits": [
18
+ "command.done",
19
+ "run.done",
20
+ "run.failed"
21
+ ]
22
+ },
23
+ "template": "pi -p --model {model} --thinking {thinking} --no-tools @{input_path} Review the attached immutable pi-actors draft batch. Evaluate every draft independently using launch history, universality, flexibility, parameterization, duplication, safety, and likely future usefulness. Treat draft and sha256 fields as batch-local opaque occurrence/content-group identifiers and return them verbatim. Choose exactly one promote or discard decision per draft; there is no selection quota. Promotion requires a unique absent snake_case target and targetSha256 null. Never return recipe content: the executor derives the exact immutable captured source. Invalid or secret-touching drafts must be discarded. Include assessment with launches, universality, flexibility, futureUsefulness, and safety. Do not mutate, move, register, or delete recipes. End stdout with DRAFT_REVIEW_RESULT on its own line followed by exactly one JSON object with batchId, createdAt, and decisions."
24
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "async": true,
3
+ "args": [
4
+ "input_path:path",
5
+ "model:string",
6
+ "thinking:string"
7
+ ],
8
+ "defaults": {
9
+ "model": "{current_model}",
10
+ "thinking": "{current_thinking}"
11
+ },
12
+ "description": "Read-only background reviewer for one immutable 36-tool evolution portfolio.",
13
+ "mailbox": {
14
+ "accepts": [
15
+ "control.kill"
16
+ ],
17
+ "emits": [
18
+ "command.done",
19
+ "run.done",
20
+ "run.failed"
21
+ ]
22
+ },
23
+ "template": "pi -p --model {model} --thinking {thinking} --no-tools @{input_path} Review the attached immutable pi-actors active-tool portfolio. Evaluate all 36 tools independently using lifetime and revision usage, adaptability, redundancy, safety, contract quality, and likely future usefulness. Treat source/name and sha256 fields as batch-local opaque occurrence/content-group identifiers, return them verbatim, and infer exact duplication only from a shared content group. Choose exactly one keep, evolve, demote, or merge decision per tool; there is no action quota. evolve may only rename one unchanged captured recipe, demote moves the unchanged recipe to draft memory, and merge may only deduplicate at least two canonically identical captured recipes. Never return recipe or outputs fields; replace and split require explicit operator-authored mutation and are unavailable to automatic review. Do not mutate, register, move, or delete recipes. End stdout with TOOL_REVIEW_RESULT on its own line followed by exactly one JSON object with reviewId, createdAt, and decisions."
24
+ }