@llblab/pi-actors 0.43.0 → 0.44.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 (109) hide show
  1. package/AGENTS.md +16 -10
  2. package/CHANGELOG.md +425 -536
  3. package/README.md +13 -11
  4. package/dist/index.js +1 -1
  5. package/dist/lib/async-runs.d.ts +2 -1
  6. package/dist/lib/async-runs.js +24 -34
  7. package/dist/lib/automatic-review-runtime.d.ts +1 -1
  8. package/dist/lib/automatic-review-runtime.js +5 -5
  9. package/dist/lib/command-templates.d.ts +2 -0
  10. package/dist/lib/command-templates.js +38 -4
  11. package/dist/lib/control-projection.d.ts +20 -0
  12. package/dist/lib/control-projection.js +66 -0
  13. package/dist/lib/control.d.ts +3 -0
  14. package/dist/lib/control.js +27 -14
  15. package/dist/lib/draft-sleep.js +3 -3
  16. package/dist/lib/file-state.d.ts +4 -1
  17. package/dist/lib/file-state.js +118 -44
  18. package/dist/lib/inspector-overlay.d.ts +2 -0
  19. package/dist/lib/inspector-overlay.js +124 -69
  20. package/dist/lib/limits.d.ts +15 -3
  21. package/dist/lib/limits.js +15 -3
  22. package/dist/lib/observability.d.ts +4 -2
  23. package/dist/lib/observability.js +43 -36
  24. package/dist/lib/prompts.d.ts +1 -1
  25. package/dist/lib/prompts.js +1 -1
  26. package/dist/lib/recipe-control.js +6 -2
  27. package/dist/lib/review-control.d.ts +1 -1
  28. package/dist/lib/review-control.js +4 -5
  29. package/dist/lib/run-evidence-policy.d.ts +95 -0
  30. package/dist/lib/run-evidence-policy.js +177 -0
  31. package/dist/lib/run-ui-runtime.js +2 -0
  32. package/dist/lib/runs-control-delivery.d.ts +8 -1
  33. package/dist/lib/runs-control-delivery.js +38 -15
  34. package/dist/lib/runs-controls.d.ts +8 -4
  35. package/dist/lib/runs-controls.js +189 -50
  36. package/dist/lib/runs-retention.js +27 -14
  37. package/dist/lib/runs-trace.d.ts +26 -2
  38. package/dist/lib/runs-trace.js +411 -18
  39. package/dist/lib/runtime-identity.d.ts +7 -0
  40. package/dist/lib/runtime-identity.js +35 -0
  41. package/dist/lib/runtime-triage.d.ts +29 -0
  42. package/dist/lib/runtime-triage.js +60 -0
  43. package/dist/lib/tool-review-scheduler.js +7 -7
  44. package/dist/lib/tools-inspect.js +91 -18
  45. package/dist/lib/tools-message.d.ts +1 -2
  46. package/dist/lib/tools-message.js +6 -6
  47. package/dist/lib/tools-response.d.ts +0 -1
  48. package/dist/lib/tools-response.js +0 -9
  49. package/dist/lib/tools.d.ts +1 -1
  50. package/dist/lib/tools.js +1 -1
  51. package/dist/lib/trace-projection.js +107 -41
  52. package/dist/scripts/conformance.mjs +5 -0
  53. package/dist/scripts/locker.mjs +40 -90
  54. package/dist/scripts/music-player.mjs +48 -142
  55. package/dist/scripts/release-gates.mjs +56 -3
  56. package/dist/scripts/validate-recipe.mjs +5 -4
  57. package/dist/skills/actors/SKILL.md +17 -11
  58. package/dist/skills/swarm/SKILL.md +2 -4
  59. package/docs/README.md +1 -4
  60. package/docs/actor-inspector.md +6 -5
  61. package/docs/async-runs.md +11 -9
  62. package/docs/command-templates.md +6 -116
  63. package/docs/recipe-library.md +4 -6
  64. package/docs/releasing.md +28 -0
  65. package/docs/template-recipes.md +1 -1
  66. package/docs/tool-registry.md +2 -2
  67. package/index.ts +1 -1
  68. package/lib/async-runs.ts +26 -50
  69. package/lib/automatic-review-runtime.ts +7 -7
  70. package/lib/command-templates.ts +44 -4
  71. package/lib/control-projection.ts +105 -0
  72. package/lib/control.ts +33 -18
  73. package/lib/draft-sleep.ts +3 -3
  74. package/lib/file-state.ts +91 -63
  75. package/lib/inspector-overlay.ts +108 -61
  76. package/lib/limits.ts +15 -3
  77. package/lib/observability.ts +55 -57
  78. package/lib/prompts.ts +1 -1
  79. package/lib/recipe-control.ts +9 -2
  80. package/lib/review-control.ts +4 -5
  81. package/lib/run-evidence-policy.ts +242 -0
  82. package/lib/run-ui-runtime.ts +2 -0
  83. package/lib/runs-control-delivery.ts +45 -17
  84. package/lib/runs-controls.ts +180 -102
  85. package/lib/runs-retention.ts +28 -20
  86. package/lib/runs-trace.ts +499 -20
  87. package/lib/runtime-identity.ts +39 -0
  88. package/lib/runtime-triage.ts +106 -0
  89. package/lib/tool-review-scheduler.ts +7 -7
  90. package/lib/tools-inspect.ts +94 -20
  91. package/lib/tools-message.ts +7 -8
  92. package/lib/tools-response.ts +0 -12
  93. package/lib/tools.ts +4 -4
  94. package/lib/trace-projection.ts +156 -71
  95. package/package.json +1 -1
  96. package/scripts/conformance.mjs +5 -0
  97. package/scripts/locker.mjs +40 -90
  98. package/scripts/music-player.mjs +48 -142
  99. package/scripts/release-gates.mjs +56 -3
  100. package/scripts/validate-recipe.mjs +5 -4
  101. package/skills/actors/SKILL.md +17 -11
  102. package/skills/swarm/SKILL.md +2 -4
  103. package/dist/lib/runtime-notifier.d.ts +0 -48
  104. package/dist/lib/runtime-notifier.js +0 -138
  105. package/docs/0.43-baseline.md +0 -44
  106. package/docs/actors-deep-reference.md +0 -108
  107. package/docs/component-recipes.md +0 -45
  108. package/docs/task-first-recipes.md +0 -261
  109. package/lib/runtime-notifier.ts +0 -211
package/README.md CHANGED
@@ -53,8 +53,10 @@ Send one exact Control:
53
53
  Run targets accept only actions declared by the captured Recipe. Runtime targets accept only reserved review actions:
54
54
 
55
55
  ```text
56
- message target=runtime action=review.retry
57
- message target=runtime action=review.reset
56
+ message target=runtime action=review.retry input={"scope":"draft"}
57
+ message target=runtime action=review.retry input={"scope":"tool"}
58
+ message target=runtime action=review.reset input={"scope":"draft"}
59
+ message target=runtime action=review.reset input={"scope":"tool"}
58
60
  ```
59
61
 
60
62
  Lifecycle `kill` remains runtime-owned rather than Recipe-declared.
@@ -67,9 +69,9 @@ Inspect one exact management target:
67
69
  inspect target=run:test view=recipe
68
70
  inspect target=run:test view=trace source=lifecycle lines=40
69
71
  inspect target=run:test view=control
70
- inspect target=runtime
71
- inspect target=recipes
72
- inspect target=tool:my_tool
72
+ inspect target=runtime view=status
73
+ inspect target=recipes view=status
74
+ inspect target=tool:my_tool view=status
73
75
  ```
74
76
 
75
77
  A Run exposes exactly `recipe`, `trace`, and `control` views.
@@ -119,13 +121,13 @@ String command-template leaves execute directly without shell interpretation. Us
119
121
  }
120
122
  ```
121
123
 
122
- Trace fields are exact: `id`, `ts`, `kind`, and optional `summary`, `data`, `level`, `attention`. Address, sender, recipient, reply, and routing fields fail validation.
124
+ Trace fields are exact: `id`, `ts`, `kind`, and optional `summary`, `data`, `level`, `attention`. Address, sender, recipient, reply, and routing fields fail validation. Trace is a bounded retained suffix, not an audit archive: the canonical authority appends within 2,048 events and 4 MiB or atomically keeps the newest suffix plus one warning-only `runtime.trace_compacted` marker. That marker means older history was discarded; `result.json`, `execution.json`, terminal state, and declared artifacts retain their own authority. `inspect view=trace` reports whether retained history is complete. Reads are newline-safe and deterministic; first-party scripts never write this file directly.
123
125
 
124
- Use `attention: "notify"` for visible status and `attention: "followup"` only when the coordinator needs semantic follow-up context. Store large evidence in artifacts or bounded execution captures.
126
+ Attention is a live wake hint, not a durable queue: persist recovery state or an artifact first. Use `attention: "notify"` for visible status and `attention: "followup"` only when the coordinator needs semantic follow-up context; compaction may discard older hints. Store large evidence in artifacts or bounded execution captures.
125
127
 
126
128
  ## Control
127
129
 
128
- Controls persist to `controls.jsonl` before transport. Token-owned dead-process-reclaiming locks serialize atomic journal replacements. Every record carries the immutable `run_instance_id`; expected-status fencing advances outcomes monotonically through queued/delivered/claimed/handled/failed evidence, while a fast consumer may claim or handle before the sender adds independent delivery-time evidence.
130
+ Controls persist to `controls.jsonl` before transport. One token-owned lock rejects a 65th pending Control or 1 MiB rewrite before admission, fails closed on malformed or stale-generation evidence, and atomically admits one record. Canonical transitions retain a 128-terminal tail, expected-state fencing, and 4 KiB errors. Admitted nonterminal Controls never expire automatically. Every record carries immutable `run_instance_id`. `inspect view=control` reports pending capacity, saturation, stale work, journal bytes, and bounded diagnostics. If a stuck Run is saturated, use runtime-owned `kill`; it bypasses actor-local capacity and creates no synthetic Control.
129
131
 
130
132
  Long-lived services publish `control-endpoint.json` only when ready:
131
133
 
@@ -138,7 +140,7 @@ Long-lived services publish `control-endpoint.json` only when ready:
138
140
  }
139
141
  ```
140
142
 
141
- Supported transports are Unix FIFO and Windows named pipe. FIFO wire documents must fit the portable 512-byte atomic-write bound; larger Control input remains available to named-pipe services. Delivery revalidates owner, generation, state, and process identity under the canonical lifecycle lock.
143
+ Supported transports are Unix FIFO and Windows named pipe. Every actor-local Control uses the same portable envelope: action is at most 64 lowercase ASCII characters, serialized JSON input is at most 380 bytes, and the newline-terminated wire record is at most 512 bytes. Put larger data in a declared artifact/path and send only its bounded reference or instruction through Control. Delivery revalidates owner, generation, state, and process identity under the canonical lifecycle lock.
142
144
 
143
145
  ## Run State
144
146
 
@@ -157,7 +159,7 @@ Core files:
157
159
  - `execution.json` — command/session provenance and complete-capture references;
158
160
  - `result.json`, command logs, progress, and declared artifacts.
159
161
 
160
- The runtime preserves owner filtering, process-identity verification, lifecycle locking, shutdown kill, terminal reconciliation, bounded captures, owned Pi sessions, path containment, and redaction.
162
+ The runtime preserves owner filtering, process-identity verification, lifecycle locking, shutdown kill independent of actor-local Control capacity, terminal reconciliation, bounded captures, owned Pi sessions, path containment, and redaction. Trace/Control quotas do not constrain user-declared artifacts, repositories, media sources, complete captures, or actor-owned workload state. Restart clears generation-local Trace, Control, endpoint, execution, and terminal evidence; archive preserves the bounded tree, while prune preserves only requested artifacts.
161
163
 
162
164
  ## Actor Inspector
163
165
 
@@ -195,7 +197,7 @@ npm run validate
195
197
  npm run test:preservation
196
198
  ```
197
199
 
198
- See the [documentation index](./docs/README.md), [Run lifecycle](./docs/async-runs.md), [Recipe library](./docs/recipe-library.md), and [0.43 baseline](./docs/0.43-baseline.md).
200
+ See the [documentation index](./docs/README.md), [Run lifecycle](./docs/async-runs.md), and [Recipe library](./docs/recipe-library.md).
199
201
 
200
202
  Project context: [AGENTS.md](./AGENTS.md) · [BACKLOG.md](./BACKLOG.md) · [CHANGELOG.md](./CHANGELOG.md).
201
203
 
package/dist/index.js CHANGED
@@ -106,7 +106,7 @@ export default function toolRegistryExtension(pi) {
106
106
  configPath: Paths.EXTENSION_RUNTIME_PATHS.configPath,
107
107
  getActiveTools: () => pi.getActiveTools(),
108
108
  getRuntimeTool: (name) => Tools.resolveActiveRuntimeTool(name, runtime.getTools(), (activeName) => actorToolDefinitions.get(activeName)),
109
- handleRuntimeMessage: automaticReview.handleMessage,
109
+ handleRuntimeControl: automaticReview.handleControl,
110
110
  registryRuntime: runtime,
111
111
  setActiveTools: (toolNames) => pi.setActiveTools(toolNames),
112
112
  }).map(withCurrentThinkingContext));
@@ -7,6 +7,7 @@ import { type CurrentPolicyProvenance } from "./model-context.ts";
7
7
  import * as RecipesReferences from "./recipes-references.ts";
8
8
  import { type RunArtifactDeclaration } from "./runs-artifacts.ts";
9
9
  import { type RunProcessIdentity } from "./runs-process.ts";
10
+ import * as RuntimeIdentity from "./runtime-identity.ts";
10
11
  import * as RunsIndex from "./runs-index.ts";
11
12
  import * as RunsParentTeardown from "./runs-parent-teardown.ts";
12
13
  import { type DeliverRunControlOptions, type DeliverRunControlRequest } from "./runs-control-delivery.ts";
@@ -77,7 +78,7 @@ export interface AsyncRunMeta {
77
78
  run: string;
78
79
  run_instance_id: string;
79
80
  state_dir: string;
80
- state_schema: "run-kernel-v1";
81
+ state_schema: typeof RuntimeIdentity.RUN_STATE_SCHEMA;
81
82
  status: AsyncRunStatus;
82
83
  tool?: string;
83
84
  template: CommandTemplateValue;
@@ -18,11 +18,11 @@ import { buildTerminalProgress, markTerminalHandled, signalOwnedRunProcess, } fr
18
18
  import { claimRunStateDirectory } from "./runs-ownership.js";
19
19
  import { appendRunRetentionEvidence, archiveTerminalRun, pruneTerminalRun, } from "./runs-retention.js";
20
20
  import { captureRunProcessIdentity, verifyRunProcessIdentity, } from "./runs-process.js";
21
+ import * as RuntimeIdentity from "./runtime-identity.js";
21
22
  import * as RunsStart from "./runs-start.js";
22
23
  import { appendRunTraceEvent } from "./runs-trace.js";
23
24
  import * as RunsIndex from "./runs-index.js";
24
25
  import * as RunsParentTeardown from "./runs-parent-teardown.js";
25
- import { appendRunControlInStateDir, updateRunControlStatusInStateDir, } from "./runs-controls.js";
26
26
  import { deliverRunControl, } from "./runs-control-delivery.js";
27
27
  import { buildRunStatus, tailFile, } from "./runs-status.js";
28
28
  import { readJsonFileResilient } from "./state-readers.js";
@@ -236,7 +236,21 @@ export function startRun(params, cwd) {
236
236
  const resolved = resolveRunTemplate(startParams);
237
237
  const run = safeRunId(startParams.run_id);
238
238
  const stateDir = resolveStateDir(startParams, run);
239
+ const recipeFile = startParams.file
240
+ ? resolveRecipeFile(startParams.file)
241
+ : undefined;
242
+ const packagedRecipeRoot = resolve(Paths.getPackagedRecipeRoot());
243
+ const recipeRelation = recipeFile
244
+ ? relative(packagedRecipeRoot, recipeFile)
245
+ : undefined;
246
+ const packagedRepo = startParams.defaults?.repo === "~/.pi/agent/extensions/pi-actors" &&
247
+ recipeRelation &&
248
+ !recipeRelation.startsWith("..") &&
249
+ !isAbsolute(recipeRelation)
250
+ ? dirname(packagedRecipeRoot)
251
+ : undefined;
239
252
  const values = {
253
+ ...(packagedRepo ? { repo: packagedRepo } : {}),
240
254
  ...(startParams.values || {}),
241
255
  run_id: run,
242
256
  state_dir: stateDir,
@@ -259,9 +273,6 @@ export function startRun(params, cwd) {
259
273
  prepareStateDirForStart(stateDir);
260
274
  const stdout = join(stateDir, "stdout.log");
261
275
  const stderr = join(stateDir, "stderr.log");
262
- const recipeFile = startParams.file
263
- ? resolveRecipeFile(startParams.file)
264
- : undefined;
265
276
  const recipe = startParams.name || getRunIdFromFile(recipeFile);
266
277
  const includeActorRecipeContext = startParams.actor_context !== false &&
267
278
  startParams.actor_context !== "off";
@@ -298,7 +309,7 @@ export function startRun(params, cwd) {
298
309
  run,
299
310
  run_instance_id: randomUUID(),
300
311
  state_dir: stateDir,
301
- state_schema: "run-kernel-v1",
312
+ state_schema: RuntimeIdentity.RUN_STATE_SCHEMA,
302
313
  status: "running",
303
314
  ...(startParams.tool ? { tool: startParams.tool } : {}),
304
315
  template: resolved.template,
@@ -564,57 +575,36 @@ function stopRun(runOrDir, signal, event, expected = {}) {
564
575
  status.run_instance_id !== expected.runInstanceId) {
565
576
  return { stopped: false, reason: "run generation changed", status };
566
577
  }
567
- const control = event === "run.kill" && typeof status.run_instance_id === "string"
568
- ? appendRunControlInStateDir(stateDir, {
569
- action: "kill",
570
- run_instance_id: status.run_instance_id,
571
- })
572
- : undefined;
573
- if (control) {
574
- updateRunControlStatusInStateDir(stateDir, control.id, "claimed", {}, ["queued"]);
575
- }
576
- const finish = (result) => {
577
- if (!control)
578
- return result;
579
- const handled = result.stopped === true;
580
- updateRunControlStatusInStateDir(stateDir, control.id, handled ? "handled" : "failed", handled ? {} : { error: String(result.reason ?? "kill rejected") }, ["claimed"]);
581
- return { ...result, control_id: control.id };
582
- };
583
578
  const pid = Number(status.pid || 0);
584
579
  if (status.status !== "running" && status.status !== "exited") {
585
- return finish({ stopped: false, reason: "not running", status });
580
+ return { stopped: false, reason: "not running", status };
586
581
  }
587
582
  const identity = verifyRunProcessIdentity(pid, status.process_identity);
588
583
  if (status.status === "exited") {
589
584
  if (identity.status === "owner_mismatch" ||
590
585
  identity.status === "unsupported_proof") {
591
- return finish({
586
+ return {
592
587
  stopped: false,
593
588
  reason: identity.status.replaceAll("_", " "),
594
589
  process_identity_status: identity.status,
595
590
  status,
596
- });
591
+ };
597
592
  }
598
- return finish({ stopped: false, reason: "not running", status });
593
+ return { stopped: false, reason: "not running", status };
599
594
  }
600
595
  if (!identity.valid) {
601
- return finish({
596
+ return {
602
597
  stopped: false,
603
598
  reason: identity.status.replaceAll("_", " "),
604
599
  process_identity_status: identity.status,
605
600
  status,
606
- });
601
+ };
607
602
  }
608
603
  let signalResult;
609
604
  try {
610
605
  signalResult = signalOwnedRunProcess(pid, signal, status.process_identity);
611
606
  }
612
607
  catch (error) {
613
- if (control) {
614
- updateRunControlStatusInStateDir(stateDir, control.id, "failed", {
615
- error: error instanceof Error ? error.message : String(error),
616
- });
617
- }
618
608
  throw error;
619
609
  }
620
610
  appendRunTraceEvent(stateDir, {
@@ -631,13 +621,13 @@ function stopRun(runOrDir, signal, event, expected = {}) {
631
621
  finalizeInterruptedExecution(stateDir, "cancelled", signal);
632
622
  markTerminalProgress(stateDir, "cancelled");
633
623
  }
634
- return finish({
624
+ return {
635
625
  stopped: true,
636
626
  pid,
637
627
  signal,
638
628
  ...signalResult,
639
629
  state_dir: stateDir,
640
- });
630
+ };
641
631
  }
642
632
  finally {
643
633
  releaseControlLock();
@@ -6,7 +6,7 @@
6
6
  import type * as Pi from "./pi.ts";
7
7
  export interface AutomaticReviewRuntime {
8
8
  close(): void;
9
- handleMessage(type: string, body: unknown): Record<string, unknown>;
9
+ handleControl(action: string, input: unknown): Record<string, unknown>;
10
10
  schedule(): void;
11
11
  start(ctx: Pi.ExtensionContext): void;
12
12
  }
@@ -26,14 +26,14 @@ export function createAutomaticReviewRuntime(deps) {
26
26
  });
27
27
  return {
28
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.");
29
+ handleControl(action, input) {
30
+ if (action !== "review.retry" && action !== "review.reset") {
31
+ throw new Error("runtime accepts review.retry or review.reset Controls.");
32
32
  }
33
- if (type === "review.retry" && !Paths.isAutomaticRecipeReviewEnabled()) {
33
+ if (action === "review.retry" && !Paths.isAutomaticRecipeReviewEnabled()) {
34
34
  throw new Error("Automatic recipe review is disabled by PI_ACTORS_AUTOMATIC_REVIEW.");
35
35
  }
36
- return ReviewControl.controlAutomaticReview(type, ReviewControl.parseAutomaticReviewScope(body), {
36
+ return ReviewControl.controlAutomaticReview(action, ReviewControl.parseAutomaticReviewScope(input), {
37
37
  scheduleDraft: () => draftScheduler?.schedule(),
38
38
  scheduleTool: () => toolScheduler?.schedule(),
39
39
  });
@@ -74,6 +74,8 @@ export declare function getCommandTemplateRiskLabels(config: CommandTemplateConf
74
74
  export declare function getCommandTemplateDefaults(config: CommandTemplateConfig | undefined): Record<string, string>;
75
75
  export declare function splitCommandTemplate(input: string): string[];
76
76
  export declare function expandCommandTemplateExecutable(command: string, cwd: string): string;
77
+ export type CommandTemplateExecutableResolver = (name: string) => string | undefined;
78
+ export declare function resolveCommandTemplateScriptInvocation(invocation: CommandTemplateInvocation, resolveExecutable?: CommandTemplateExecutableResolver): CommandTemplateInvocation;
77
79
  export declare function shouldRunCommandTemplateNode(value: boolean | string | undefined, values: Record<string, unknown>): boolean;
78
80
  export declare function substituteCommandTemplateToken(token: string, values: Record<string, unknown>, missingLabel?: string, depth?: number): string;
79
81
  export declare function execCommandTemplate(command: string, args: string[], options?: CommandTemplateExecOptions): Promise<CommandTemplateExecResult>;
@@ -4,9 +4,9 @@
4
4
  * Owns portable command-template parsing, expansion, risk checks, retries, timeouts, and direct execution.
5
5
  */
6
6
  import { spawn } from "node:child_process";
7
- import { appendFileSync, mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
7
+ import { accessSync, appendFileSync, constants, mkdirSync, mkdtempSync, statSync, writeFileSync } from "node:fs";
8
8
  import { homedir, tmpdir } from "node:os";
9
- import { isAbsolute, join, resolve as resolvePath } from "node:path";
9
+ import { basename, delimiter, extname, isAbsolute, join, resolve as resolvePath } from "node:path";
10
10
  const DEFAULT_COMMAND_CAPTURE_LIMIT_BYTES = 1024 * 1024;
11
11
  const COMMAND_TEMPLATE_RISK_LABEL_ORDER = [
12
12
  "risk.shell",
@@ -135,7 +135,7 @@ function getLeafCommandTemplateRiskLabels(config) {
135
135
  const command = getExecutableName(parts[0]);
136
136
  const args = parts.slice(1);
137
137
  const labels = new Set();
138
- if (["bash", "sh", "zsh", "fish"].includes(command)) {
138
+ if (["bash", "sh", "zsh", "fish"].includes(command) || command.endsWith(".sh")) {
139
139
  labels.add("risk.shell");
140
140
  if (hasAnyFlag(args, ["-c"]))
141
141
  labels.add("risk.eval");
@@ -393,6 +393,40 @@ export function expandCommandTemplateExecutable(command, cwd) {
393
393
  return resolvePath(cwd, command);
394
394
  return command;
395
395
  }
396
+ function resolveExecutableOnPath(name) {
397
+ if (name === "node" && /^node(?:\.exe)?$/i.test(basename(process.execPath)))
398
+ return process.execPath;
399
+ const pathValue = Object.entries(process.env)
400
+ .find(([key]) => key.toLowerCase() === "path")?.[1] ?? "";
401
+ const suffixes = process.platform === "win32" ? [".exe", ".com"] : [""];
402
+ for (const entry of pathValue.split(delimiter).filter(Boolean)) {
403
+ const directory = entry.replace(/^"|"$/g, "");
404
+ for (const suffix of suffixes) {
405
+ const candidate = join(directory, `${name}${suffix}`);
406
+ try {
407
+ accessSync(candidate, constants.X_OK);
408
+ if (statSync(candidate).isFile())
409
+ return candidate;
410
+ }
411
+ catch { }
412
+ }
413
+ }
414
+ return undefined;
415
+ }
416
+ export function resolveCommandTemplateScriptInvocation(invocation, resolveExecutable = resolveExecutableOnPath) {
417
+ const extension = extname(invocation.command).toLowerCase();
418
+ const interpreters = extension === ".js" || extension === ".mjs"
419
+ ? [["node"], ["bun"], ["deno", "run"]]
420
+ : extension === ".sh" ? [["bash"]] : [];
421
+ if (interpreters.length === 0)
422
+ return invocation;
423
+ for (const [name, ...prefix] of interpreters) {
424
+ const executable = resolveExecutable(name);
425
+ if (executable)
426
+ return { command: executable, args: [...prefix, invocation.command, ...invocation.args] };
427
+ }
428
+ throw new Error(`No interpreter available for command-template ${extension} script; tried ${interpreters.map(([name]) => name).join(", ")}.`);
429
+ }
396
430
  function evaluateCommandTemplateExpression(expression, values) {
397
431
  let index = 0;
398
432
  const source = expression.replace(/\s+/g, "");
@@ -766,5 +800,5 @@ export function buildCommandTemplateInvocation(config, values, cwd, options = {}
766
800
  .slice(1)
767
801
  .map((part) => substituteCommandTemplateToken(part, resolvedValues, options.missingLabel))
768
802
  .filter((part) => part !== "");
769
- return { command, args };
803
+ return resolveCommandTemplateScriptInvocation({ command, args });
770
804
  }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Model-facing Run Control projection.
3
+ * Zones: structured redaction, bounded Control input/error, decision-useful evidence
4
+ * Owns safe Control reads; durable journals and lifecycle transitions remain in runs-controls.
5
+ */
6
+ import type { RunControlRecord, RunControlStatus } from "./runs-controls.ts";
7
+ export interface ProjectedRunControl {
8
+ action: string;
9
+ claimed_at?: string;
10
+ delivered_at?: string;
11
+ error?: string;
12
+ failed_at?: string;
13
+ handled_at?: string;
14
+ id: string;
15
+ input?: unknown;
16
+ queued_at: string;
17
+ run_instance_id: string;
18
+ status: RunControlStatus;
19
+ }
20
+ export declare function projectRunControl(control: RunControlRecord): ProjectedRunControl;
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Model-facing Run Control projection.
3
+ * Zones: structured redaction, bounded Control input/error, decision-useful evidence
4
+ * Owns safe Control reads; durable journals and lifecycle transitions remain in runs-controls.
5
+ */
6
+ import * as Limits from "./limits.js";
7
+ import * as SessionEvidence from "./session-evidence.js";
8
+ const OPTIONAL_TIMESTAMP_FIELDS = [
9
+ "claimed_at",
10
+ "delivered_at",
11
+ "failed_at",
12
+ "handled_at",
13
+ ];
14
+ function boundedRedactedString(value, maxChars) {
15
+ const redacted = SessionEvidence.redactSessionEvidenceValue(value, maxChars);
16
+ return typeof redacted === "string"
17
+ ? redacted
18
+ : String(redacted).slice(0, maxChars);
19
+ }
20
+ function boundedRedactedValue(value) {
21
+ const redacted = SessionEvidence.redactSessionEvidenceValue(value, Limits.INSPECTOR_BODY_PREVIEW_CHARS);
22
+ let serialized;
23
+ try {
24
+ serialized = JSON.stringify(redacted);
25
+ }
26
+ catch {
27
+ return "[UNSERIALIZABLE]";
28
+ }
29
+ if (serialized === undefined)
30
+ return "[UNSERIALIZABLE]";
31
+ if (serialized.length <= Limits.INSPECTOR_BODY_PREVIEW_CHARS)
32
+ return redacted;
33
+ let preview = serialized.slice(0, Limits.INSPECTOR_BODY_PREVIEW_CHARS);
34
+ let bounded = { preview: `${preview}…`, truncated: true };
35
+ while (preview &&
36
+ JSON.stringify(bounded).length > Limits.INSPECTOR_BODY_PREVIEW_CHARS) {
37
+ const excess = JSON.stringify(bounded).length - Limits.INSPECTOR_BODY_PREVIEW_CHARS;
38
+ preview = preview.slice(0, Math.max(0, preview.length - Math.max(1, excess)));
39
+ bounded = { preview: `${preview}…`, truncated: true };
40
+ }
41
+ return bounded;
42
+ }
43
+ export function projectRunControl(control) {
44
+ const timestamps = Object.fromEntries(OPTIONAL_TIMESTAMP_FIELDS.flatMap((field) => {
45
+ const value = control[field];
46
+ return typeof value === "string"
47
+ ? [[field, boundedRedactedString(value, Limits.COMPACT_PREVIEW_CHARS)]]
48
+ : [];
49
+ }));
50
+ return {
51
+ action: boundedRedactedString(control.action, Limits.COMPACT_PREVIEW_CHARS),
52
+ ...timestamps,
53
+ ...(typeof control.error === "string"
54
+ ? {
55
+ error: boundedRedactedString(control.error, Limits.INSPECTOR_BODY_PREVIEW_CHARS),
56
+ }
57
+ : {}),
58
+ id: boundedRedactedString(control.id, Limits.COMPACT_PREVIEW_CHARS),
59
+ ...(control.input !== undefined
60
+ ? { input: boundedRedactedValue(control.input) }
61
+ : {}),
62
+ queued_at: boundedRedactedString(control.queued_at, Limits.COMPACT_PREVIEW_CHARS),
63
+ run_instance_id: boundedRedactedString(control.run_instance_id, Limits.COMPACT_PREVIEW_CHARS),
64
+ status: boundedRedactedString(control.status, Limits.COMPACT_PREVIEW_CHARS),
65
+ };
66
+ }
@@ -9,4 +9,7 @@ export interface ControlRequest {
9
9
  input?: unknown;
10
10
  verbose?: boolean;
11
11
  }
12
+ export declare function isControlAction(value: string): boolean;
13
+ export declare function normalizeControlAction(value: unknown): string;
14
+ export declare function normalizeControlInput(input: unknown): unknown;
12
15
  export declare function normalizeControlRequest(input: unknown): ControlRequest;
@@ -17,7 +17,23 @@ const REMOVED_FIELDS = new Set([
17
17
  "to",
18
18
  "type",
19
19
  ]);
20
- function serializedInputBytes(input) {
20
+ export function isControlAction(value) {
21
+ return ACTION_PATTERN.test(value);
22
+ }
23
+ export function normalizeControlAction(value) {
24
+ if (typeof value !== "string" || !value.trim()) {
25
+ throw new Error("control.action is required");
26
+ }
27
+ const action = value.trim();
28
+ if (!isControlAction(action)) {
29
+ throw new Error(`invalid control action: ${action}`);
30
+ }
31
+ if (action.length > Limits.CONTROL_ACTION_MAX_LENGTH) {
32
+ throw new Error(`control.action exceeds ${Limits.CONTROL_ACTION_MAX_LENGTH} ASCII characters`);
33
+ }
34
+ return action;
35
+ }
36
+ export function normalizeControlInput(input) {
21
37
  let serialized;
22
38
  try {
23
39
  serialized = JSON.stringify(input);
@@ -28,7 +44,11 @@ function serializedInputBytes(input) {
28
44
  if (serialized === undefined) {
29
45
  throw new Error("control.input must be JSON-serializable");
30
46
  }
31
- return Buffer.byteLength(serialized);
47
+ const bytes = Buffer.byteLength(serialized);
48
+ if (bytes > Limits.CONTROL_INPUT_MAX_BYTES) {
49
+ throw new Error(`control.input exceeds ${Limits.CONTROL_INPUT_MAX_BYTES} serialized bytes`);
50
+ }
51
+ return JSON.parse(serialized);
32
52
  }
33
53
  function normalizeTarget(value) {
34
54
  if (typeof value !== "string" || !value.trim()) {
@@ -61,24 +81,17 @@ export function normalizeControlRequest(input) {
61
81
  throw new Error(`unsupported control fields: ${unknown.sort().join(", ")}`);
62
82
  }
63
83
  const target = normalizeTarget(record.target);
64
- if (typeof record.action !== "string" || !record.action.trim()) {
65
- throw new Error("control.action is required");
66
- }
67
- const action = record.action.trim();
68
- if (!ACTION_PATTERN.test(action)) {
69
- throw new Error(`invalid control action: ${action}`);
70
- }
84
+ const action = normalizeControlAction(record.action);
71
85
  if (record.verbose !== undefined && typeof record.verbose !== "boolean") {
72
86
  throw new Error("control.verbose must be a boolean");
73
87
  }
74
- if (record.input !== undefined &&
75
- serializedInputBytes(record.input) > Limits.CONTROL_INPUT_MAX_BYTES) {
76
- throw new Error(`control.input exceeds ${Limits.CONTROL_INPUT_MAX_BYTES} bytes`);
77
- }
88
+ const normalizedInput = record.input === undefined
89
+ ? undefined
90
+ : normalizeControlInput(record.input);
78
91
  return {
79
92
  target,
80
93
  action,
81
- ...(record.input !== undefined ? { input: record.input } : {}),
94
+ ...(normalizedInput !== undefined ? { input: normalizedInput } : {}),
82
95
  ...(record.verbose !== undefined ? { verbose: record.verbose } : {}),
83
96
  };
84
97
  }
@@ -391,7 +391,7 @@ export function createDraftSleepScheduler(deps) {
391
391
  ...existing,
392
392
  failedStage: processed.stage ?? "result_processing",
393
393
  lastError: processed.error ?? "automatic draft review processing failed",
394
- nextAction: "message to=tool:pi-actors type=review.retry body={\"scope\":\"draft\"}",
394
+ nextAction: "message target=runtime action=review.retry input={\"scope\":\"draft\"}",
395
395
  phase: "processing_failed",
396
396
  processingAttempts: (existing.processingAttempts ?? 0) + 1,
397
397
  updatedAt: now().toISOString(),
@@ -402,7 +402,7 @@ export function createDraftSleepScheduler(deps) {
402
402
  ...existing,
403
403
  failedStage: processed.stage ?? "review_result",
404
404
  lastError: processed.error ?? "automatic draft reviewer failed",
405
- nextAction: "message to=tool:pi-actors type=review.retry body={\"scope\":\"draft\"}",
405
+ nextAction: "message target=runtime action=review.retry input={\"scope\":\"draft\"}",
406
406
  phase: "failed",
407
407
  runId: undefined,
408
408
  updatedAt: now().toISOString(),
@@ -444,7 +444,7 @@ export function createDraftSleepScheduler(deps) {
444
444
  attempts: (prior?.batchId === batch.batchId ? prior.attempts : 0) + 1,
445
445
  failedStage: "review_launch",
446
446
  lastError: error instanceof Error ? error.message : String(error),
447
- nextAction: "message to=tool:pi-actors type=review.retry body={\"scope\":\"draft\"}",
447
+ nextAction: "message target=runtime action=review.retry input={\"scope\":\"draft\"}",
448
448
  batchId: batch.batchId,
449
449
  inputPath: batch.inputPath,
450
450
  phase: "failed",
@@ -5,11 +5,14 @@
5
5
  */
6
6
  export declare function mutationLockPath(path: string): string;
7
7
  export interface FileMutationLockOptions {
8
+ onBeforeLockPublish?(): void;
8
9
  onBeforeReclaimRemove?(): void;
9
10
  onContention?(): void;
10
11
  onRemovalContention?(): void;
11
12
  }
12
13
  export declare function acquireFileMutationLock(path: string, options?: FileMutationLockOptions): () => void;
13
14
  export declare function withFileMutationLock<T>(path: string, mutate: () => T, options?: FileMutationLockOptions): T;
14
- export declare function writeTextAtomic(path: string, content: string): void;
15
+ export declare function writeTextAtomic(path: string, content: string, options?: {
16
+ onBeforeReplace?(): void;
17
+ }): void;
15
18
  export declare function writeJsonAtomic(path: string, value: unknown): void;