@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
@@ -6,11 +6,13 @@
6
6
  import { execFileSync } from "node:child_process";
7
7
  import { existsSync, readFileSync } from "node:fs";
8
8
  import { dirname, join } from "node:path";
9
+ import { fileURLToPath } from "node:url";
9
10
  import * as AsyncRuns from "./async-runs.js";
10
11
  import * as Limits from "./limits.js";
11
12
  import * as Messages from "./messages.js";
12
13
  import * as Paths from "./paths.js";
13
14
  import * as RecipesDiscovery from "./recipes-discovery.js";
15
+ import * as ReviewDiagnostics from "./review-diagnostics.js";
14
16
  import * as Rooms from "./rooms.js";
15
17
  import * as Schema from "./schema.js";
16
18
  import * as ToolsAccess from "./tools-access.js";
@@ -18,6 +20,14 @@ import * as ToolsMailbox from "./tools-mailbox.js";
18
20
  import * as ToolsResponse from "./tools-response.js";
19
21
  const asRecord = ToolsResponse.asRecord;
20
22
  const maybeJsonText = ToolsResponse.maybeJsonText;
23
+ function compactAutomaticReviewDiagnostics(diagnostics) {
24
+ const draft = asRecord(diagnostics.draft_review);
25
+ const tool = asRecord(diagnostics.tool_review);
26
+ return [
27
+ `automatic_reviews draft=${String(draft.phase ?? "idle")} tool=${String(tool.phase ?? "idle")}`,
28
+ `lineages=${String(diagnostics.lineage_count ?? 0)} revision_snapshots=${String(diagnostics.revision_snapshots ?? 0)}`,
29
+ ].join("\n");
30
+ }
21
31
  function compactRunMessages(messages) {
22
32
  if (messages.length === 0)
23
33
  return "\n(no actor messages)";
@@ -220,8 +230,9 @@ function getPiActorsRuntimeStatus() {
220
230
  catch {
221
231
  git_commit = undefined;
222
232
  }
223
- const entrypoint = new URL(import.meta.url).pathname;
233
+ const entrypoint = fileURLToPath(import.meta.url);
224
234
  return {
235
+ automatic_recipe_review: Paths.isAutomaticRecipeReviewEnabled(),
225
236
  entrypoint,
226
237
  git_commit,
227
238
  mode: entrypoint.includes("/dist/") ? "dist" : "source",
@@ -233,7 +244,7 @@ function getPiActorsRuntimeStatus() {
233
244
  };
234
245
  }
235
246
  function compactPiActorsRuntimeStatus(status) {
236
- 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)}` : ""}`;
247
+ 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)}` : ""}`;
237
248
  }
238
249
  function isStaleClaim(message, now) {
239
250
  if (message.status !== "claimed")
@@ -436,8 +447,21 @@ export function createInspectToolDefinition(deps = {}) {
436
447
  if (view !== "status" &&
437
448
  view !== "summary" &&
438
449
  view !== "doctor" &&
439
- view !== "imports") {
440
- throw new Error("inspect recipes supports view=status, view=summary, view=doctor, or view=imports.");
450
+ view !== "imports" &&
451
+ view !== "reviews") {
452
+ throw new Error("inspect recipes supports view=status, view=summary, view=doctor, view=imports, or view=reviews.");
453
+ }
454
+ if (view === "reviews") {
455
+ const diagnostics = ReviewDiagnostics.readAutomaticReviewDiagnostics({
456
+ recipeRoot: deps.recipeRoot ?? Paths.getRecipeRoot(),
457
+ });
458
+ return {
459
+ content: [{
460
+ type: "text",
461
+ text: maybeJsonText(diagnostics, input.verbose === true, compactAutomaticReviewDiagnostics(diagnostics)),
462
+ }],
463
+ details: diagnostics,
464
+ };
441
465
  }
442
466
  const discovered = RecipesDiscovery.discoverRecipeSources([
443
467
  {
@@ -100,6 +100,10 @@ export function createRuntimeToolDefinition(cfg, exec) {
100
100
  }
101
101
  if (isAsyncRecipe)
102
102
  paramSchema.run_id = Schema.stringSchema("Optional run id override for this async template recipe invocation.");
103
+ if (isAsyncRecipe) {
104
+ paramSchema.correlation_id = Schema.stringSchema("Optional workflow correlation id preserved in terminal follow-up delivery.");
105
+ paramSchema.transport_context = Schema.looseObjectSchema("Optional originating transport route preserved for detached terminal follow-up.");
106
+ }
103
107
  return {
104
108
  name: cfg.name,
105
109
  label: cfg.name,
@@ -108,13 +112,15 @@ export function createRuntimeToolDefinition(cfg, exec) {
108
112
  promptSnippet: isRecipe
109
113
  ? Prompts.formatRecipeToolPromptSnippet(cfg.recipe?.name ?? String(cfg.template), isAsyncRecipe)
110
114
  : Prompts.formatRegisteredToolPromptSnippet(cfg.template),
111
- async execute(_toolCallId, params, signal, _onUpdate, ctx) {
115
+ async execute(toolCallId, params, signal, _onUpdate, ctx) {
112
116
  try {
113
- if (cfg.sourcePath)
114
- RecipesUsage.recordRecipeLaunch(cfg.sourcePath, new Date(), "tool");
117
+ if (cfg.sourcePath &&
118
+ !RecipesUsage.recordRecipeLaunch(cfg.sourcePath, new Date(), "tool")) {
119
+ throw new Error(`Recipe launch rejected because its source changed during activation: ${cfg.sourcePath}. Reload recipe tools and retry.`);
120
+ }
115
121
  if (isAsyncRecipe) {
116
122
  const input = params;
117
- const { run_id, ...values } = input;
123
+ const { correlation_id, run_id, transport_context, ...values } = input;
118
124
  const base = cfg.recipe ? cfg.recipe : { file: String(cfg.template) };
119
125
  const runId = typeof run_id === "string" && run_id.trim()
120
126
  ? run_id.trim()
@@ -122,6 +128,17 @@ export function createRuntimeToolDefinition(cfg, exec) {
122
128
  const meta = AsyncRuns.startRun({
123
129
  ...base,
124
130
  launch_source: "tool",
131
+ launch_correlation: {
132
+ ...(typeof correlation_id === "string"
133
+ ? { correlation_id } : {}),
134
+ tool_call_id: toolCallId,
135
+ },
136
+ ...(transport_context &&
137
+ typeof transport_context === "object" &&
138
+ !Array.isArray(transport_context)
139
+ ? {
140
+ transport_context: transport_context,
141
+ } : {}),
125
142
  ownerId: getRunOwnerId(ctx),
126
143
  run_id: runId,
127
144
  tool: cfg.name,
@@ -5,5 +5,6 @@
5
5
  */
6
6
  export interface ActorMessageToolDeps<TContext = unknown> {
7
7
  getTool?: (name: string) => any | undefined;
8
+ handleRuntimeMessage?: (type: string, body: unknown) => Record<string, unknown>;
8
9
  }
9
10
  export declare function createActorMessageToolDefinition<TContext = unknown>(deps?: ActorMessageToolDeps<TContext>): any;
@@ -274,7 +274,14 @@ export function createActorMessageToolDefinition(deps = {}) {
274
274
  ]
275
275
  : [];
276
276
  if (message.type === "control.kill") {
277
- result = AsyncRuns.killRun(address.value);
277
+ result = typeof status.run_instance_id === "string"
278
+ ? AsyncRuns.killRun(address.value, {
279
+ ...(typeof status.ownerId === "string"
280
+ ? { ownerId: status.ownerId }
281
+ : {}),
282
+ runInstanceId: status.run_instance_id,
283
+ })
284
+ : { killed: false, reason: "run generation unavailable" };
278
285
  }
279
286
  else if (message.type === "control.archive") {
280
287
  result = AsyncRuns.archiveRun(address.value);
@@ -344,24 +351,29 @@ export function createActorMessageToolDefinition(deps = {}) {
344
351
  };
345
352
  }
346
353
  else if (address.kind === "tool" && address.value) {
347
- const tool = deps.getTool?.(address.value);
348
- if (!tool || typeof tool.execute !== "function") {
349
- throw new Error(`tool actor not found or not executable: ${address.value}`);
354
+ if (address.value === "pi-actors" && deps.handleRuntimeMessage) {
355
+ result = deps.handleRuntimeMessage(message.type, message.body);
350
356
  }
351
- const toolParams = messageBodyToToolParams(message);
352
- let toolResult;
353
- try {
354
- toolResult = await tool.execute(`message:${message.type}`, toolParams, _signal, _onUpdate, ctx);
355
- }
356
- catch (error) {
357
- throw formatToolActorFailure(address.value, message, toolParams, error);
357
+ else {
358
+ const tool = deps.getTool?.(address.value);
359
+ if (!tool || typeof tool.execute !== "function") {
360
+ throw new Error(`tool actor not found or not executable: ${address.value}`);
361
+ }
362
+ const toolParams = messageBodyToToolParams(message);
363
+ let toolResult;
364
+ try {
365
+ toolResult = await tool.execute(`message:${message.type}`, toolParams, _signal, _onUpdate, ctx);
366
+ }
367
+ catch (error) {
368
+ throw formatToolActorFailure(address.value, message, toolParams, error);
369
+ }
370
+ result = {
371
+ invoked: true,
372
+ sent: true,
373
+ tool: address.value,
374
+ tool_result: toolResult,
375
+ };
358
376
  }
359
- result = {
360
- invoked: true,
361
- sent: true,
362
- tool: address.value,
363
- tool_result: toolResult,
364
- };
365
377
  }
366
378
  else if (address.kind === "coordinator" || address.kind === "session") {
367
379
  if (!message.from) {
@@ -6,10 +6,12 @@
6
6
  import { mkdirSync, writeFileSync } from "node:fs";
7
7
  import { join } from "node:path";
8
8
  import * as AsyncRuns from "./async-runs.js";
9
+ import { withFileMutationLock } from "./file-state.js";
9
10
  import * as Messages from "./messages.js";
10
11
  import * as ModelContext from "./model-context.js";
11
12
  import * as Paths from "./paths.js";
12
13
  import * as RecipesDiscovery from "./recipes-discovery.js";
14
+ import * as RecipesUsage from "./recipes-usage.js";
13
15
  import * as Rooms from "./rooms.js";
14
16
  import * as Schema from "./schema.js";
15
17
  import * as ToolsResponse from "./tools-response.js";
@@ -62,7 +64,8 @@ function writeSpawnDraftRecipe(input, meta) {
62
64
  ...(defaults ? { defaults } : {}),
63
65
  template: input.template,
64
66
  };
65
- writeFileSync(path, `${JSON.stringify(recipe, null, 2)}\n`, { flag: "wx" });
67
+ withFileMutationLock(root, () => withFileMutationLock(path, () => writeFileSync(path, `${JSON.stringify(recipe, null, 2)}\n`, { flag: "wx" })));
68
+ RecipesUsage.recordRecipeLaunch(path, new Date(), "spawn", Paths.getRecipeRoot());
66
69
  return path;
67
70
  }
68
71
  function enhanceSpawnRecipeError(error, recipe) {
@@ -92,6 +95,7 @@ export function createSpawnToolDefinition() {
92
95
  parameters: Schema.objectSchema({
93
96
  artifacts: Schema.looseObjectSchema("Optional named artifact paths for the spawned actor."),
94
97
  as: Schema.stringSchema("Optional actor address for the spawned run, e.g. run:<id>."),
98
+ correlation_id: Schema.stringSchema("Optional workflow correlation id preserved in terminal follow-up delivery."),
95
99
  file: Schema.stringSchema("Optional template recipe JSON file. Bare names resolve under ~/.pi/agent/recipes."),
96
100
  recipe: Schema.stringSchema("Alias for file; template recipe JSON file/name to spawn."),
97
101
  template: Schema.unionSchema([
@@ -100,9 +104,10 @@ export function createSpawnToolDefinition() {
100
104
  Schema.looseObjectSchema("Inline command-template object with flags such as parallel, repeat, retry, failure, and nested template."),
101
105
  ]),
102
106
  values: Schema.looseObjectSchema("Runtime placeholder values passed to the actor."),
107
+ transport_context: Schema.looseObjectSchema("Optional originating transport route preserved for detached terminal follow-up, e.g. Telegram chat_id and thread_id."),
103
108
  verbose: Schema.booleanSchema("Return full JSON instead of compact text."),
104
109
  }, []),
105
- async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
110
+ async execute(toolCallId, params, _signal, _onUpdate, ctx) {
106
111
  const input = asRecord(params);
107
112
  if (input.state_dir !== undefined) {
108
113
  throw new Error("spawn.state_dir is not supported; run state is runtime-owned so run:<id> remains addressable and retention-safe.");
@@ -118,7 +123,14 @@ export function createSpawnToolDefinition() {
118
123
  meta = AsyncRuns.startRun({
119
124
  file: recipe,
120
125
  launch_source: "spawn",
126
+ launch_correlation: {
127
+ ...(typeof input.correlation_id === "string"
128
+ ? { correlation_id: input.correlation_id } : {}),
129
+ tool_call_id: toolCallId,
130
+ },
121
131
  ownerId: getRunOwnerId(ctx),
132
+ ...(input.transport_context
133
+ ? { transport_context: asRecord(input.transport_context) } : {}),
122
134
  run_id: runId,
123
135
  ...(input.template !== undefined
124
136
  ? {
@@ -15,6 +15,7 @@ export interface CoreActorToolDefinitionDeps<TContext extends RuntimeToolContext
15
15
  configPath: string;
16
16
  getActiveTools: () => string[];
17
17
  getRuntimeTool: (name: string) => unknown;
18
+ handleRuntimeMessage?: (type: string, body: unknown) => Record<string, unknown>;
18
19
  registryRuntime: Pick<RegisterToolRuntimeDeps<TContext>, "getToolNameBlocker" | "getTools" | "notify" | "registerRuntimeTool">;
19
20
  setActiveTools: (toolNames: string[]) => void;
20
21
  }
package/dist/lib/tools.js CHANGED
@@ -38,6 +38,7 @@ export function createCoreActorToolDefinitions(deps) {
38
38
  ToolsSpawn.createSpawnToolDefinition(),
39
39
  ToolsMessage.createActorMessageToolDefinition({
40
40
  getTool: (name) => deps.getRuntimeTool(name),
41
+ handleRuntimeMessage: deps.handleRuntimeMessage,
41
42
  }),
42
43
  ToolsInspect.createInspectToolDefinition({
43
44
  getTool: (name) => deps.getRuntimeTool(name),
@@ -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
+ }
@@ -135,7 +135,7 @@ export async function runAsyncRunner(stateDir = process.argv[2]) {
135
135
  try {
136
136
  return readdirSync(sessionDir, { withFileTypes: true })
137
137
  .filter((entry) => entry.isFile() && entry.name.endsWith(".jsonl"))
138
- .map((entry) => relative(stateDir, join(sessionDir, entry.name)))
138
+ .map((entry) => relative(stateDir, join(sessionDir, entry.name)).replaceAll("\\", "/"))
139
139
  .sort();
140
140
  } catch {
141
141
  return [];
@@ -167,12 +167,12 @@ export async function runAsyncRunner(stateDir = process.argv[2]) {
167
167
  ...(recipeContext ? { recipe_context: recipeContext } : {}),
168
168
  command: commandDetail,
169
169
  ...(materialized.promptFile
170
- ? { prompt_file: relative(stateDir, materialized.promptFile) }
170
+ ? { prompt_file: relative(stateDir, materialized.promptFile).replaceAll("\\", "/") }
171
171
  : {}),
172
172
  ...(materialized.promptBytes
173
173
  ? { prompt_bytes: materialized.promptBytes }
174
174
  : {}),
175
- ...(sessionDir ? { session_dir: relative(stateDir, sessionDir) } : {}),
175
+ ...(sessionDir ? { session_dir: relative(stateDir, sessionDir).replaceAll("\\", "/") } : {}),
176
176
  attempts: [],
177
177
  semantic_acceptance:
178
178
  options?.evidenceContext?.acceptOutput === "review_evidence" ||
@@ -209,11 +209,11 @@ export async function runAsyncRunner(stateDir = process.argv[2]) {
209
209
  attempts.push({
210
210
  attempt,
211
211
  stdout: {
212
- path: relative(stateDir, stdoutFile),
212
+ path: relative(stateDir, stdoutFile).replaceAll("\\", "/"),
213
213
  bytes: existsSync(stdoutFile) ? statSync(stdoutFile).size : 0,
214
214
  },
215
215
  stderr: {
216
- path: relative(stateDir, stderrFile),
216
+ path: relative(stateDir, stderrFile).replaceAll("\\", "/"),
217
217
  bytes: existsSync(stderrFile) ? statSync(stderrFile).size : 0,
218
218
  },
219
219
  });
@@ -248,12 +248,12 @@ export async function runAsyncRunner(stateDir = process.argv[2]) {
248
248
  ...(recipeContext ? { recipe_context: recipeContext } : {}),
249
249
  command: commandDetail,
250
250
  ...(materialized.promptFile
251
- ? { prompt_file: relative(stateDir, materialized.promptFile) }
251
+ ? { prompt_file: relative(stateDir, materialized.promptFile).replaceAll("\\", "/") }
252
252
  : {}),
253
253
  ...(materialized.promptBytes
254
254
  ? { prompt_bytes: materialized.promptBytes }
255
255
  : {}),
256
- ...(sessionDir ? { session_dir: relative(stateDir, sessionDir) } : {}),
256
+ ...(sessionDir ? { session_dir: relative(stateDir, sessionDir).replaceAll("\\", "/") } : {}),
257
257
  ...(commandSessionFiles(sessionDir).length > 0
258
258
  ? { session_files: commandSessionFiles(sessionDir) }
259
259
  : {}),
@@ -386,7 +386,7 @@ export async function runAsyncRunner(stateDir = process.argv[2]) {
386
386
  command: commandDetail,
387
387
  ...(materialized.promptFile ? { prompt_file: materialized.promptFile } : {}),
388
388
  ...(materialized.promptBytes ? { prompt_bytes: materialized.promptBytes } : {}),
389
- ...(session.sessionDir ? { session_dir: relative(stateDir, session.sessionDir) } : {}),
389
+ ...(session.sessionDir ? { session_dir: relative(stateDir, session.sessionDir).replaceAll("\\", "/") } : {}),
390
390
  });
391
391
  progressRunning();
392
392
  const captureDir = join(stateDir, "captures", commandId);
@@ -457,7 +457,7 @@ export async function runAsyncRunner(stateDir = process.argv[2]) {
457
457
  ...captureDetails(result),
458
458
  ...(materialized.promptFile ? { prompt_file: materialized.promptFile } : {}),
459
459
  ...(materialized.promptBytes ? { prompt_bytes: materialized.promptBytes } : {}),
460
- ...(session.sessionDir ? { session_dir: relative(stateDir, session.sessionDir) } : {}),
460
+ ...(session.sessionDir ? { session_dir: relative(stateDir, session.sessionDir).replaceAll("\\", "/") } : {}),
461
461
  ...(commandSessionFiles(session.sessionDir).length > 0
462
462
  ? { session_files: commandSessionFiles(session.sessionDir) }
463
463
  : {}),
@@ -20,13 +20,18 @@ import { join } from "node:path";
20
20
 
21
21
  function run(command, args) {
22
22
  const result = spawnSync(command, args, { stdio: "inherit" });
23
+ if (result.error) throw result.error;
23
24
  if (result.status !== 0) process.exit(result.status ?? 1);
24
25
  }
25
26
 
26
27
  rmSync("dist", { recursive: true, force: true });
27
28
  mkdirSync("dist", { recursive: true });
28
29
 
29
- run("tsc", ["-p", "tsconfig.build.json"]);
30
+ run(process.execPath, [
31
+ join("node_modules", "typescript", "bin", "tsc"),
32
+ "-p",
33
+ "tsconfig.build.json",
34
+ ]);
30
35
 
31
36
  mkdirSync(join("dist", "pi-actors"), { recursive: true });
32
37
  writeFileSync(
@@ -28,7 +28,12 @@ function packageRoot() {
28
28
 
29
29
  const result = spawnSync(
30
30
  process.execPath,
31
- ["--experimental-strip-types", "--test", ...conformanceSuites],
31
+ [
32
+ "--experimental-strip-types",
33
+ "--test",
34
+ "--test-concurrency=1",
35
+ ...conformanceSuites,
36
+ ],
32
37
  { cwd: packageRoot(), encoding: "utf8", stdio: "pipe" },
33
38
  );
34
39
 
@@ -16,7 +16,7 @@ import {
16
16
  statSync,
17
17
  writeFileSync,
18
18
  } from "node:fs";
19
- import { dirname, extname, join, relative, resolve } from "node:path";
19
+ import { basename, dirname, extname, join, relative, resolve, sep } from "node:path";
20
20
 
21
21
  function usage() {
22
22
  console.error(`Usage:
@@ -101,7 +101,7 @@ function collectRunSummary(rootValue) {
101
101
  const root = resolve(
102
102
  rootValue.replace(/^~(?=\/|$)/, process.env.HOME ?? "~"),
103
103
  );
104
- const files = walkFiles(root, 2).filter((file) => file.endsWith("/run.json"));
104
+ const files = walkFiles(root, 2).filter((file) => basename(file) === "run.json");
105
105
  const rows = [];
106
106
  for (const file of files) {
107
107
  const run = readJson(file);
@@ -118,7 +118,7 @@ function collectRunSummary(rootValue) {
118
118
  const progress = readJson(join(runDir, "progress.json"));
119
119
  const result = readJson(join(runDir, "result.json"));
120
120
  rows.push({
121
- run: run.run_id ?? run.run ?? relative(root, file).split("/")[0],
121
+ run: run.run_id ?? run.run ?? relative(root, file).split(sep)[0],
122
122
  status: getRunStatus(run, progress, result),
123
123
  recipe: run.recipe ?? run.recipe_file ?? "",
124
124
  updated:
@@ -0,0 +1,165 @@
1
+ #!/usr/bin/env node
2
+ /** Reproducible exact-tree, Domain DAG, secret-hygiene, and ABCd release gates. */
3
+
4
+ import { mkdtempSync, rmSync } from "node:fs";
5
+ import { tmpdir } from "node:os";
6
+ import { dirname, extname, join, normalize, relative, resolve } from "node:path";
7
+ import { spawnSync } from "node:child_process";
8
+ import { fileURLToPath } from "node:url";
9
+
10
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
11
+ const temp = mkdtempSync(join(tmpdir(), "pi-actors-release-index-"));
12
+ const indexPath = join(temp, "index");
13
+ const gitEnv = { ...process.env, GIT_INDEX_FILE: indexPath };
14
+ const failures = [];
15
+
16
+ function run(command, args, options = {}) {
17
+ const result = spawnSync(command, args, {
18
+ cwd: root,
19
+ encoding: "utf8",
20
+ maxBuffer: 32 * 1024 * 1024,
21
+ ...options,
22
+ });
23
+ if (result.status !== 0) {
24
+ throw new Error(
25
+ `${command} ${args.join(" ")} failed (${result.status}): ${(result.stderr || result.stdout || "no output").trim()}`,
26
+ );
27
+ }
28
+ return result.stdout;
29
+ }
30
+
31
+ function check(condition, message) {
32
+ if (!condition) failures.push(message);
33
+ }
34
+
35
+ function stagedText(path) {
36
+ const result = spawnSync("git", ["show", `:${path}`], {
37
+ cwd: root,
38
+ env: gitEnv,
39
+ encoding: "buffer",
40
+ maxBuffer: 16 * 1024 * 1024,
41
+ });
42
+ if (result.status !== 0) {
43
+ failures.push(`could not read staged file: ${path}`);
44
+ return undefined;
45
+ }
46
+ const bytes = result.stdout;
47
+ if (bytes.includes(0)) return undefined;
48
+ return bytes.toString("utf8");
49
+ }
50
+
51
+ function sourceImports(source) {
52
+ return [...source.matchAll(/^import(?:\s+type)?\s+(?:[^"']+?\s+from\s+)?["']([^"']+)["'];?$/gmu)]
53
+ .map((match) => match[1]);
54
+ }
55
+
56
+ function resolveLocalImport(from, specifier, sourceSet) {
57
+ if (!specifier.startsWith(".")) return undefined;
58
+ const base = normalize(join(dirname(from), specifier)).replaceAll("\\", "/");
59
+ const candidates = extname(base)
60
+ ? [base.replace(/\.js$/u, ".ts")]
61
+ : [`${base}.ts`, `${base}/index.ts`];
62
+ return candidates.find((candidate) => sourceSet.has(candidate));
63
+ }
64
+
65
+ try {
66
+ run("git", ["read-tree", "HEAD"], { env: gitEnv });
67
+ run("git", ["add", "-A"], { env: gitEnv });
68
+ run("git", ["diff", "--cached", "--check"], { env: gitEnv });
69
+ const files = run("git", ["ls-files", "-z"], { env: gitEnv })
70
+ .split("\0")
71
+ .filter(Boolean);
72
+ const fileSet = new Set(files);
73
+ console.log(`[release] exact temporary index: ${files.length} files`);
74
+
75
+ const secretPatterns = [
76
+ [/-----BEGIN(?: [A-Z0-9]+)? PRIVATE KEY-----[\s\S]{80,}?-----END(?: [A-Z0-9]+)? PRIVATE KEY-----/u, "private-key block"],
77
+ [/\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/u, "AWS access key"],
78
+ [/\bgithub_pat_[A-Za-z0-9_]{20,}\b|\bgh[pousr]_[A-Za-z0-9]{20,}\b/u, "GitHub token"],
79
+ [/\bglpat-[A-Za-z0-9_-]{20,}\b|\bnpm_[A-Za-z0-9]{20,}\b|\bxox[baprs]-[A-Za-z0-9-]{10,}\b/u, "service token"],
80
+ ];
81
+ const publicPath = /^(?:README\.md|AGENTS\.md|BACKLOG\.md|CHANGELOG\.md|docs\/|skills\/|recipes\/)/u;
82
+ for (const path of files) {
83
+ const text = stagedText(path);
84
+ if (text === undefined) continue;
85
+ if (!path.startsWith("tests/") && !path.startsWith("fixtures/")) {
86
+ for (const [pattern, label] of secretPatterns) {
87
+ check(!pattern.test(text), `${label} detected in ${path}`);
88
+ }
89
+ }
90
+ if (publicPath.test(path)) {
91
+ check(!/(?:^|[\s"'`])\/home\/[A-Za-z0-9._-]+\//u.test(text), `machine-local path in ${path}`);
92
+ }
93
+ }
94
+ console.log("[release] bounded secret and public-path hygiene checked");
95
+
96
+ const sources = files.filter((path) => path === "index.ts" || (path.startsWith("lib/") && path.endsWith(".ts")));
97
+ const sourceSet = new Set(sources);
98
+ const graph = new Map();
99
+ for (const path of sources) {
100
+ const text = stagedText(path) ?? "";
101
+ const headerEnd = text.indexOf("*/");
102
+ const header = headerEnd >= 0 ? text.slice(0, headerEnd + 2) : "";
103
+ check(
104
+ header.startsWith("/**") && (header.includes("Zones:") || header.includes("Owns:")),
105
+ `missing domain header: ${path}`,
106
+ );
107
+ const edges = sourceImports(text)
108
+ .map((specifier) => resolveLocalImport(path, specifier, sourceSet))
109
+ .filter(Boolean);
110
+ graph.set(path, edges);
111
+ if (path.startsWith("lib/")) {
112
+ check(!edges.includes("index.ts"), `domain imports entrypoint: ${path}`);
113
+ }
114
+ }
115
+ const visiting = new Set();
116
+ const visited = new Set();
117
+ function visit(path, stack = []) {
118
+ if (visiting.has(path)) {
119
+ failures.push(`Domain DAG cycle: ${[...stack, path].join(" -> ")}`);
120
+ return;
121
+ }
122
+ if (visited.has(path)) return;
123
+ visiting.add(path);
124
+ for (const next of graph.get(path) ?? []) visit(next, [...stack, path]);
125
+ visiting.delete(path);
126
+ visited.add(path);
127
+ }
128
+ for (const path of sources) visit(path);
129
+ console.log(`[release] strict Domain DAG: ${sources.length} sources, acyclic`);
130
+
131
+ for (const path of ["README.md", "AGENTS.md", "BACKLOG.md", "CHANGELOG.md", "docs/README.md"]) {
132
+ check(fileSet.has(path), `missing ABCd root/context file: ${path}`);
133
+ }
134
+ const readStaged = (path) => stagedText(path) ?? "";
135
+ const readme = readStaged("README.md");
136
+ for (const path of ["AGENTS.md", "BACKLOG.md", "CHANGELOG.md", "docs/README.md"]) {
137
+ check(readme.includes(path), `README.md does not route to ${path}`);
138
+ }
139
+ for (const path of files.filter((candidate) => candidate.endsWith(".md"))) {
140
+ const text = readStaged(path);
141
+ for (const match of text.matchAll(/\[[^\]]*\]\(([^)]+)\)/gu)) {
142
+ const target = match[1].split("#", 1)[0];
143
+ if (!target || /^(?:https?:|mailto:)/u.test(target)) continue;
144
+ const resolved = normalize(join(dirname(path), decodeURIComponent(target))).replaceAll("\\", "/");
145
+ check(fileSet.has(resolved), `broken Markdown link: ${path} -> ${target}`);
146
+ }
147
+ }
148
+ const docsIndex = readStaged("docs/README.md");
149
+ for (const path of files.filter((candidate) => candidate.startsWith("docs/") && candidate.endsWith(".md") && candidate !== "docs/README.md")) {
150
+ check(docsIndex.includes(relative("docs", path).replaceAll("\\", "/")), `docs/README.md omits ${path}`);
151
+ }
152
+ console.log("[release] ABCd context roots, routing, and Markdown links checked");
153
+
154
+ if (failures.length > 0) {
155
+ for (const failure of failures) console.error(`[FAIL] ${failure}`);
156
+ process.exitCode = 1;
157
+ } else {
158
+ console.log("[release] all supplemental release gates passed");
159
+ }
160
+ } catch (error) {
161
+ console.error(`[FAIL] ${error instanceof Error ? error.message : String(error)}`);
162
+ process.exitCode = 1;
163
+ } finally {
164
+ rmSync(temp, { recursive: true, force: true });
165
+ }