@llblab/pi-actors 0.41.1 → 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 (147) hide show
  1. package/AGENTS.md +11 -6
  2. package/BACKLOG.md +2 -62
  3. package/CHANGELOG.md +15 -0
  4. package/README.md +22 -4
  5. package/dist/index.js +25 -121
  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 +20 -3
  25. package/dist/lib/inspector-overlay.js +275 -74
  26. package/dist/lib/inspector.d.ts +9 -0
  27. package/dist/lib/inspector.js +48 -0
  28. package/dist/lib/observability.d.ts +1 -0
  29. package/dist/lib/observability.js +11 -1
  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-spawn.js +4 -1
  86. package/dist/lib/tools.d.ts +1 -0
  87. package/dist/lib/tools.js +1 -0
  88. package/dist/recipes/draft-review.json +24 -0
  89. package/dist/recipes/tool-review.json +24 -0
  90. package/dist/scripts/release-gates.mjs +165 -0
  91. package/dist/skills/actors/SKILL.md +9 -8
  92. package/dist/skills/swarm/SKILL.md +1 -1
  93. package/docs/actor-inspector.md +20 -11
  94. package/docs/async-runs.md +9 -1
  95. package/docs/recipe-library.md +7 -3
  96. package/docs/template-recipes.md +4 -11
  97. package/docs/tool-registry.md +11 -4
  98. package/index.ts +27 -142
  99. package/lib/async-runs.ts +218 -62
  100. package/lib/automatic-review-runtime.ts +135 -0
  101. package/lib/draft-consolidation-transaction.ts +821 -0
  102. package/lib/draft-consolidation.ts +181 -0
  103. package/lib/draft-review.ts +325 -0
  104. package/lib/draft-sleep.ts +576 -0
  105. package/lib/file-state.ts +143 -19
  106. package/lib/inspector-actions.ts +79 -0
  107. package/lib/inspector-command.ts +54 -0
  108. package/lib/inspector-overlay.ts +325 -91
  109. package/lib/inspector.ts +72 -0
  110. package/lib/observability.ts +14 -1
  111. package/lib/paths.ts +43 -0
  112. package/lib/recipes-discovery.ts +34 -26
  113. package/lib/recipes-usage.ts +569 -40
  114. package/lib/review-control.ts +137 -0
  115. package/lib/review-diagnostics.ts +164 -0
  116. package/lib/review-projection.ts +200 -0
  117. package/lib/run-ui-runtime.ts +153 -0
  118. package/lib/runs-artifacts.ts +1 -1
  119. package/lib/runs-control.ts +49 -5
  120. package/lib/runs-identity.ts +1 -1
  121. package/lib/runs-index.ts +57 -21
  122. package/lib/runs-mailbox.ts +1 -1
  123. package/lib/runs-messages.ts +1 -1
  124. package/lib/runs-outbox.ts +1 -1
  125. package/lib/runs-ownership.ts +1 -1
  126. package/lib/runs-parent-teardown.ts +257 -0
  127. package/lib/runs-process.ts +1 -1
  128. package/lib/runs-retention.ts +1 -1
  129. package/lib/runs-start.ts +12 -68
  130. package/lib/runs-status.ts +12 -8
  131. package/lib/runtime.ts +34 -17
  132. package/lib/tool-review-lineage-transaction.ts +881 -0
  133. package/lib/tool-review-lineage.ts +145 -0
  134. package/lib/tool-review-scheduler.ts +635 -0
  135. package/lib/tool-review-transaction.ts +563 -0
  136. package/lib/tool-review.ts +270 -0
  137. package/lib/tools-inspect.ts +33 -3
  138. package/lib/tools-local.ts +8 -2
  139. package/lib/tools-message.ts +45 -30
  140. package/lib/tools-spawn.ts +8 -1
  141. package/lib/tools.ts +5 -0
  142. package/package.json +3 -2
  143. package/recipes/draft-review.json +24 -0
  144. package/recipes/tool-review.json +24 -0
  145. package/scripts/release-gates.mjs +165 -0
  146. package/skills/actors/SKILL.md +9 -8
  147. 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
+ }
@@ -13,6 +13,7 @@ import * as Limits from "./limits.ts";
13
13
  import * as Messages from "./messages.ts";
14
14
  import * as Paths from "./paths.ts";
15
15
  import * as RecipesDiscovery from "./recipes-discovery.ts";
16
+ import * as ReviewDiagnostics from "./review-diagnostics.ts";
16
17
  import * as Rooms from "./rooms.ts";
17
18
  import * as Schema from "./schema.ts";
18
19
  import * as ToolsAccess from "./tools-access.ts";
@@ -22,6 +23,17 @@ import * as ToolsResponse from "./tools-response.ts";
22
23
  const asRecord = ToolsResponse.asRecord;
23
24
  const maybeJsonText = ToolsResponse.maybeJsonText;
24
25
 
26
+ function compactAutomaticReviewDiagnostics(
27
+ diagnostics: Record<string, unknown>,
28
+ ): string {
29
+ const draft = asRecord(diagnostics.draft_review);
30
+ const tool = asRecord(diagnostics.tool_review);
31
+ return [
32
+ `automatic_reviews draft=${String(draft.phase ?? "idle")} tool=${String(tool.phase ?? "idle")}`,
33
+ `lineages=${String(diagnostics.lineage_count ?? 0)} revision_snapshots=${String(diagnostics.revision_snapshots ?? 0)}`,
34
+ ].join("\n");
35
+ }
36
+
25
37
  function compactRunMessages(messages: AsyncRuns.RunOutboxEvent[]): string {
26
38
  if (messages.length === 0) return "\n(no actor messages)";
27
39
  return `\n${messages
@@ -259,6 +271,7 @@ function getPiActorsRuntimeStatus(): Record<string, unknown> {
259
271
  }
260
272
  const entrypoint = new URL(import.meta.url).pathname;
261
273
  return {
274
+ automatic_recipe_review: Paths.isAutomaticRecipeReviewEnabled(),
262
275
  entrypoint,
263
276
  git_commit,
264
277
  mode: entrypoint.includes("/dist/") ? "dist" : "source",
@@ -271,7 +284,7 @@ function getPiActorsRuntimeStatus(): Record<string, unknown> {
271
284
  }
272
285
 
273
286
  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)}` : ""}`;
287
+ 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
288
  }
276
289
 
277
290
  function isStaleClaim(message: AsyncRuns.RunInboxMessage, now: number): boolean {
@@ -515,12 +528,29 @@ export function createInspectToolDefinition<TContext = unknown>(
515
528
  view !== "status" &&
516
529
  view !== "summary" &&
517
530
  view !== "doctor" &&
518
- view !== "imports"
531
+ view !== "imports" &&
532
+ view !== "reviews"
519
533
  ) {
520
534
  throw new Error(
521
- "inspect recipes supports view=status, view=summary, view=doctor, or view=imports.",
535
+ "inspect recipes supports view=status, view=summary, view=doctor, view=imports, or view=reviews.",
522
536
  );
523
537
  }
538
+ if (view === "reviews") {
539
+ const diagnostics = ReviewDiagnostics.readAutomaticReviewDiagnostics({
540
+ recipeRoot: deps.recipeRoot ?? Paths.getRecipeRoot(),
541
+ });
542
+ return {
543
+ content: [{
544
+ type: "text" as const,
545
+ text: maybeJsonText(
546
+ diagnostics,
547
+ input.verbose === true,
548
+ compactAutomaticReviewDiagnostics(diagnostics),
549
+ ),
550
+ }],
551
+ details: diagnostics,
552
+ };
553
+ }
524
554
  const discovered = RecipesDiscovery.discoverRecipeSources([
525
555
  {
526
556
  root: deps.recipeRoot ?? Paths.getRecipeRoot(),
@@ -162,8 +162,14 @@ export function createRuntimeToolDefinition(
162
162
  ctx: RuntimeToolContext,
163
163
  ) {
164
164
  try {
165
- if (cfg.sourcePath)
166
- RecipesUsage.recordRecipeLaunch(cfg.sourcePath, new Date(), "tool");
165
+ if (
166
+ cfg.sourcePath &&
167
+ !RecipesUsage.recordRecipeLaunch(cfg.sourcePath, new Date(), "tool")
168
+ ) {
169
+ throw new Error(
170
+ `Recipe launch rejected because its source changed during activation: ${cfg.sourcePath}. Reload recipe tools and retry.`,
171
+ );
172
+ }
167
173
  if (isAsyncRecipe) {
168
174
  const input = params as Record<string, unknown>;
169
175
  const { run_id, ...values } = input;
@@ -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
 
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.0",
4
4
  "private": false,
5
5
  "description": "Local Actor Kernel for Pi",
6
6
  "keywords": [
@@ -30,7 +30,8 @@
30
30
  "test": "node --experimental-strip-types --test 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 audit --audit-level=high && npm run pack:dry",
34
35
  "build": "node scripts/build-dist.mjs",
35
36
  "prepack": "npm run build"
36
37
  },
@@ -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
+ }