@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
@@ -11,6 +11,7 @@ import * as Limits from "./limits.js";
11
11
  import * as Messages from "./messages.js";
12
12
  import * as Paths from "./paths.js";
13
13
  import * as RecipesDiscovery from "./recipes-discovery.js";
14
+ import * as ReviewDiagnostics from "./review-diagnostics.js";
14
15
  import * as Rooms from "./rooms.js";
15
16
  import * as Schema from "./schema.js";
16
17
  import * as ToolsAccess from "./tools-access.js";
@@ -18,6 +19,14 @@ import * as ToolsMailbox from "./tools-mailbox.js";
18
19
  import * as ToolsResponse from "./tools-response.js";
19
20
  const asRecord = ToolsResponse.asRecord;
20
21
  const maybeJsonText = ToolsResponse.maybeJsonText;
22
+ function compactAutomaticReviewDiagnostics(diagnostics) {
23
+ const draft = asRecord(diagnostics.draft_review);
24
+ const tool = asRecord(diagnostics.tool_review);
25
+ return [
26
+ `automatic_reviews draft=${String(draft.phase ?? "idle")} tool=${String(tool.phase ?? "idle")}`,
27
+ `lineages=${String(diagnostics.lineage_count ?? 0)} revision_snapshots=${String(diagnostics.revision_snapshots ?? 0)}`,
28
+ ].join("\n");
29
+ }
21
30
  function compactRunMessages(messages) {
22
31
  if (messages.length === 0)
23
32
  return "\n(no actor messages)";
@@ -222,6 +231,7 @@ function getPiActorsRuntimeStatus() {
222
231
  }
223
232
  const entrypoint = new URL(import.meta.url).pathname;
224
233
  return {
234
+ automatic_recipe_review: Paths.isAutomaticRecipeReviewEnabled(),
225
235
  entrypoint,
226
236
  git_commit,
227
237
  mode: entrypoint.includes("/dist/") ? "dist" : "source",
@@ -233,7 +243,7 @@ function getPiActorsRuntimeStatus() {
233
243
  };
234
244
  }
235
245
  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)}` : ""}`;
246
+ 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
247
  }
238
248
  function isStaleClaim(message, now) {
239
249
  if (message.status !== "claimed")
@@ -436,8 +446,21 @@ export function createInspectToolDefinition(deps = {}) {
436
446
  if (view !== "status" &&
437
447
  view !== "summary" &&
438
448
  view !== "doctor" &&
439
- view !== "imports") {
440
- throw new Error("inspect recipes supports view=status, view=summary, view=doctor, or view=imports.");
449
+ view !== "imports" &&
450
+ view !== "reviews") {
451
+ throw new Error("inspect recipes supports view=status, view=summary, view=doctor, view=imports, or view=reviews.");
452
+ }
453
+ if (view === "reviews") {
454
+ const diagnostics = ReviewDiagnostics.readAutomaticReviewDiagnostics({
455
+ recipeRoot: deps.recipeRoot ?? Paths.getRecipeRoot(),
456
+ });
457
+ return {
458
+ content: [{
459
+ type: "text",
460
+ text: maybeJsonText(diagnostics, input.verbose === true, compactAutomaticReviewDiagnostics(diagnostics)),
461
+ }],
462
+ details: diagnostics,
463
+ };
441
464
  }
442
465
  const discovered = RecipesDiscovery.discoverRecipeSources([
443
466
  {
@@ -110,8 +110,10 @@ export function createRuntimeToolDefinition(cfg, exec) {
110
110
  : Prompts.formatRegisteredToolPromptSnippet(cfg.template),
111
111
  async execute(_toolCallId, params, signal, _onUpdate, ctx) {
112
112
  try {
113
- if (cfg.sourcePath)
114
- RecipesUsage.recordRecipeLaunch(cfg.sourcePath, new Date(), "tool");
113
+ if (cfg.sourcePath &&
114
+ !RecipesUsage.recordRecipeLaunch(cfg.sourcePath, new Date(), "tool")) {
115
+ throw new Error(`Recipe launch rejected because its source changed during activation: ${cfg.sourcePath}. Reload recipe tools and retry.`);
116
+ }
115
117
  if (isAsyncRecipe) {
116
118
  const input = params;
117
119
  const { run_id, ...values } = input;
@@ -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) {
@@ -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
+ }
@@ -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
+ }
@@ -2,7 +2,7 @@
2
2
  name: actors
3
3
  description: Required practical guide for non-trivial pi-actors use, including parallel actor launches, subagent fanout, and autonomous coordinator workflows. Read before using or changing spawn, message, inspect, actor runs, tools, recipes, command templates, async lifecycle, mailboxes, artifacts, and local orchestration mechanics.
4
4
  metadata:
5
- version: 0.41.1
5
+ version: 0.42.0
6
6
  ---
7
7
 
8
8
  # Actors (pi-actors)
@@ -40,7 +40,7 @@ Trusted local capability
40
40
  - **Command template**: portable execution graph. String leaf, sequence array, or object node with controls.
41
41
  - **Recipe**: saved JSON definition wrapping a template with args, defaults, imports, mailbox, artifacts, metadata, and optional `async: true`.
42
42
  - **Run actor**: one detached execution instance addressable as `run:<id>` with status, logs, messages, mailbox metadata, files, and artifacts.
43
- - **Tool actor**: registered persistent capability addressable as `tool:<name>` and callable through the generated tool or `message`. `tool:pi-actors` is reserved for runtime/status inspection.
43
+ - **Tool actor**: registered persistent capability addressable as `tool:<name>` and callable through the generated tool or `message`. `tool:pi-actors` is reserved for runtime status and explicit automatic-review retry/reset control.
44
44
  - **Artifact**: named durable output path declared by a recipe/run.
45
45
  - **Mailbox**: interaction contract: message types the actor accepts/emits.
46
46
  - **Advanced group/coordination surfaces**: `branch:<run>/<branch>`, `room:<run>`, `coordinator`, `session:<id>`, and communication snapshots exist for multi-actor workflows and diagnostics; do not make them the default mental model.
@@ -94,7 +94,8 @@ Envelope fields:
94
94
  - Group posts to `room:<run>` require `from` from the same run (`run:<run>` or `branch:<run>/<branch>`).
95
95
  - Runtime termination message: `control.kill` is the only documented actor message that kills a run. `control.stop` and `control.cancel` are actor-local mailbox vocabulary only when a recipe declares and handles them. Terminal retention messages: `control.archive`, `control.prune`.
96
96
  - Long-lived child processes should remain in the run-owned process group unless the recipe implements an explicit daemon termination bridge; do not leave unowned detached services behind `control.kill`.
97
- - Run controls revalidate a persisted cross-platform process identity proof before delivery or signaling. Treat `dead pid`, `owner mismatch`, and `unsupported proof` as distinct fail-closed states; never bypass them with direct pid signals.
97
+ - Run controls revalidate a persisted cross-platform process identity proof at authorization and again immediately before signaling. Treat `dead pid`, `owner mismatch`, and `unsupported proof` as distinct fail-closed states; on Unix, only process-group `ESRCH` plus one more matching identity check permits exact-pid fallback, while permission/authorization errors remain terminal. Node exposes no portable pidfd/process-group handle, so retain the documented residual exit/reuse window instead of claiming atomic signaling or bypassing control with direct pid signals.
98
+ - Detached actors survive ordinary agent turns. On `session_shutdown` (quit, reload, or session replacement), pi-actors attempts canonical `control.kill` for each discovered readable still-running exact-owner run. Control compares immutable run generation inside the canonical boundary and serializes against same-directory restart; terminal, ambiguous, changed-generation, and other-session runs remain untouched. Teardown scans without the ordinary index depth cap, reports unreadable/corrupt state as failures, and persists a bounded summary under the run root. Descendant Pi sessions remain separate owners and rely on their own shutdown hooks; hard host termination can still leave an orphan requiring summary-guided OS/manual recovery, so never describe teardown as an absolute no-survivor guarantee.
98
99
 
99
100
  Check `inspect view=mailbox` before domain-specific messages.
100
101
 
@@ -216,15 +217,15 @@ Only matching filename ids compete. Higher priority shadows lower priority; with
216
217
  Muscle-memory lens: pi-actors has two durable executable-memory layers.
217
218
 
218
219
  1. `~/.pi/agent/recipes/*.json` and `*.md` are the agent's active capability memory. Every recipe in that directory becomes an easy-to-call tool automatically and survives into later sessions. Descriptions matter here because they become the tool's operator-facing title/context.
219
- 2. `~/.pi/agent/recipes/drafts/*.json` is draft memory captured from successful inline `spawn template=...` runs. Drafts do not enter the injected tool surface. They remain reusable by explicit path, e.g. `spawn file="~/.pi/agent/recipes/drafts/<name>.json"`, and can be promoted by moving or copying one level up into `~/.pi/agent/recipes`.
220
+ 2. `~/.pi/agent/recipes/drafts/*.json` is draft memory captured from successful inline `spawn template=...` runs. Drafts do not enter the injected tool surface and remain reusable by explicit path, e.g. `spawn file="~/.pi/agent/recipes/drafts/<name>.json"`.
220
221
 
221
- Agents grow active memory by calling `register_tool` or by deliberate recipe-file edits. They grow draft memory by trying ad hoc actors successfully. Treat both as executable habits: drafts are the workbench/proving ground; root recipes are promoted muscle memory.
222
+ Agents grow draft memory by trying ad hoc actors successfully. Active memory grows through explicit `register_tool`, deliberate operator recipe edits, or the bounded automatic review cycle. Treat drafts as the workbench and root recipes as active muscle memory.
222
223
 
223
- Usage lens: user recipe launches update extension-maintained `.usage/<recipe-filename>.json` sidecars with fields such as `usage.calls` and `usage.last_called`; authored recipe files are not rewritten for telemetry. Discovery merges the sidecar into inspection. Agents should not hand-edit counters as part of normal recipe maintenance. Treat usage as evidence for usefulness analysis: heavily used recipes are good candidates for promotion, documentation, or stronger tests; unused recipes are cleanup candidates. Do not use failure counts as a primary usefulness signal because failures may reflect bad caller judgment rather than bad recipes. Do not delete or demote solely from counters without operator approval.
224
+ Usage lens: user recipe launches update an extension-maintained canonical name-and-priority lineage ledger under `.usage/recipes/<recipe-name>.json`; authored recipe files are not rewritten for telemetry. Accounting briefly shares the portfolio mutation fence so source quarantine cannot erase an authorized launch; if another session already changed the source, the stale invocation rejects and requests reload rather than executing without evidence. Lifetime calls survive rename, revision, promotion, and demotion, while revision-local calls restart when executable content changes. The bounded unversioned ledger retains former names/paths, revision ancestry, transition events, and review epochs. Discovery merges lineage usage into inspection. Agents should not hand-edit counters; usage remains evidence rather than a sufficient usefulness verdict.
224
225
 
225
- Promotion lens: successful transient/ad hoc actor runs are evidence, not commands. Inline spawns leave draft recipes as replayable evidence, not active tools. If a draft is repeatable, parameterized, safe enough, and likely useful later, the agent may promote it by moving/copying it into `~/.pi/agent/recipes` or by calling `register_tool` with a concise name, typed args/defaults, and a reviewed template or recipe path. Do not auto-register every success; do not promote temp paths, secrets, one-off prompts, or project-private assumptions without normalization and approval.
226
+ Automatic review lens: successful transient/ad hoc actor runs leave replayable drafts rather than active tools. At twelve eligible drafts, pi-actors captures one exact trusted batch and attaches only its identity-opaque value-free structural projection to a silent no-tools reviewer after the foreground turn and active actors finish. Its complete quota-free `promote`/`discard` result contains no recipe content: the deterministic executor derives promotions from exact captured sources and revalidates source/target CAS, complete recipes, root identity, quarantine hashes, and recovery state before commit. Newer drafts remain for a later batch; malformed, stale, unsafe, or incomplete decisions fail closed. Unchanged automatic demotions remain in cooldown until their executable fingerprint changes. Prefer fenced `register_tool draft=...` for an explicit single-draft promotion. A deliberate move/copy into the recipe root also remains valid, but may invalidate and defer an already captured batch; do not reconstruct removed batch commands or ask the operator to drive an automatic batch.
226
227
 
227
- Cleanup rule: periodically inspect `~/.pi/agent/recipes` as the live muscle-memory set. For each stale, duplicate, too-specific, or low-value recipe, choose one explicit action: keep as a tool, move it out of the agent recipe root to retain recipe-only memory, merge into a better recipe, or delete/archive the file. Prefer moving over deletion when the recipe may still be useful as a component. Never silently remove tools during unrelated work.
228
+ Portfolio lens: thirty-six eligible non-sensitive active revisions trigger a no-tools review of an attached value-free structural projection; canonical names, draft basenames, raw hashes, recipe bodies, template/default values, authored prose, and filesystem paths remain in the separate trusted capture; batch-local occurrence IDs and equality-only content groups preserve correlation and deduplication. Set `PI_ACTORS_AUTOMATIC_REVIEW=off` before Pi starts to disable both reviewer scheduling and safe-boundary portfolio activation; verify the effective value with `inspect target=tool:pi-actors view=status`. The reviewer may select keep, unchanged-source rename, unchanged-source demotion, or deduplication of canonically identical captured recipes; it cannot return recipe content. Replacement, split, and executable contract changes require explicit operator authoring. Approval remains immutable until the next safe session boundary, where journaled filesystem and lineage executors apply only the captured recipe bytes. Use `inspect target=recipes view=reviews` for bounded evidence including failed stage/error/next action. Recover a failed cycle through `message to=tool:pi-actors type=review.retry body={"scope":"draft"|"tool"}`. Draft retry resumes an existing authenticated transaction plan and original reviewer run rather than generating decisions after filesystem commit; `review.reset` clears only disposable terminal admission state and rejects tool recovery evidence that must roll forward.
228
229
 
229
230
  ## Registered Tools
230
231
 
@@ -2,7 +2,7 @@
2
2
  name: swarm
3
3
  description: Subagent and actor orchestration with scoped locks, fanout, and quorum consensus. Use before launching multiple parallel actors or subagents for independent implementation, artifact generation, review, delegated audit, coordinated execution, or any workflow that needs autonomous coordinator decomposition and integration.
4
4
  metadata:
5
- version: 0.41.1
5
+ version: 0.42.0
6
6
  ---
7
7
 
8
8
  # Swarm
@@ -1,12 +1,13 @@
1
1
  # Actor Inspector
2
2
 
3
- The actor inspector is a manually opened, read-only TUI navigator for owned actor runs. It keeps communication evidence and persisted subagent execution evidence in one hierarchy without merging their meanings.
3
+ The actor inspector is a manually opened TUI navigator for owned actor runs. Evidence remains read-only; its one explicit lifecycle action can send canonical `control.kill` to the selected running run after confirmation. It keeps recipe/launch identity, communication evidence, and persisted subagent execution evidence in one hierarchy without merging their meanings.
4
4
 
5
5
  ```text
6
6
  owned run
7
+ → recipe
7
8
  → messages | turns
8
9
  → filtered timeline
9
- → bounded detail
10
+ → one bounded detail level
10
11
  ```
11
12
 
12
13
  ## Navigation
@@ -16,18 +17,20 @@ owned run
16
17
  The overlay exposes an explicit focus hierarchy:
17
18
 
18
19
  ```text
19
- Run ←/→ chooses the previous/next owned run, Enter opens runs, ↓ enters tabs
20
- Tabs ←/→ chooses Messages or Turns, Enter opens filter parameters
21
- Filters ↑/↓ chooses Channel/State or Subagent, Enter opens values to the right
20
+ Run ←/→ chooses the previous/next owned run, Enter opens runs, K asks to Kill a running run, ↓ enters tabs
21
+ Tabs ←/→ chooses Recipe, Messages, or Turns
22
+ Recipe ↑/↓ scroll; PageUp/PageDown jumps by viewport; ↑ at top, Escape, or ← returns to tabs
23
+ Filters Enter on Messages/Turns opens Channel/State or Subagent; Enter opens values
22
24
  Values ↑/↓ hovers, Enter applies, Escape returns one menu level
23
- List ↑/↓ chooses, Enter/→ opens detail
24
- Detail ↑/↓ scroll, Enter/→ opens readable transcript, Escape/← returns
25
- Readable ↑/↓ scroll, Escape/← returns to evidence detail
25
+ List ↑/↓ chooses, PageUp/PageDown jumps by viewport, Enter/→ opens detail, ← returns to tabs
26
+ Detail ↑/↓ scroll, PageUp/PageDown jumps by viewport, Escape/← returns to the list
26
27
  Escape Close (or cancel the active options popup)
27
28
  ```
28
29
 
29
30
  Navigation stays bounded by available actions. `↑` on Run does nothing because no higher control exists. `↓` on Tabs enters the timeline only when it contains rows. Empty timelines therefore never receive focus.
30
31
 
32
+ `K` appears only while Run is focused and the selected owned run reports `running`. It opens an in-overlay destructive confirmation; `Y`/Enter confirms and `N`/Escape cancels. Confirmation captures the immutable run generation and routes expected owner/generation through canonical `control.kill`; control compares both while serialized against same-directory restart, so terminal, ownership, or replacement-generation races reject without signaling. Success, cancellation, rejection, and failure remain bounded in the content area; terminal runs expose no Kill hint and reject a stale keypress.
33
+
31
34
  Selection and focus remain separate visual states. Accent-blue text marks the current tab, active filter popup, and applied option. The Run control uses `← … →` markers plus a light neutral background to show both focus and horizontal cycling; menus and timeline rows retain the single `▶` focus marker, while selected tabs retain brackets. Opening a popup keeps its parent filter blue so the relationship remains visible. The footer uses accent color only for key names and arrows; descriptions remain muted.
32
35
 
33
36
  The top Run control aligns vertically with the tab labels, names the selected owned run, and colors its textual lifecycle status semantically. ←/→ cycles owned runs directly with wraparound, while Enter opens the complete owned-run list immediately beneath the control. That run list starts one cell farther left than the filter menus so its border aligns with the Run control rather than the tab/filter grid. It still overlays the tab row rather than leaving a detached gap. The timeline no longer renders run metadata as a data row.
@@ -36,7 +39,13 @@ Filters live behind their tab rather than occupying a permanent row. Non-default
36
39
 
37
40
  Nested menus overlay rather than replace the timeline. Only rows and columns containing menu borders or values occlude underlying cells. When adjacent menus have different heights, the unused corner remains transparent and preserves the separator, striped background, and timeline data beneath it. Every run, filter, and nested value menu is viewport-bounded: ↑/↓ moves through the complete option set, the visible window follows focus, and `↑`/`↓` border markers disclose hidden options above or below without growing past the available inspector rows.
38
41
 
39
- The overlay uses most of the available terminal width and height and reduces its content/menu viewport on shorter terminals. The bordered header keeps both tabs visible, while the list body shows the selected run and its current status above the evidence rows. Run, Message, and Turn lists place the newest retained item directly below their control; a newly opened Inspector therefore selects the latest owned run, and ↓ moves backward in time toward older entries. Run options, Messages, and Turns all use compact descending `#N` labels, providing one timestamp-free time axis without repeating type words on every row. Evidence rows retain stable alternating backgrounds based on their absolute timeline position, including while scrolling: even rows keep the dark overlay background, while odd rows use the neutral `customMessageBg` stripe. Unused viewport padding stays on the plain overlay background instead of drawing fake striped rows beneath the last item. The footer exposes the active keys. Messages retain attention markers and unread filtering and open into bounded detail without leaving the overlay. The overlay refreshes while visible and distinguishes true empty timelines from filtered-empty results; filtered-empty copy points back to Enter on the active tab without moving focus.
42
+ The overlay uses most of the available terminal width and height and reduces its content/menu viewport on shorter terminals. The bordered header keeps all three tabs visible, while the body shows the selected run and its current status above the active document or evidence rows. Run, Message, and Turn lists place the newest retained item directly below their control; a newly opened Inspector therefore selects the latest owned run, and ↓ moves backward in time toward older entries. Run options, Messages, and Turns all use compact descending `#N` labels, providing one timestamp-free time axis without repeating type words on every row. Evidence rows retain stable alternating backgrounds based on their absolute timeline position, including while scrolling: even rows keep the dark overlay background, while odd rows use the neutral `customMessageBg` stripe. Unused viewport padding stays on the plain overlay background instead of drawing fake striped rows beneath the last item. The footer exposes the active keys. Messages retain attention markers and unread filtering and open into bounded detail without leaving the overlay. The overlay refreshes while visible and distinguishes true empty timelines from filtered-empty results; filtered-empty copy points back to Enter on the active tab without moving focus.
43
+
44
+ ## Recipe Document
45
+
46
+ `Recipe` is the first and initially selected tab. It reads only persisted owned-run evidence from `run.json`: recipe identity and source, the authored recipe context captured at launch, the resolved executable template and runtime values, composition records, model policy, mailbox, artifacts, notification/retirement policy, and bounded read diagnostics. It never follows a mutable external recipe path while the Inspector is open.
47
+
48
+ The document renders as labeled, indented terminal text rather than raw JSON and scrolls as one level. Secret-bearing values receive the same redaction as turn evidence. Recipe context lives here rather than repeating inside every Turn.
40
49
 
41
50
  ## Communication Timeline
42
51
 
@@ -64,9 +73,9 @@ Each turn groups:
64
73
  - Tool calls in assistant source order;
65
74
  - Tool results correlated by `toolCallId`, regardless of completion order.
66
75
 
67
- Enter/→ opens the selected turn as structured evidence inside the overlay. A compact `Subagent N` heading with an optional meaningful role leads into meaning-first sections: User, persisted Thinking, Assistant, Tools, Execution, and Diagnostics. A final Provenance section retains session/prompt paths, truncation state, and recipe context without making transport metadata the first screen. Generic internal stages such as `command` and `subagent` stay hidden; technical `command-NNN` provenance remains available through the session and prompt paths without producing a redundant `Command / command-NNN (command)` block. Secondary qualifiers use parentheses rather than centered-dot separators. Long text, paths, and structured values wrap to subsequent terminal rows instead of receiving visual ellipsis; lines that already fit the available inner width remain intact, leading indentation is reserved before wrapping long unbroken paths so it cannot become a whitespace-only row, and every section plus all of its explicit or wrapped continuations keeps one background stripe. Blank-only source lines and trailing line breaks are omitted from both evidence and readable rendering. Section boundaries change the stripe without inserting separator rows, so the next heading follows the previous value immediately. ↑/↓ scrolls the resulting visual-row document while the footer remains visible. Source evidence remains bounded by the persisted session reader, but the detail view no longer truncates that retained evidence to one terminal row per field.
76
+ Enter/→ opens the selected turn as one structured, scrollable detail document inside the overlay. A compact `Subagent N` heading with an optional meaningful role leads into meaning-first sections: User, persisted Thinking, Assistant, Tools, Execution, and Diagnostics. A final Provenance section retains session/prompt paths and truncation state without duplicating recipe context from the Recipe tab. Generic internal stages such as `command` and `subagent` stay hidden; technical `command-NNN` provenance remains available through the session and prompt paths without producing a redundant `Command / command-NNN (command)` block. Secondary qualifiers use parentheses rather than centered-dot separators. Long text, paths, and structured values wrap to subsequent terminal rows instead of receiving visual ellipsis; lines that already fit the available inner width remain intact, leading indentation is reserved before wrapping long unbroken paths so it cannot become a whitespace-only row, and every section plus all of its explicit or wrapped continuations keeps one background stripe. Blank-only source lines and trailing line breaks are omitted from both evidence and readable rendering. Section boundaries change the stripe without inserting separator rows, so the next heading follows the previous value immediately. ↑/↓ scrolls the resulting visual-row document while the footer remains visible. Source evidence remains bounded by the persisted session reader, but the detail view no longer truncates that retained evidence to one terminal row per field.
68
77
 
69
- Enter/→ once more opens a plain readable transcript of the same turn. This second level removes provenance, model, usage, ids, and other evidence metadata, retaining only User, Thinking when persisted, Assistant, Tool input/result, and Error content in execution order. When Pi persisted the user prompt as one `<file name="…">…</file>` transport wrapper, readable mode removes that wrapper and shows only its actual prompt text. Structured values render as indented key/value text rather than one-line JSON. Escape/← returns from transcript to evidence detail, then from evidence detail to the Turns list.
78
+ The detail view removes a single enclosing `<file name="…">…</file>` prompt transport wrapper and renders structured values as indented key/value text rather than one-line JSON. It has no nested transcript mode: Escape/← returns directly to the Turns list.
70
79
 
71
80
  ## Evidence And Privacy Boundary
72
81
 
@@ -246,7 +246,7 @@ Use coordinator/session-bound messages for completion and decision points, not f
246
246
 
247
247
  An async run belongs to the current user, cwd, and launching agent session at start time. Send, cancellation, and force-kill target only the recorded runner pid when command line and cwd still match the recorded owner data. Stale pid reuse must fail closed.
248
248
 
249
- On Unix-like systems, `control.kill` signals the runner process group when available, then falls back to the runner pid. On native Windows, `control.kill` uses Windows process-tree termination through the platform adapter. The runner starts command-template children in the owned process tree, so long-running descendants such as audio players should stop with the run instead of becoming orphaned background processes. A recipe may manage a true detached daemon, but then daemon ownership is recipe-local: the script must persist and verify a pid or service handle, expose status/stop behavior, and bridge `control.kill` to daemon cleanup. The generic runner does not scan for or guess detached services. After the process exits, status reflects the operator action as `killed` instead of a generic `exited`. Programmatic `cancelRun()` remains an internal lifecycle helper for retirement and tests, but it is not a documented actor-message action.
249
+ Immediately before signaling, control revalidates the persisted process identity a second time inside the state-directory lifecycle lock. On Unix-like systems, `control.kill` signals the runner process group when available and falls back to the exact runner pid only when group signaling returns `ESRCH` and one additional identity revalidation still matches; authorization and permission errors fail closed without fallback. On native Windows, `control.kill` uses Windows process-tree termination through the platform adapter. The runner starts command-template children in the owned process tree, so long-running descendants such as audio players should stop with the run instead of becoming orphaned background processes. A recipe may manage a true detached daemon, but then daemon ownership is recipe-local: the script must persist and verify a pid or service handle, expose status/stop behavior, and bridge `control.kill` to daemon cleanup. The generic runner does not scan for or guess detached services. After the process exits, status reflects the operator action as `killed` instead of a generic `exited`. Programmatic `cancelRun()` remains an internal lifecycle helper for retirement and tests, but it is not a documented actor-message action. Node does not expose one portable identity-stable process-group handle across Linux, macOS, and Windows, so a runner can theoretically exit and its PID/PGID can be reused after the final identity read but before the OS signal call. Generation fencing, lifecycle serialization, immediate revalidation, and error-specific fallback minimize this residual platform window; docs and evidence must not claim pidfd/handle-level atomic signaling where the host cannot provide it.
250
250
 
251
251
  State is append-only where practical. Final result writes should be atomic. Recipe-local control endpoints and actor-message logs may live in the state dir. pi-actors core owns the generic run-local message adapter and runtime attention policy; command and message vocabularies belong to the recipe/script.
252
252
 
@@ -270,6 +270,14 @@ Rules:
270
270
  - The `runs` state root is preserved by startup cleanup; run lifecycle cleanup must be explicit and run-aware.
271
271
  - State that must survive restarts belongs in the agent root, not in `tmp`.
272
272
 
273
+ ## Parent Session Teardown
274
+
275
+ Async actors may outlive individual agent turns. Every Pi `session_shutdown` reason (`quit`, `reload`, `new`, `resume`, or `fork`) scans persisted run state and attempts teardown for discovered readable `running` runs whose exact `ownerId` matches the retiring coordinator session. Each new run persists immutable `run_instance_id`; teardown carries expected owner/generation into canonical `control.kill`, which compares both while holding the state-directory lifecycle lock shared with restart. Missing ownership or generation fails closed; terminal, ambiguous, changed-generation, and other-session runs remain untouched. Teardown never signals processes directly.
276
+
277
+ Teardown remains idempotent and best-effort across discovered siblings: one signal, process-proof, or evidence-write failure does not block later candidates. A `run.parent_teardown` event is written only while the selected generation still owns that state directory; replacement generations cannot receive stale teardown evidence. Successful kills retain `run.kill`, terminal progress, process-identity fencing, and handled-marker evidence.
278
+
279
+ This boundary intentionally does not run at ordinary `agent_end`. Teardown uses unbounded directory discovery rather than the ordinary index depth cap. Unreadable directories and corrupt run state become explicit failures, and every invocation persists a bounded summary under `<run-root>/teardown/`; shutdown warnings include that path when failures remain. Actors launched by descendant Pi sessions deliberately remain outside the exact-owner contract and rely on their own session shutdown hook. A hard OS/process kill can still prevent either hook; use the persisted summary plus OS-level/manual recovery for an orphan that a replacement session cannot control safely.
280
+
273
281
  ## Ambient Observability
274
282
 
275
283
  Interactive sessions expose compact activity with minimal screen cost: