@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
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * Command-template async run lifecycle facade.
3
- * Owns launch, state observation, listing, message/control facade methods, and retention while runs-* subdomains own narrower run internals.
3
+ * Owns: launch, state observation, listing, message/control facade methods, and retention while runs-* subdomains own narrower run internals.
4
4
  */
5
5
  import { spawn } from "node:child_process";
6
+ import { randomUUID } from "node:crypto";
6
7
  import { closeSync, existsSync, mkdirSync, openSync, readdirSync, statSync, writeFileSync, } from "node:fs";
7
8
  import { basename, dirname, extname, join, relative, resolve } from "node:path";
8
9
  import { fileURLToPath } from "node:url";
@@ -20,9 +21,10 @@ import { archiveTerminalRun, pruneTerminalRun } from "./runs-retention.js";
20
21
  import { captureRunProcessIdentity, verifyRunProcessIdentity, } from "./runs-process.js";
21
22
  import * as RunsStart from "./runs-start.js";
22
23
  import * as RunsIndex from "./runs-index.js";
24
+ import * as RunsParentTeardown from "./runs-parent-teardown.js";
23
25
  import { claimRunInboxMessageInStateDir, parseRunInboxLine, processRunInboxMessagesInStateDir, runInboxFile, updateRunInboxMessageStatusInStateDir, } from "./runs-mailbox.js";
24
26
  import { deliverRunMessage, } from "./runs-messages.js";
25
- import { buildRunStatus, tailFile, tailLines } from "./runs-status.js";
27
+ import { buildRunStatus, tailFile, tailLines, } from "./runs-status.js";
26
28
  import { readJsonFileResilient } from "./state-readers.js";
27
29
  const RUNNER_IDENTITY_GRACE_MS = 5000;
28
30
  const DEFAULT_STATE_ROOT = Paths.getRunStateRoot();
@@ -201,8 +203,8 @@ function assertCurrentPlaceholderReferencesResolved(template, defaults, values,
201
203
  throw Object.assign(new Error(`Template recipe requires the current Pi ${check.label} for inheritance, but no current ${check.label} was available. Pass explicit values or launch from a Pi session with a selected ${check.label}. unresolved=${refs.slice(0, 4).join(",")}`), { model_policy: modelPolicy });
202
204
  }
203
205
  }
204
- function acquireStateStartLock(stateDir) {
205
- return RunsStart.acquireStateStartLock(stateDir);
206
+ function acquireStateStartLock(stateDir, options = {}) {
207
+ return RunsStart.acquireStateStartLock(stateDir, options);
206
208
  }
207
209
  function prepareStateDirForStart(stateDir) {
208
210
  RunsStart.prepareStateDirForStart(stateDir, readJson, RUNNER_PATH);
@@ -228,7 +230,9 @@ export function startRun(params, cwd) {
228
230
  assertCurrentPlaceholderReferencesResolved(resolved.template, startParams.defaults, values, modelPolicy);
229
231
  assertNoActiveRunState(stateDir);
230
232
  mkdirSync(stateDir, { recursive: true });
231
- const releaseStartLock = acquireStateStartLock(stateDir);
233
+ const releaseStartLock = acquireStateStartLock(stateDir, {
234
+ onContention: startParams.lifecycleHooks?.onLockContention,
235
+ });
232
236
  try {
233
237
  claimRunStateDirectory(stateDir, run);
234
238
  assertNoActiveRunState(stateDir);
@@ -244,8 +248,10 @@ export function startRun(params, cwd) {
244
248
  const recipeContextRecords = recipeFile && includeActorRecipeContext
245
249
  ? RecipesReferences.buildRecipeContextRecords(recipeFile)
246
250
  : undefined;
247
- if (recipeFile && isMutableUsageRecipeFile(recipeFile)) {
248
- RecipesUsage.recordRecipeLaunch(recipeFile, new Date(), startParams.launch_source === "tool" ? "tool" : "spawn");
251
+ if (recipeFile &&
252
+ isMutableUsageRecipeFile(recipeFile) &&
253
+ !RecipesUsage.recordRecipeLaunch(recipeFile, new Date(), startParams.launch_source === "tool" ? "tool" : "spawn")) {
254
+ throw new Error(`Recipe launch rejected because its source changed during activation: ${recipeFile}. Reload recipe tools and retry.`);
249
255
  }
250
256
  const outFd = openSync(stdout, "a");
251
257
  const errFd = openSync(stderr, "a");
@@ -267,6 +273,7 @@ export function startRun(params, cwd) {
267
273
  ...(recipe ? { recipe } : {}),
268
274
  ...(recipeFile ? { recipe_file: recipeFile } : {}),
269
275
  run,
276
+ run_instance_id: randomUUID(),
270
277
  state_dir: stateDir,
271
278
  status: "running",
272
279
  ...(startParams.tool ? { tool: startParams.tool } : {}),
@@ -276,6 +283,9 @@ export function startRun(params, cwd) {
276
283
  ...(artifacts ? { artifacts } : {}),
277
284
  ...(startParams.control ? { control: startParams.control } : {}),
278
285
  ...(startParams.mailbox ? { mailbox: startParams.mailbox } : {}),
286
+ ...(startParams.notification_policy === "silent"
287
+ ? { notification_policy: "silent" }
288
+ : {}),
279
289
  ...(recipeContextRecords && recipeContextRecords.length > 0
280
290
  ? { recipe_context_records: recipeContextRecords }
281
291
  : {}),
@@ -303,7 +313,7 @@ export function startRun(params, cwd) {
303
313
  phase: "starting",
304
314
  updatedAt: new Date().toISOString(),
305
315
  });
306
- writeFileSync(join(stateDir, "events.jsonl"), `${JSON.stringify({ event: "run.start", run, pid: meta.pid, ts: new Date().toISOString() })}\n`, { flag: "a" });
316
+ writeFileSync(join(stateDir, "events.jsonl"), `${JSON.stringify({ event: "run.start", run, run_instance_id: meta.run_instance_id, pid: meta.pid, ts: new Date().toISOString() })}\n`, { flag: "a" });
307
317
  child.unref();
308
318
  return meta;
309
319
  }
@@ -312,17 +322,20 @@ export function startRun(params, cwd) {
312
322
  }
313
323
  }
314
324
  export { parseRunOutboxEventLine } from "./runs-outbox.js";
315
- export function getRunStatus(runOrDir) {
316
- const stateDir = resolve(runOrDir.includes("/")
325
+ function resolveRunStateDir(runOrDir) {
326
+ return resolve(runOrDir.includes("/")
317
327
  ? runOrDir
318
328
  : join(DEFAULT_STATE_ROOT, safeRunId(runOrDir)));
329
+ }
330
+ export function getRunStatus(runOrDir) {
331
+ const stateDir = resolveRunStateDir(runOrDir);
319
332
  const meta = readJson(join(stateDir, "run.json"));
320
333
  if (!meta)
321
334
  throw new Error(`Run not found: ${runOrDir}`);
322
335
  return buildRunStatus(stateDir, runOrDir, meta, readJson, RUNNER_PATH, RUNNER_IDENTITY_GRACE_MS);
323
336
  }
324
- export function listRunStateDirs(stateRoot = DEFAULT_STATE_ROOT, depth = 0, seen = new Set()) {
325
- return RunsIndex.listRunStateDirs(stateRoot, depth, seen);
337
+ export function listRunStateDirs(stateRoot = DEFAULT_STATE_ROOT) {
338
+ return RunsIndex.listRunStateDirs(stateRoot);
326
339
  }
327
340
  export function rebuildRunStateIndex(stateRoot = DEFAULT_STATE_ROOT) {
328
341
  return RunsIndex.rebuildRunStateIndex(stateRoot, getRunStatus);
@@ -333,6 +346,67 @@ export function readRunStateIndex(stateRoot = DEFAULT_STATE_ROOT) {
333
346
  export function listRuns(stateRoot = DEFAULT_STATE_ROOT, statusFilter) {
334
347
  return RunsIndex.listRuns(stateRoot, getRunStatus, readJson, statusFilter);
335
348
  }
349
+ export function teardownRunsOwnedByParent(ownerId, stateRoot = DEFAULT_STATE_ROOT, options = {}) {
350
+ const result = RunsParentTeardown.teardownParentRuns(ownerId, {
351
+ getRunStatus,
352
+ killRun,
353
+ listRunStatuses: () => {
354
+ const discovered = RunsIndex.discoverRunStateDirs(stateRoot, Number.POSITIVE_INFINITY);
355
+ const failures = discovered.issues.map((issue) => ({
356
+ path: issue.path,
357
+ reason: issue.reason.replaceAll("_", " "),
358
+ }));
359
+ const statuses = discovered.stateDirs.flatMap((stateDir) => {
360
+ try {
361
+ return [getRunStatus(stateDir)];
362
+ }
363
+ catch (error) {
364
+ failures.push({
365
+ path: stateDir,
366
+ reason: error instanceof Error ? error.message : String(error),
367
+ });
368
+ return [];
369
+ }
370
+ });
371
+ return { failures, statuses };
372
+ },
373
+ recordAttempt: (attempt) => {
374
+ const current = getRunStatus(attempt.stateDir);
375
+ if (!attempt.runInstanceId ||
376
+ current.run_instance_id !== attempt.runInstanceId) {
377
+ throw new Error("run generation changed before teardown evidence");
378
+ }
379
+ writeFileSync(join(attempt.stateDir, "events.jsonl"), `${JSON.stringify({
380
+ event: "run.parent_teardown",
381
+ ownerId: attempt.ownerId,
382
+ outcome: attempt.outcome,
383
+ reason: attempt.reason,
384
+ run: attempt.run,
385
+ run_instance_id: attempt.runInstanceId,
386
+ type: "control.kill",
387
+ trigger: options.trigger ?? "parent_shutdown",
388
+ ts: new Date().toISOString(),
389
+ })}\n`, { flag: "a" });
390
+ },
391
+ });
392
+ const summaryPath = join(stateRoot, "teardown", `${Date.now()}-${randomUUID()}.json`);
393
+ try {
394
+ writeJsonAtomic(summaryPath, RunsParentTeardown.buildBoundedParentTeardownSummary(result, ownerId ?? "unknown", options.trigger ?? "parent_shutdown", new Date().toISOString()));
395
+ return { ...result, summaryPath };
396
+ }
397
+ catch (error) {
398
+ const failure = {
399
+ path: summaryPath,
400
+ reason: `summary: ${error instanceof Error ? error.message : String(error)}`,
401
+ };
402
+ return {
403
+ ...result,
404
+ discoveryFailed: result.discoveryFailed + 1,
405
+ discoveryFailures: [...result.discoveryFailures, failure],
406
+ failed: result.failed + 1,
407
+ };
408
+ }
409
+ }
336
410
  export function tailRun(runOrDir, lines = 40) {
337
411
  const status = getRunStatus(runOrDir);
338
412
  const stateDir = String(status.state_dir);
@@ -470,17 +544,38 @@ function finalizeInterruptedReviewEvidence(stateDir, phase, signal) {
470
544
  updated_at: completedAt,
471
545
  });
472
546
  }
473
- function stopRun(runOrDir, signal, event) {
474
- const status = getRunStatus(runOrDir);
475
- const pid = Number(status.pid || 0);
476
- const stateDir = String(status.state_dir);
477
- if (status.status !== "running" && status.status !== "exited") {
478
- return { stopped: false, reason: "not running", status };
479
- }
480
- const identity = verifyRunProcessIdentity(pid, status.process_identity);
481
- if (status.status === "exited") {
482
- if (identity.status === "owner_mismatch" ||
483
- identity.status === "unsupported_proof") {
547
+ function stopRun(runOrDir, signal, event, expected = {}) {
548
+ const stateDir = resolveRunStateDir(runOrDir);
549
+ const releaseControlLock = RunsStart.acquireStateStartLock(stateDir);
550
+ try {
551
+ expected.onLocked?.();
552
+ const status = getRunStatus(stateDir);
553
+ if (expected.ownerId !== undefined &&
554
+ status.ownerId !== expected.ownerId) {
555
+ return { stopped: false, reason: "ownership changed", status };
556
+ }
557
+ if (expected.runInstanceId !== undefined &&
558
+ status.run_instance_id !== expected.runInstanceId) {
559
+ return { stopped: false, reason: "run generation changed", status };
560
+ }
561
+ const pid = Number(status.pid || 0);
562
+ if (status.status !== "running" && status.status !== "exited") {
563
+ return { stopped: false, reason: "not running", status };
564
+ }
565
+ const identity = verifyRunProcessIdentity(pid, status.process_identity);
566
+ if (status.status === "exited") {
567
+ if (identity.status === "owner_mismatch" ||
568
+ identity.status === "unsupported_proof") {
569
+ return {
570
+ stopped: false,
571
+ reason: identity.status.replaceAll("_", " "),
572
+ process_identity_status: identity.status,
573
+ status,
574
+ };
575
+ }
576
+ return { stopped: false, reason: "not running", status };
577
+ }
578
+ if (!identity.valid) {
484
579
  return {
485
580
  stopped: false,
486
581
  reason: identity.status.replaceAll("_", " "),
@@ -488,28 +583,22 @@ function stopRun(runOrDir, signal, event) {
488
583
  status,
489
584
  };
490
585
  }
491
- return { stopped: false, reason: "not running", status };
492
- }
493
- if (!identity.valid) {
494
- return {
495
- stopped: false,
496
- reason: identity.status.replaceAll("_", " "),
497
- process_identity_status: identity.status,
498
- status,
499
- };
500
- }
501
- const signalResult = signalOwnedRunProcess(pid, signal);
502
- writeFileSync(join(stateDir, "events.jsonl"), `${JSON.stringify({ event, pid, signal, ...signalResult, ts: new Date().toISOString() })}\n`, { flag: "a" });
503
- markTerminalHandled(stateDir, { event, signal });
504
- if (event === "run.kill") {
505
- finalizeInterruptedReviewEvidence(stateDir, "killed", signal);
506
- markTerminalProgress(stateDir, "killed");
586
+ const signalResult = signalOwnedRunProcess(pid, signal, status.process_identity);
587
+ writeFileSync(join(stateDir, "events.jsonl"), `${JSON.stringify({ event, pid, signal, ...signalResult, ts: new Date().toISOString() })}\n`, { flag: "a" });
588
+ markTerminalHandled(stateDir, { event, signal });
589
+ if (event === "run.kill") {
590
+ finalizeInterruptedReviewEvidence(stateDir, "killed", signal);
591
+ markTerminalProgress(stateDir, "killed");
592
+ }
593
+ if (event === "run.cancel") {
594
+ finalizeInterruptedReviewEvidence(stateDir, "cancelled", signal);
595
+ markTerminalProgress(stateDir, "cancelled");
596
+ }
597
+ return { stopped: true, pid, signal, ...signalResult, state_dir: stateDir };
507
598
  }
508
- if (event === "run.cancel") {
509
- finalizeInterruptedReviewEvidence(stateDir, "cancelled", signal);
510
- markTerminalProgress(stateDir, "cancelled");
599
+ finally {
600
+ releaseControlLock();
511
601
  }
512
- return { stopped: true, pid, signal, ...signalResult, state_dir: stateDir };
513
602
  }
514
603
  export function markRunTerminalNotificationHandled(stateDir, status) {
515
604
  markTerminalHandled(stateDir, {
@@ -517,8 +606,8 @@ export function markRunTerminalNotificationHandled(stateDir, status) {
517
606
  status,
518
607
  });
519
608
  }
520
- export function cancelRun(runOrDir) {
521
- const result = stopRun(runOrDir, "SIGTERM", "run.cancel");
609
+ export function cancelRun(runOrDir, expected = {}) {
610
+ const result = stopRun(runOrDir, "SIGTERM", "run.cancel", expected);
522
611
  return Object.hasOwn(result, "stopped")
523
612
  ? { cancelled: result.stopped, ...result }
524
613
  : result;
@@ -536,8 +625,8 @@ export function archiveRun(runOrDir) {
536
625
  export function pruneRun(runOrDir, options = {}) {
537
626
  return pruneTerminalRun(assertTerminalRun(runOrDir), options);
538
627
  }
539
- export function killRun(runOrDir) {
540
- const result = stopRun(runOrDir, "SIGKILL", "run.kill");
628
+ export function killRun(runOrDir, expected = {}) {
629
+ const result = stopRun(runOrDir, "SIGKILL", "run.kill", expected);
541
630
  return Object.hasOwn(result, "stopped")
542
631
  ? { killed: result.stopped, ...result }
543
632
  : result;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Automatic recipe-review runtime composition.
3
+ * Zones: session-bound review schedulers, silent reviewer launch adapters, review controls
4
+ * Owns automatic-review lifecycle wiring without owning review decisions or filesystem transactions.
5
+ */
6
+ import type * as Pi from "./pi.ts";
7
+ export interface AutomaticReviewRuntime {
8
+ close(): void;
9
+ handleMessage(type: string, body: unknown): Record<string, unknown>;
10
+ schedule(): void;
11
+ start(ctx: Pi.ExtensionContext): void;
12
+ }
13
+ export interface AutomaticReviewRuntimeDeps {
14
+ getActiveContext(): Pi.ExtensionContext | undefined;
15
+ getRunOwnerId(ctx: Pi.ExtensionContext): string;
16
+ getThinkingLevel(): unknown;
17
+ }
18
+ export declare function createAutomaticReviewRuntime(deps: AutomaticReviewRuntimeDeps): AutomaticReviewRuntime;
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Automatic recipe-review runtime composition.
3
+ * Zones: session-bound review schedulers, silent reviewer launch adapters, review controls
4
+ * Owns automatic-review lifecycle wiring without owning review decisions or filesystem transactions.
5
+ */
6
+ import { join } from "node:path";
7
+ import * as AsyncRuns from "./async-runs.js";
8
+ import * as DraftSleep from "./draft-sleep.js";
9
+ import * as ModelContext from "./model-context.js";
10
+ import * as Paths from "./paths.js";
11
+ import * as ReviewControl from "./review-control.js";
12
+ import * as ToolReviewScheduler from "./tool-review-scheduler.js";
13
+ export function createAutomaticReviewRuntime(deps) {
14
+ let draftScheduler;
15
+ let toolScheduler;
16
+ const close = () => {
17
+ draftScheduler?.close();
18
+ draftScheduler = undefined;
19
+ toolScheduler?.close();
20
+ toolScheduler = undefined;
21
+ };
22
+ const hasActiveActors = () => AsyncRuns.listRuns(Paths.EXTENSION_RUNTIME_PATHS.runStateRoot, "running").length > 0;
23
+ const policyValues = (ctx) => ModelContext.withCurrentModelValues({}, {
24
+ ...ctx,
25
+ getThinkingLevel: deps.getThinkingLevel,
26
+ });
27
+ return {
28
+ close,
29
+ handleMessage(type, body) {
30
+ if (type !== "review.retry" && type !== "review.reset") {
31
+ throw new Error("tool:pi-actors accepts review.retry or review.reset messages.");
32
+ }
33
+ if (type === "review.retry" && !Paths.isAutomaticRecipeReviewEnabled()) {
34
+ throw new Error("Automatic recipe review is disabled by PI_ACTORS_AUTOMATIC_REVIEW.");
35
+ }
36
+ return ReviewControl.controlAutomaticReview(type, ReviewControl.parseAutomaticReviewScope(body), {
37
+ scheduleDraft: () => draftScheduler?.schedule(),
38
+ scheduleTool: () => toolScheduler?.schedule(),
39
+ });
40
+ },
41
+ schedule() {
42
+ draftScheduler?.schedule();
43
+ toolScheduler?.schedule();
44
+ },
45
+ start(ctx) {
46
+ close();
47
+ if (!Paths.isAutomaticRecipeReviewEnabled())
48
+ return;
49
+ ToolReviewScheduler.applyApprovedToolReviewAtSessionBoundary({
50
+ recipeRoot: Paths.getRecipeRoot(),
51
+ });
52
+ draftScheduler = DraftSleep.createDraftSleepScheduler({
53
+ hasActiveActors,
54
+ launch: (batch) => {
55
+ if (deps.getActiveContext() !== ctx) {
56
+ throw new Error("Draft review session changed before launch.");
57
+ }
58
+ return AsyncRuns.startRun({
59
+ file: join(Paths.getPackagedRecipeRoot(), "draft-review.json"),
60
+ launch_source: "tool",
61
+ notification_policy: "silent",
62
+ ownerId: deps.getRunOwnerId(ctx),
63
+ policy_values: policyValues(ctx),
64
+ run_id: DraftSleep.draftSleepRunId(batch.batchId),
65
+ values: { input_path: batch.reviewerInputPath },
66
+ }, ctx.cwd);
67
+ },
68
+ process: (state) => DraftSleep.processDraftSleepReview(state, {
69
+ getRunStatus: AsyncRuns.getRunStatus,
70
+ recipeRoot: Paths.getRecipeRoot(),
71
+ }),
72
+ });
73
+ toolScheduler = ToolReviewScheduler.createToolReviewScheduler({
74
+ hasActiveActors,
75
+ launch: (batch) => {
76
+ if (deps.getActiveContext() !== ctx) {
77
+ throw new Error("Tool review session changed before launch.");
78
+ }
79
+ return AsyncRuns.startRun({
80
+ file: join(Paths.getPackagedRecipeRoot(), "tool-review.json"),
81
+ launch_source: "tool",
82
+ notification_policy: "silent",
83
+ ownerId: deps.getRunOwnerId(ctx),
84
+ policy_values: policyValues(ctx),
85
+ run_id: ToolReviewScheduler.toolReviewRunId(batch.reviewId),
86
+ values: { input_path: batch.reviewerInputPath },
87
+ }, ctx.cwd);
88
+ },
89
+ process: (state) => ToolReviewScheduler.processToolReviewResult(state, {
90
+ getRunStatus: AsyncRuns.getRunStatus,
91
+ recipeRoot: Paths.getRecipeRoot(),
92
+ }),
93
+ });
94
+ },
95
+ };
96
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Deterministic draft consolidation transaction and crash recovery.
3
+ * Owns: cycle claim, source/target compare-and-swap, complete recipe persistence, source quarantine, journal transitions, rollback, and runtime evidence.
4
+ */
5
+ import type { DraftConsolidationPlan } from "./draft-consolidation.ts";
6
+ import { type DraftConsolidationInventoryItem } from "./draft-consolidation.ts";
7
+ export type DraftConsolidationTransactionPhase = "prepared" | "targets_validated" | "sources_quarantined" | "committed_evidence_pending" | "committed" | "rolled_back" | "rollback_required";
8
+ interface TargetOperation {
9
+ backupPath?: string;
10
+ expectedSha256: string | null;
11
+ intendedSha256: string;
12
+ path: string;
13
+ recipe: Record<string, unknown>;
14
+ target: string;
15
+ }
16
+ interface SourceOperation {
17
+ path: string;
18
+ quarantinePath: string;
19
+ sha256: string;
20
+ }
21
+ interface ConsolidationRootIdentity {
22
+ dev: string;
23
+ ino: string;
24
+ path: string;
25
+ realpath: string;
26
+ }
27
+ interface ConsolidationRootIdentities {
28
+ cycleDir: ConsolidationRootIdentity;
29
+ draftRoot: ConsolidationRootIdentity;
30
+ recipeRoot: ConsolidationRootIdentity;
31
+ }
32
+ export interface DraftConsolidationJournal {
33
+ cycleId: string;
34
+ error?: string;
35
+ operationsSha256: string;
36
+ phase: DraftConsolidationTransactionPhase;
37
+ plan: DraftConsolidationPlan;
38
+ planSha256: string;
39
+ roots: ConsolidationRootIdentities;
40
+ sourceScope: "batch" | "complete";
41
+ sources: SourceOperation[];
42
+ targets: TargetOperation[];
43
+ updatedAt: string;
44
+ }
45
+ export type DraftConsolidationCheckpoint = "prepared" | "target_written" | "targets_validated" | "source_quarantined" | "sources_quarantined" | "committed_evidence_pending" | "evidence_written";
46
+ export interface DraftConsolidationTransactionOptions {
47
+ checkpoint?(point: DraftConsolidationCheckpoint): void;
48
+ cycleDir: string;
49
+ draftRoot: string;
50
+ inventory: DraftConsolidationInventoryItem[];
51
+ onLockContention?(): void;
52
+ sourceScope?: "batch" | "complete";
53
+ recipeRoot: string;
54
+ }
55
+ export interface DraftConsolidationTransactionResult {
56
+ cycleId: string;
57
+ evidencePath: string;
58
+ phase: "committed" | "rolled_back";
59
+ plan: DraftConsolidationPlan;
60
+ sources: number;
61
+ targets: number;
62
+ }
63
+ export declare function applyDraftConsolidationPlan(plan: DraftConsolidationPlan, options: DraftConsolidationTransactionOptions): DraftConsolidationTransactionResult;
64
+ export declare function recoverDraftConsolidationCycle(options: Pick<DraftConsolidationTransactionOptions, "cycleDir" | "draftRoot" | "recipeRoot">): DraftConsolidationTransactionResult;
65
+ export {};