@llblab/pi-actors 0.42.3 → 0.43.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (279) hide show
  1. package/AGENTS.md +127 -175
  2. package/BACKLOG.md +189 -1
  3. package/CHANGELOG.md +183 -292
  4. package/README.md +115 -276
  5. package/dist/fixtures/protocol/control-endpoint.json +6 -0
  6. package/dist/fixtures/protocol/control-record.json +9 -0
  7. package/dist/fixtures/protocol/recipe-summary.json +4 -12
  8. package/dist/fixtures/protocol/trace-event.json +9 -0
  9. package/dist/index.js +1 -1
  10. package/dist/lib/async-runs.d.ts +15 -38
  11. package/dist/lib/async-runs.js +173 -111
  12. package/dist/lib/automatic-review-runtime.d.ts +1 -1
  13. package/dist/lib/automatic-review-runtime.js +5 -5
  14. package/dist/lib/command-templates.d.ts +2 -0
  15. package/dist/lib/command-templates.js +38 -4
  16. package/dist/lib/control-projection.d.ts +20 -0
  17. package/dist/lib/control-projection.js +66 -0
  18. package/dist/lib/control.d.ts +15 -0
  19. package/dist/lib/control.js +97 -0
  20. package/dist/lib/draft-sleep.js +3 -3
  21. package/dist/lib/execution-sessions.d.ts +17 -0
  22. package/dist/lib/execution-sessions.js +85 -0
  23. package/dist/lib/file-state.d.ts +2 -0
  24. package/dist/lib/file-state.js +114 -46
  25. package/dist/lib/inspector-actions.d.ts +2 -2
  26. package/dist/lib/inspector-actions.js +2 -2
  27. package/dist/lib/inspector-command.js +3 -3
  28. package/dist/lib/inspector-overlay.d.ts +54 -70
  29. package/dist/lib/inspector-overlay.js +576 -910
  30. package/dist/lib/inspector.d.ts +3 -71
  31. package/dist/lib/inspector.js +19 -665
  32. package/dist/lib/limits.d.ts +7 -3
  33. package/dist/lib/limits.js +7 -3
  34. package/dist/lib/observability.d.ts +16 -16
  35. package/dist/lib/observability.js +43 -82
  36. package/dist/lib/prompts.d.ts +1 -1
  37. package/dist/lib/prompts.js +3 -3
  38. package/dist/lib/recipe-control.d.ts +7 -0
  39. package/dist/lib/recipe-control.js +43 -0
  40. package/dist/lib/recipes-discovery.js +2 -0
  41. package/dist/lib/recipes-references.d.ts +1 -14
  42. package/dist/lib/recipes-references.js +6 -21
  43. package/dist/lib/review-control.d.ts +1 -1
  44. package/dist/lib/review-control.js +4 -5
  45. package/dist/lib/review-projection.js +1 -5
  46. package/dist/lib/run-ui-runtime.js +2 -2
  47. package/dist/lib/runs-control-delivery.d.ts +28 -0
  48. package/dist/lib/runs-control-delivery.js +150 -0
  49. package/dist/lib/runs-controls.d.ts +37 -0
  50. package/dist/lib/runs-controls.js +146 -0
  51. package/dist/lib/runs-retention.d.ts +7 -0
  52. package/dist/lib/runs-retention.js +27 -3
  53. package/dist/lib/runs-start.js +4 -2
  54. package/dist/lib/runs-status.js +11 -6
  55. package/dist/lib/runs-trace.d.ts +24 -0
  56. package/dist/lib/runs-trace.js +102 -0
  57. package/dist/lib/runtime-identity.d.ts +7 -0
  58. package/dist/lib/runtime-identity.js +35 -0
  59. package/dist/lib/runtime-notifier.d.ts +1 -1
  60. package/dist/lib/runtime-notifier.js +1 -1
  61. package/dist/lib/runtime-triage.d.ts +29 -0
  62. package/dist/lib/runtime-triage.js +76 -0
  63. package/dist/lib/tool-review-scheduler.js +7 -7
  64. package/dist/lib/tools-inspect.d.ts +3 -3
  65. package/dist/lib/tools-inspect.js +241 -707
  66. package/dist/lib/tools-local.js +2 -10
  67. package/dist/lib/tools-message.d.ts +6 -7
  68. package/dist/lib/tools-message.js +95 -396
  69. package/dist/lib/tools-response.d.ts +1 -5
  70. package/dist/lib/tools-response.js +5 -48
  71. package/dist/lib/tools-spawn.js +16 -28
  72. package/dist/lib/tools.d.ts +1 -1
  73. package/dist/lib/tools.js +2 -3
  74. package/dist/lib/trace-projection.d.ts +22 -0
  75. package/dist/lib/trace-projection.js +185 -0
  76. package/dist/recipes/draft-review.json +0 -10
  77. package/dist/recipes/lens-swarm.json +0 -14
  78. package/dist/recipes/music-player.json +10 -19
  79. package/dist/recipes/pipeline-architect-coordinator.json +0 -11
  80. package/dist/recipes/pipeline-artifact-bundle.json +1 -22
  81. package/dist/recipes/pipeline-artifact-report.json +1 -18
  82. package/dist/recipes/pipeline-artifact-write.json +1 -18
  83. package/dist/recipes/pipeline-async-run-ops.json +0 -12
  84. package/dist/recipes/pipeline-checkpoint-continuation.json +0 -14
  85. package/dist/recipes/pipeline-development-tasking.json +0 -12
  86. package/dist/recipes/pipeline-docs-maintenance.json +0 -12
  87. package/dist/recipes/pipeline-media-library.json +0 -12
  88. package/dist/recipes/pipeline-quorum-review.json +0 -12
  89. package/dist/recipes/pipeline-release-readiness.json +0 -12
  90. package/dist/recipes/pipeline-release-summary.json +0 -12
  91. package/dist/recipes/pipeline-repo-health.json +0 -12
  92. package/dist/recipes/pipeline-research-synthesis.json +0 -11
  93. package/dist/recipes/pipeline-review-readiness.json +0 -12
  94. package/dist/recipes/resource-locker.json +27 -0
  95. package/dist/recipes/subagent-artifact.json +0 -9
  96. package/dist/recipes/subagent-checkpoint.json +0 -10
  97. package/dist/recipes/subagent-conflict-report.json +0 -11
  98. package/dist/recipes/subagent-contradiction-map.json +0 -11
  99. package/dist/recipes/subagent-critic.json +0 -11
  100. package/dist/recipes/subagent-evidence-map.json +0 -11
  101. package/dist/recipes/subagent-followup.json +0 -10
  102. package/dist/recipes/subagent-judge.json +0 -11
  103. package/dist/recipes/subagent-merge.json +0 -11
  104. package/dist/recipes/subagent-normalize.json +0 -11
  105. package/dist/recipes/subagent-plan.json +0 -11
  106. package/dist/recipes/subagent-preflight.json +0 -11
  107. package/dist/recipes/subagent-prompt.json +0 -10
  108. package/dist/recipes/subagent-quorum.json +0 -10
  109. package/dist/recipes/subagent-review-coordinator.json +0 -14
  110. package/dist/recipes/subagent-review.json +0 -11
  111. package/dist/recipes/subagent-task-card.json +0 -11
  112. package/dist/recipes/subagent-tools.json +0 -10
  113. package/dist/recipes/subagent-verify.json +0 -11
  114. package/dist/recipes/subagents-prompts.json +0 -10
  115. package/dist/recipes/tool-review.json +0 -10
  116. package/dist/scripts/async-runner.mjs +25 -25
  117. package/dist/scripts/conformance.mjs +4 -2
  118. package/dist/scripts/locker.mjs +196 -69
  119. package/dist/scripts/music-player.mjs +162 -159
  120. package/dist/scripts/recipe-utils.mjs +6 -96
  121. package/dist/scripts/release-gates.mjs +91 -0
  122. package/dist/scripts/validate-recipe.mjs +8 -57
  123. package/dist/skills/actors/SKILL.md +57 -265
  124. package/dist/skills/swarm/SKILL.md +10 -34
  125. package/docs/0.43-baseline.md +39 -0
  126. package/docs/README.md +4 -6
  127. package/docs/actor-inspector.md +26 -64
  128. package/docs/async-runs.md +81 -328
  129. package/docs/command-templates.md +8 -118
  130. package/docs/recipe-library.md +55 -182
  131. package/docs/releasing.md +28 -0
  132. package/docs/template-recipes.md +76 -289
  133. package/docs/tool-registry.md +41 -161
  134. package/fixtures/protocol/control-endpoint.json +6 -0
  135. package/fixtures/protocol/control-record.json +9 -0
  136. package/fixtures/protocol/recipe-summary.json +4 -12
  137. package/fixtures/protocol/trace-event.json +9 -0
  138. package/index.ts +1 -1
  139. package/lib/async-runs.ts +218 -204
  140. package/lib/automatic-review-runtime.ts +7 -7
  141. package/lib/command-templates.ts +44 -4
  142. package/lib/control-projection.ts +105 -0
  143. package/lib/control.ts +117 -0
  144. package/lib/draft-sleep.ts +3 -3
  145. package/lib/execution-sessions.ts +111 -0
  146. package/lib/file-state.ts +84 -64
  147. package/lib/inspector-actions.ts +2 -2
  148. package/lib/inspector-command.ts +3 -3
  149. package/lib/inspector-overlay.ts +617 -1126
  150. package/lib/inspector.ts +46 -979
  151. package/lib/limits.ts +7 -3
  152. package/lib/observability.ts +60 -101
  153. package/lib/prompts.ts +3 -3
  154. package/lib/recipe-control.ts +52 -0
  155. package/lib/recipes-discovery.ts +2 -0
  156. package/lib/recipes-references.ts +9 -45
  157. package/lib/review-control.ts +4 -5
  158. package/lib/review-projection.ts +1 -5
  159. package/lib/run-ui-runtime.ts +2 -2
  160. package/lib/runs-control-delivery.ts +209 -0
  161. package/lib/runs-controls.ts +213 -0
  162. package/lib/runs-retention.ts +38 -3
  163. package/lib/runs-start.ts +4 -2
  164. package/lib/runs-status.ts +11 -6
  165. package/lib/runs-trace.ts +136 -0
  166. package/lib/runtime-identity.ts +39 -0
  167. package/lib/runtime-notifier.ts +1 -1
  168. package/lib/runtime-triage.ts +120 -0
  169. package/lib/tool-review-scheduler.ts +7 -7
  170. package/lib/tools-inspect.ts +283 -900
  171. package/lib/tools-local.ts +2 -12
  172. package/lib/tools-message.ts +112 -520
  173. package/lib/tools-response.ts +5 -64
  174. package/lib/tools-spawn.ts +16 -32
  175. package/lib/tools.ts +5 -6
  176. package/lib/trace-projection.ts +244 -0
  177. package/package.json +2 -1
  178. package/recipes/draft-review.json +0 -10
  179. package/recipes/lens-swarm.json +0 -14
  180. package/recipes/music-player.json +10 -19
  181. package/recipes/pipeline-architect-coordinator.json +0 -11
  182. package/recipes/pipeline-artifact-bundle.json +1 -22
  183. package/recipes/pipeline-artifact-report.json +1 -18
  184. package/recipes/pipeline-artifact-write.json +1 -18
  185. package/recipes/pipeline-async-run-ops.json +0 -12
  186. package/recipes/pipeline-checkpoint-continuation.json +0 -14
  187. package/recipes/pipeline-development-tasking.json +0 -12
  188. package/recipes/pipeline-docs-maintenance.json +0 -12
  189. package/recipes/pipeline-media-library.json +0 -12
  190. package/recipes/pipeline-quorum-review.json +0 -12
  191. package/recipes/pipeline-release-readiness.json +0 -12
  192. package/recipes/pipeline-release-summary.json +0 -12
  193. package/recipes/pipeline-repo-health.json +0 -12
  194. package/recipes/pipeline-research-synthesis.json +0 -11
  195. package/recipes/pipeline-review-readiness.json +0 -12
  196. package/recipes/resource-locker.json +27 -0
  197. package/recipes/subagent-artifact.json +0 -9
  198. package/recipes/subagent-checkpoint.json +0 -10
  199. package/recipes/subagent-conflict-report.json +0 -11
  200. package/recipes/subagent-contradiction-map.json +0 -11
  201. package/recipes/subagent-critic.json +0 -11
  202. package/recipes/subagent-evidence-map.json +0 -11
  203. package/recipes/subagent-followup.json +0 -10
  204. package/recipes/subagent-judge.json +0 -11
  205. package/recipes/subagent-merge.json +0 -11
  206. package/recipes/subagent-normalize.json +0 -11
  207. package/recipes/subagent-plan.json +0 -11
  208. package/recipes/subagent-preflight.json +0 -11
  209. package/recipes/subagent-prompt.json +0 -10
  210. package/recipes/subagent-quorum.json +0 -10
  211. package/recipes/subagent-review-coordinator.json +0 -14
  212. package/recipes/subagent-review.json +0 -11
  213. package/recipes/subagent-task-card.json +0 -11
  214. package/recipes/subagent-tools.json +0 -10
  215. package/recipes/subagent-verify.json +0 -11
  216. package/recipes/subagents-prompts.json +0 -10
  217. package/recipes/tool-review.json +0 -10
  218. package/scripts/async-runner.mjs +25 -25
  219. package/scripts/conformance.mjs +4 -2
  220. package/scripts/locker.mjs +196 -69
  221. package/scripts/music-player.mjs +162 -159
  222. package/scripts/recipe-utils.mjs +6 -96
  223. package/scripts/release-gates.mjs +91 -0
  224. package/scripts/validate-recipe.mjs +8 -57
  225. package/skills/actors/SKILL.md +57 -265
  226. package/skills/swarm/SKILL.md +10 -34
  227. package/dist/fixtures/protocol/actor-message-branch.json +0 -13
  228. package/dist/fixtures/protocol/mailbox-contract.json +0 -15
  229. package/dist/fixtures/protocol/room-message.json +0 -11
  230. package/dist/fixtures/protocol/room-roster.json +0 -11
  231. package/dist/fixtures/protocol/run-inbox-message.json +0 -9
  232. package/dist/fixtures/protocol/run-outbox-event.json +0 -9
  233. package/dist/lib/mailbox-loop.d.ts +0 -41
  234. package/dist/lib/mailbox-loop.js +0 -60
  235. package/dist/lib/messages.d.ts +0 -25
  236. package/dist/lib/messages.js +0 -122
  237. package/dist/lib/rooms.d.ts +0 -104
  238. package/dist/lib/rooms.js +0 -647
  239. package/dist/lib/runs-mailbox.d.ts +0 -25
  240. package/dist/lib/runs-mailbox.js +0 -146
  241. package/dist/lib/runs-messages.d.ts +0 -15
  242. package/dist/lib/runs-messages.js +0 -179
  243. package/dist/lib/runs-outbox.d.ts +0 -41
  244. package/dist/lib/runs-outbox.js +0 -87
  245. package/dist/lib/tools-mailbox.d.ts +0 -8
  246. package/dist/lib/tools-mailbox.js +0 -48
  247. package/dist/recipes/actor-worker.json +0 -39
  248. package/dist/recipes/coordinator-locker.json +0 -45
  249. package/dist/recipes/locker.json +0 -45
  250. package/dist/recipes/pipeline-room-swarm.json +0 -50
  251. package/dist/recipes/subagent-message.json +0 -32
  252. package/dist/recipes/utility-actor-message.json +0 -23
  253. package/dist/scripts/actor-worker.mjs +0 -214
  254. package/dist/scripts/coordinator.mjs +0 -799
  255. package/docs/actor-messages.md +0 -225
  256. package/docs/actors-deep-reference.md +0 -66
  257. package/docs/component-recipes.md +0 -148
  258. package/docs/task-first-recipes.md +0 -263
  259. package/fixtures/protocol/actor-message-branch.json +0 -13
  260. package/fixtures/protocol/mailbox-contract.json +0 -15
  261. package/fixtures/protocol/room-message.json +0 -11
  262. package/fixtures/protocol/room-roster.json +0 -11
  263. package/fixtures/protocol/run-inbox-message.json +0 -9
  264. package/fixtures/protocol/run-outbox-event.json +0 -9
  265. package/lib/mailbox-loop.ts +0 -144
  266. package/lib/messages.ts +0 -151
  267. package/lib/rooms.ts +0 -939
  268. package/lib/runs-mailbox.ts +0 -208
  269. package/lib/runs-messages.ts +0 -252
  270. package/lib/runs-outbox.ts +0 -144
  271. package/lib/tools-mailbox.ts +0 -56
  272. package/recipes/actor-worker.json +0 -39
  273. package/recipes/coordinator-locker.json +0 -45
  274. package/recipes/locker.json +0 -45
  275. package/recipes/pipeline-room-swarm.json +0 -50
  276. package/recipes/subagent-message.json +0 -32
  277. package/recipes/utility-actor-message.json +0 -23
  278. package/scripts/actor-worker.mjs +0 -214
  279. package/scripts/coordinator.mjs +0 -799
@@ -6,22 +6,21 @@ import type { CommandTemplateFailureScope, CommandTemplateValue } from "./comman
6
6
  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
- import { type RunOutboxEvent } from "./runs-outbox.ts";
10
9
  import { type RunProcessIdentity } from "./runs-process.ts";
10
+ import * as RuntimeIdentity from "./runtime-identity.ts";
11
11
  import * as RunsIndex from "./runs-index.ts";
12
12
  import * as RunsParentTeardown from "./runs-parent-teardown.ts";
13
- import { type ProcessRunInboxResult, type RunInboxMessage, type RunInboxStatus } from "./runs-mailbox.ts";
14
- import { type SendRunMessageOptions } from "./runs-messages.ts";
13
+ import { type DeliverRunControlOptions, type DeliverRunControlRequest } from "./runs-control-delivery.ts";
15
14
  import { type AsyncRunStatus } from "./runs-status.ts";
16
15
  export type AsyncRunLaunchSource = "spawn" | "tool";
17
16
  export interface AsyncRunControlEndpoint {
18
17
  path: string;
19
- type: "fifo" | "mailbox" | "named-pipe";
18
+ type: "fifo" | "named-pipe";
20
19
  }
21
20
  export declare function normalizeRunTransportContext(value: unknown): Record<string, string | number | boolean> | undefined;
22
21
  export interface AsyncRunStartParams {
23
22
  async?: boolean;
24
- control?: AsyncRunControlEndpoint;
23
+ control_endpoint?: AsyncRunControlEndpoint;
25
24
  file?: string;
26
25
  launch_source?: AsyncRunLaunchSource;
27
26
  lifecycleHooks?: {
@@ -49,7 +48,7 @@ export interface AsyncRunStartParams {
49
48
  accept_output?: "review_evidence";
50
49
  output?: string;
51
50
  artifacts?: Record<string, RunArtifactDeclaration>;
52
- mailbox?: RecipesReferences.TemplateRecipeMailbox;
51
+ control?: string[];
53
52
  notification_policy?: "normal" | "silent";
54
53
  retire_when?: "children_terminal";
55
54
  retry?: number | string;
@@ -79,13 +78,14 @@ export interface AsyncRunMeta {
79
78
  run: string;
80
79
  run_instance_id: string;
81
80
  state_dir: string;
81
+ state_schema: typeof RuntimeIdentity.RUN_STATE_SCHEMA;
82
82
  status: AsyncRunStatus;
83
83
  tool?: string;
84
84
  template: CommandTemplateValue;
85
85
  values: Record<string, unknown>;
86
86
  artifacts?: Record<string, RunArtifactDeclaration>;
87
- control?: AsyncRunControlEndpoint;
88
- mailbox?: RecipesReferences.TemplateRecipeMailbox;
87
+ control?: string[];
88
+ control_endpoint?: AsyncRunControlEndpoint;
89
89
  model_policy?: CurrentPolicyProvenance;
90
90
  notification_policy?: "normal" | "silent";
91
91
  process_identity?: RunProcessIdentity;
@@ -97,8 +97,6 @@ export { safeRunId } from "./runs-identity.ts";
97
97
  export { resolveArtifactManifest } from "./runs-artifacts.ts";
98
98
  export type { RunArtifactDeclaration, RunArtifactManifestEntry, } from "./runs-artifacts.ts";
99
99
  export declare function startRun(params: AsyncRunStartParams, cwd: string): AsyncRunMeta;
100
- export { parseRunOutboxEventLine } from "./runs-outbox.ts";
101
- export type { RunOutboxDelivery, RunOutboxEvent, RunOutboxLevel, } from "./runs-outbox.ts";
102
100
  export declare function getRunStatus(runOrDir: string): Record<string, unknown>;
103
101
  export type { RunStateIndexEntry } from "./runs-index.ts";
104
102
  export declare function listRunStateDirs(stateRoot?: string): string[];
@@ -113,32 +111,11 @@ export declare function teardownRunsOwnedByParent(ownerId: string | undefined, s
113
111
  trigger?: string;
114
112
  }): ParentRunsTeardownSummaryResult;
115
113
  export declare function tailRun(runOrDir: string, lines?: number): string;
116
- export declare function readRunEvents(runOrDir: string, lines?: number): RunOutboxEvent[];
117
- export type { ProcessRunInboxResult, RunInboxMessage, RunInboxStatus, } from "./runs-mailbox.ts";
118
- export declare function readRunInboxMessages(runOrDir: string, lines?: number): RunInboxMessage[];
119
- export declare function updateRunInboxMessageStatus(runOrDir: string, id: string, nextStatus: RunInboxStatus, metadata?: Record<string, unknown>): boolean;
120
- export declare function claimRunInboxMessage(runOrDir: string, owner?: string, statuses?: string[]): RunInboxMessage | undefined;
121
- export declare function processRunInboxMessages(runOrDir: string, handler: (message: RunInboxMessage) => Promise<void> | void, options?: {
122
- limit?: number;
123
- owner?: string;
124
- statuses?: string[];
125
- }): Promise<ProcessRunInboxResult>;
126
- export declare function appendRunOutboxEvent(runOrDir: string, event: {
127
- body?: unknown;
128
- correlation_id?: string;
129
- data?: unknown;
130
- delivery?: string;
131
- event?: string;
132
- from?: string;
133
- level?: string;
134
- metadata?: Record<string, unknown>;
135
- reply_to?: string;
136
- summary?: string;
137
- to?: string;
138
- type?: string;
139
- }): Record<string, unknown>;
140
- export type { SendRunMessageOptions } from "./runs-messages.ts";
141
- export declare function sendRunMessage(runOrDir: string, message: string, options?: SendRunMessageOptions): Promise<Record<string, unknown>>;
114
+ export type { DeliverRunControlOptions, DeliverRunControlRequest, } from "./runs-control-delivery.ts";
115
+ export interface SendRunControlOptions extends DeliverRunControlOptions {
116
+ ownerId?: string;
117
+ }
118
+ export declare function sendRunControl(runOrDir: string, request: DeliverRunControlRequest, options?: SendRunControlOptions): Promise<Record<string, unknown>>;
142
119
  export { getRunProcessSignalPlan } from "./runs-control.ts";
143
120
  export type { RunProcessSignalPlan } from "./runs-control.ts";
144
121
  export interface RunControlExpectation {
@@ -149,8 +126,8 @@ export interface RunControlExpectation {
149
126
  export declare function markRunTerminalNotificationHandled(stateDir: string, status: string): void;
150
127
  export declare function recordRunTerminalDeliveryFailure(stateDir: string, status: string, error: unknown): void;
151
128
  export declare function cancelRun(runOrDir: string, expected?: RunControlExpectation): Record<string, unknown>;
152
- export declare function archiveRun(runOrDir: string): Record<string, unknown>;
129
+ export declare function archiveRun(runOrDir: string, expected?: RunControlExpectation): Record<string, unknown>;
153
130
  export declare function pruneRun(runOrDir: string, options?: {
154
131
  preserveArtifacts?: boolean;
155
- }): Record<string, unknown>;
132
+ }, expected?: RunControlExpectation): Record<string, unknown>;
156
133
  export declare function killRun(runOrDir: string, expected?: RunControlExpectation): Record<string, unknown>;
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import { spawn } from "node:child_process";
6
6
  import { randomUUID } from "node:crypto";
7
- import { closeSync, existsSync, mkdirSync, openSync, readdirSync, statSync, writeFileSync, } from "node:fs";
7
+ import { closeSync, existsSync, mkdirSync, openSync, readdirSync, statSync, } from "node:fs";
8
8
  import { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
9
9
  import { fileURLToPath } from "node:url";
10
10
  import { writeJsonAtomic } from "./file-state.js";
@@ -15,16 +15,17 @@ import * as RecipesUsage from "./recipes-usage.js";
15
15
  import { resolveArtifactPaths, } from "./runs-artifacts.js";
16
16
  import { safeRunId } from "./runs-identity.js";
17
17
  import { buildTerminalProgress, markTerminalHandled, signalOwnedRunProcess, } from "./runs-control.js";
18
- import { buildRunOutboxEventPayload, parseRunOutboxEventLine, } from "./runs-outbox.js";
19
18
  import { claimRunStateDirectory } from "./runs-ownership.js";
20
- import { archiveTerminalRun, pruneTerminalRun } from "./runs-retention.js";
19
+ import { appendRunRetentionEvidence, archiveTerminalRun, pruneTerminalRun, } from "./runs-retention.js";
21
20
  import { captureRunProcessIdentity, verifyRunProcessIdentity, } from "./runs-process.js";
21
+ import * as RuntimeIdentity from "./runtime-identity.js";
22
22
  import * as RunsStart from "./runs-start.js";
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 { claimRunInboxMessageInStateDir, parseRunInboxLine, processRunInboxMessagesInStateDir, runInboxFile, updateRunInboxMessageStatusInStateDir, } from "./runs-mailbox.js";
26
- import { deliverRunMessage, } from "./runs-messages.js";
27
- import { buildRunStatus, tailFile, tailLines, } from "./runs-status.js";
26
+ import { appendRunControlInStateDir, updateRunControlStatusInStateDir, } from "./runs-controls.js";
27
+ import { deliverRunControl, } from "./runs-control-delivery.js";
28
+ import { buildRunStatus, tailFile, } from "./runs-status.js";
28
29
  import { readJsonFileResilient } from "./state-readers.js";
29
30
  const RUNNER_IDENTITY_GRACE_MS = 5000;
30
31
  export function normalizeRunTransportContext(value) {
@@ -236,13 +237,25 @@ export function startRun(params, cwd) {
236
237
  const resolved = resolveRunTemplate(startParams);
237
238
  const run = safeRunId(startParams.run_id);
238
239
  const stateDir = resolveStateDir(startParams, run);
240
+ const recipeFile = startParams.file
241
+ ? resolveRecipeFile(startParams.file)
242
+ : undefined;
243
+ const packagedRecipeRoot = resolve(Paths.getPackagedRecipeRoot());
244
+ const recipeRelation = recipeFile
245
+ ? relative(packagedRecipeRoot, recipeFile)
246
+ : undefined;
247
+ const packagedRepo = startParams.defaults?.repo === "~/.pi/agent/extensions/pi-actors" &&
248
+ recipeRelation &&
249
+ !recipeRelation.startsWith("..") &&
250
+ !isAbsolute(recipeRelation)
251
+ ? dirname(packagedRecipeRoot)
252
+ : undefined;
239
253
  const values = {
254
+ ...(packagedRepo ? { repo: packagedRepo } : {}),
240
255
  ...(startParams.values || {}),
241
- actor_address: `run:${run}`,
242
- communication_file: join(stateDir, "communication.json"),
243
- default_room: `room:${run}`,
244
256
  run_id: run,
245
257
  state_dir: stateDir,
258
+ trace_file: join(stateDir, "trace.jsonl"),
246
259
  };
247
260
  const modelPolicy = describeCurrentPolicyProvenance({
248
261
  defaults: startParams.defaults,
@@ -261,9 +274,6 @@ export function startRun(params, cwd) {
261
274
  prepareStateDirForStart(stateDir);
262
275
  const stdout = join(stateDir, "stdout.log");
263
276
  const stderr = join(stateDir, "stderr.log");
264
- const recipeFile = startParams.file
265
- ? resolveRecipeFile(startParams.file)
266
- : undefined;
267
277
  const recipe = startParams.name || getRunIdFromFile(recipeFile);
268
278
  const includeActorRecipeContext = startParams.actor_context !== false &&
269
279
  startParams.actor_context !== "off";
@@ -300,6 +310,7 @@ export function startRun(params, cwd) {
300
310
  run,
301
311
  run_instance_id: randomUUID(),
302
312
  state_dir: stateDir,
313
+ state_schema: RuntimeIdentity.RUN_STATE_SCHEMA,
303
314
  status: "running",
304
315
  ...(startParams.tool ? { tool: startParams.tool } : {}),
305
316
  template: resolved.template,
@@ -307,7 +318,9 @@ export function startRun(params, cwd) {
307
318
  model_policy: modelPolicy,
308
319
  ...(artifacts ? { artifacts } : {}),
309
320
  ...(startParams.control ? { control: startParams.control } : {}),
310
- ...(startParams.mailbox ? { mailbox: startParams.mailbox } : {}),
321
+ ...(startParams.control_endpoint
322
+ ? { control_endpoint: startParams.control_endpoint }
323
+ : {}),
311
324
  ...(startParams.notification_policy === "silent"
312
325
  ? { notification_policy: "silent" }
313
326
  : {}),
@@ -340,7 +353,14 @@ export function startRun(params, cwd) {
340
353
  if (processIdentity)
341
354
  meta.process_identity = processIdentity;
342
355
  writeJsonAtomic(join(stateDir, "run.json"), meta);
343
- 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" });
356
+ appendRunTraceEvent(stateDir, {
357
+ kind: "run.start",
358
+ summary: `Run ${run} started`,
359
+ data: {
360
+ pid: meta.pid,
361
+ run_instance_id: meta.run_instance_id,
362
+ },
363
+ });
344
364
  child.unref();
345
365
  return meta;
346
366
  }
@@ -348,7 +368,6 @@ export function startRun(params, cwd) {
348
368
  releaseStartLock();
349
369
  }
350
370
  }
351
- export { parseRunOutboxEventLine } from "./runs-outbox.js";
352
371
  function resolveRunStateDir(runOrDir) {
353
372
  return resolve(/[\\/]/u.test(runOrDir)
354
373
  ? runOrDir
@@ -403,17 +422,19 @@ export function teardownRunsOwnedByParent(ownerId, stateRoot = DEFAULT_STATE_ROO
403
422
  current.run_instance_id !== attempt.runInstanceId) {
404
423
  throw new Error("run generation changed before teardown evidence");
405
424
  }
406
- writeFileSync(join(attempt.stateDir, "events.jsonl"), `${JSON.stringify({
407
- event: "run.parent_teardown",
408
- ownerId: attempt.ownerId,
409
- outcome: attempt.outcome,
410
- reason: attempt.reason,
411
- run: attempt.run,
412
- run_instance_id: attempt.runInstanceId,
413
- type: "control.kill",
414
- trigger: options.trigger ?? "parent_shutdown",
415
- ts: new Date().toISOString(),
416
- })}\n`, { flag: "a" });
425
+ appendRunTraceEvent(attempt.stateDir, {
426
+ kind: "run.parent_teardown",
427
+ summary: `Parent teardown ${attempt.outcome}`,
428
+ data: {
429
+ outcome: attempt.outcome,
430
+ owner_id: attempt.ownerId,
431
+ reason: attempt.reason,
432
+ run: attempt.run,
433
+ run_instance_id: attempt.runInstanceId,
434
+ trigger: options.trigger ?? "parent_shutdown",
435
+ },
436
+ ...(attempt.outcome === "failed" ? { level: "error" } : {}),
437
+ });
417
438
  },
418
439
  });
419
440
  const summaryPath = join(stateRoot, "teardown", `${Date.now()}-${randomUUID()}.json`);
@@ -437,73 +458,43 @@ export function teardownRunsOwnedByParent(ownerId, stateRoot = DEFAULT_STATE_ROO
437
458
  export function tailRun(runOrDir, lines = 40) {
438
459
  const status = getRunStatus(runOrDir);
439
460
  const stateDir = String(status.state_dir);
440
- const events = tailFile(join(stateDir, "events.jsonl"), lines);
441
- if (events)
442
- return events;
461
+ const trace = tailFile(join(stateDir, "trace.jsonl"), lines);
462
+ if (trace)
463
+ return trace;
443
464
  return (tailFile(join(stateDir, "stdout.log"), lines) ||
444
465
  tailFile(join(stateDir, "stderr.log"), lines));
445
466
  }
446
- export function readRunEvents(runOrDir, lines = 40) {
447
- const status = getRunStatus(runOrDir);
448
- const stateDir = String(status.state_dir);
449
- const run = String(status.run ?? runOrDir);
450
- return tailLines(join(stateDir, "outbox.jsonl"), lines)
451
- .map((line, index) => parseRunOutboxEventLine(line, run, stateDir, index))
452
- .filter((event) => Boolean(event));
453
- }
454
- export function readRunInboxMessages(runOrDir, lines = 40) {
455
- const status = getRunStatus(runOrDir);
456
- const stateDir = String(status.state_dir);
457
- return tailLines(runInboxFile(stateDir), lines)
458
- .map(parseRunInboxLine)
459
- .filter((message) => Boolean(message));
460
- }
461
- export function updateRunInboxMessageStatus(runOrDir, id, nextStatus, metadata = {}) {
462
- const status = getRunStatus(runOrDir);
463
- const stateDir = String(status.state_dir);
464
- return updateRunInboxMessageStatusInStateDir(stateDir, id, nextStatus, metadata);
465
- }
466
- export function claimRunInboxMessage(runOrDir, owner = "runtime", statuses = ["queued"]) {
467
- const status = getRunStatus(runOrDir);
468
- const stateDir = String(status.state_dir);
469
- return claimRunInboxMessageInStateDir(stateDir, owner, statuses);
470
- }
471
- export async function processRunInboxMessages(runOrDir, handler, options = {}) {
472
- const status = getRunStatus(runOrDir);
473
- return processRunInboxMessagesInStateDir(String(status.state_dir), handler, options);
474
- }
475
- export function appendRunOutboxEvent(runOrDir, event) {
476
- const status = getRunStatus(runOrDir);
477
- const stateDir = String(status.state_dir);
478
- const run = String(status.run ?? runOrDir);
479
- const payload = buildRunOutboxEventPayload(run, event);
480
- const line = `${JSON.stringify(payload)}\n`;
481
- writeFileSync(join(stateDir, "outbox.jsonl"), line, { flag: "a" });
482
- return {
483
- bytes: Buffer.byteLength(line),
484
- outbox: "outbox.jsonl",
485
- run,
486
- sent: true,
487
- state_dir: stateDir,
488
- };
489
- }
490
- export async function sendRunMessage(runOrDir, message, options = {}) {
491
- const status = getRunStatus(runOrDir);
492
- const stateDir = String(status.state_dir);
493
- const run = String(status.run ?? runOrDir);
494
- const pid = Number(status.pid || 0);
495
- const identity = verifyRunProcessIdentity(pid, status.process_identity);
496
- if (status.status !== "running") {
497
- if (identity.status === "owner_mismatch" ||
498
- identity.status === "unsupported_proof") {
499
- throw new Error(`Run process identity ${identity.status}: ${run}`);
467
+ export async function sendRunControl(runOrDir, request, options = {}) {
468
+ const stateDir = resolveRunStateDir(runOrDir);
469
+ const releaseControlLock = RunsStart.acquireStateStartLock(stateDir);
470
+ try {
471
+ const status = getRunStatus(stateDir);
472
+ const run = String(status.run ?? runOrDir);
473
+ if (options.ownerId !== undefined && status.ownerId !== options.ownerId) {
474
+ throw Object.assign(new Error(`Run ownership changed: ${run}`), {
475
+ reason: "owner_mismatch",
476
+ });
477
+ }
478
+ if (status.run_instance_id !== request.run_instance_id) {
479
+ throw Object.assign(new Error(`Run generation changed: ${run}`), {
480
+ reason: "generation_mismatch",
481
+ });
482
+ }
483
+ if (status.status !== "running") {
484
+ throw Object.assign(new Error(`Run is not running: ${run}`), {
485
+ reason: "terminal_state",
486
+ });
500
487
  }
501
- throw new Error(`Run is not running: ${run}`);
488
+ const pid = Number(status.pid || 0);
489
+ const identity = verifyRunProcessIdentity(pid, status.process_identity);
490
+ if (!identity.valid) {
491
+ throw Object.assign(new Error(`Run process identity ${identity.status}: ${run}`), { reason: `process_identity_${identity.status}` });
492
+ }
493
+ return await deliverRunControl(run, stateDir, request, options);
502
494
  }
503
- if (!identity.valid) {
504
- throw new Error(`Run process identity ${identity.status}: ${run}`);
495
+ finally {
496
+ releaseControlLock();
505
497
  }
506
- return deliverRunMessage(status, run, stateDir, message, options);
507
498
  }
508
499
  export { getRunProcessSignalPlan } from "./runs-control.js";
509
500
  function markTerminalProgress(stateDir, phase) {
@@ -513,9 +504,9 @@ function markTerminalProgress(stateDir, phase) {
513
504
  : undefined;
514
505
  writeJsonAtomic(join(stateDir, "progress.json"), buildTerminalProgress(progress, phase));
515
506
  }
516
- function finalizeInterruptedReviewEvidence(stateDir, phase, signal) {
517
- const evidencePath = join(stateDir, "review-evidence.json");
518
- const manifest = readJson(evidencePath);
507
+ function finalizeInterruptedExecution(stateDir, phase, signal) {
508
+ const executionPath = join(stateDir, "execution.json");
509
+ const manifest = readJson(executionPath);
519
510
  if (!manifest || typeof manifest !== "object" || Array.isArray(manifest))
520
511
  return;
521
512
  const record = manifest;
@@ -564,7 +555,7 @@ function finalizeInterruptedReviewEvidence(stateDir, phase, signal) {
564
555
  : {}),
565
556
  };
566
557
  });
567
- writeJsonAtomic(evidencePath, {
558
+ writeJsonAtomic(executionPath, {
568
559
  ...record,
569
560
  status: phase,
570
561
  commands,
@@ -585,43 +576,80 @@ function stopRun(runOrDir, signal, event, expected = {}) {
585
576
  status.run_instance_id !== expected.runInstanceId) {
586
577
  return { stopped: false, reason: "run generation changed", status };
587
578
  }
579
+ const control = event === "run.kill" && typeof status.run_instance_id === "string"
580
+ ? appendRunControlInStateDir(stateDir, {
581
+ action: "kill",
582
+ run_instance_id: status.run_instance_id,
583
+ })
584
+ : undefined;
585
+ if (control) {
586
+ updateRunControlStatusInStateDir(stateDir, control.id, "claimed", {}, ["queued"]);
587
+ }
588
+ const finish = (result) => {
589
+ if (!control)
590
+ return result;
591
+ const handled = result.stopped === true;
592
+ updateRunControlStatusInStateDir(stateDir, control.id, handled ? "handled" : "failed", handled ? {} : { error: String(result.reason ?? "kill rejected") }, ["claimed"]);
593
+ return { ...result, control_id: control.id };
594
+ };
588
595
  const pid = Number(status.pid || 0);
589
596
  if (status.status !== "running" && status.status !== "exited") {
590
- return { stopped: false, reason: "not running", status };
597
+ return finish({ stopped: false, reason: "not running", status });
591
598
  }
592
599
  const identity = verifyRunProcessIdentity(pid, status.process_identity);
593
600
  if (status.status === "exited") {
594
601
  if (identity.status === "owner_mismatch" ||
595
602
  identity.status === "unsupported_proof") {
596
- return {
603
+ return finish({
597
604
  stopped: false,
598
605
  reason: identity.status.replaceAll("_", " "),
599
606
  process_identity_status: identity.status,
600
607
  status,
601
- };
608
+ });
602
609
  }
603
- return { stopped: false, reason: "not running", status };
610
+ return finish({ stopped: false, reason: "not running", status });
604
611
  }
605
612
  if (!identity.valid) {
606
- return {
613
+ return finish({
607
614
  stopped: false,
608
615
  reason: identity.status.replaceAll("_", " "),
609
616
  process_identity_status: identity.status,
610
617
  status,
611
- };
618
+ });
612
619
  }
613
- const signalResult = signalOwnedRunProcess(pid, signal, status.process_identity);
614
- writeFileSync(join(stateDir, "events.jsonl"), `${JSON.stringify({ event, pid, signal, ...signalResult, ts: new Date().toISOString() })}\n`, { flag: "a" });
620
+ let signalResult;
621
+ try {
622
+ signalResult = signalOwnedRunProcess(pid, signal, status.process_identity);
623
+ }
624
+ catch (error) {
625
+ if (control) {
626
+ updateRunControlStatusInStateDir(stateDir, control.id, "failed", {
627
+ error: error instanceof Error ? error.message : String(error),
628
+ });
629
+ }
630
+ throw error;
631
+ }
632
+ appendRunTraceEvent(stateDir, {
633
+ kind: event,
634
+ summary: `${event === "run.kill" ? "Killed" : "Cancelled"} Run process`,
635
+ data: { pid, signal, ...signalResult },
636
+ });
615
637
  markTerminalHandled(stateDir, { event, signal });
616
638
  if (event === "run.kill") {
617
- finalizeInterruptedReviewEvidence(stateDir, "killed", signal);
639
+ finalizeInterruptedExecution(stateDir, "killed", signal);
618
640
  markTerminalProgress(stateDir, "killed");
619
641
  }
620
642
  if (event === "run.cancel") {
621
- finalizeInterruptedReviewEvidence(stateDir, "cancelled", signal);
643
+ finalizeInterruptedExecution(stateDir, "cancelled", signal);
622
644
  markTerminalProgress(stateDir, "cancelled");
623
645
  }
624
- return { stopped: true, pid, signal, ...signalResult, state_dir: stateDir };
646
+ return finish({
647
+ stopped: true,
648
+ pid,
649
+ signal,
650
+ ...signalResult,
651
+ state_dir: stateDir,
652
+ });
625
653
  }
626
654
  finally {
627
655
  releaseControlLock();
@@ -653,18 +681,52 @@ export function cancelRun(runOrDir, expected = {}) {
653
681
  ? { cancelled: result.stopped, ...result }
654
682
  : result;
655
683
  }
656
- function assertTerminalRun(runOrDir) {
657
- const status = getRunStatus(runOrDir);
658
- if (status.status === "running") {
659
- throw new Error("Only terminal runs can be archived or pruned.");
684
+ function retainRun(runOrDir, action, expected, options = {}) {
685
+ const stateDir = resolveRunStateDir(runOrDir);
686
+ const releaseControlLock = RunsStart.acquireStateStartLock(stateDir);
687
+ let status;
688
+ let evidenceId;
689
+ try {
690
+ expected.onLocked?.();
691
+ status = getRunStatus(stateDir);
692
+ if (expected.ownerId !== undefined && status.ownerId !== expected.ownerId) {
693
+ return { [`${action}d`]: false, reason: "ownership changed", status };
694
+ }
695
+ if (expected.runInstanceId !== undefined &&
696
+ status.run_instance_id !== expected.runInstanceId) {
697
+ return { [`${action}d`]: false, reason: "run generation changed", status };
698
+ }
699
+ if (status.status === "running") {
700
+ throw new Error("Only terminal runs can be archived or pruned.");
701
+ }
702
+ evidenceId = appendRunRetentionEvidence(status, action, "queued");
703
+ const result = action === "archive"
704
+ ? archiveTerminalRun(status)
705
+ : pruneTerminalRun(status, options);
706
+ appendRunRetentionEvidence(status, action, "handled", {
707
+ id: evidenceId,
708
+ result,
709
+ });
710
+ return { ...result, retention_id: evidenceId };
711
+ }
712
+ catch (error) {
713
+ if (status && evidenceId) {
714
+ appendRunRetentionEvidence(status, action, "failed", {
715
+ error: error instanceof Error ? error.message : String(error),
716
+ id: evidenceId,
717
+ });
718
+ }
719
+ throw error;
720
+ }
721
+ finally {
722
+ releaseControlLock();
660
723
  }
661
- return status;
662
724
  }
663
- export function archiveRun(runOrDir) {
664
- return archiveTerminalRun(assertTerminalRun(runOrDir));
725
+ export function archiveRun(runOrDir, expected = {}) {
726
+ return retainRun(runOrDir, "archive", expected);
665
727
  }
666
- export function pruneRun(runOrDir, options = {}) {
667
- return pruneTerminalRun(assertTerminalRun(runOrDir), options);
728
+ export function pruneRun(runOrDir, options = {}, expected = {}) {
729
+ return retainRun(runOrDir, "prune", expected, options);
668
730
  }
669
731
  export function killRun(runOrDir, expected = {}) {
670
732
  const result = stopRun(runOrDir, "SIGKILL", "run.kill", expected);
@@ -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
  }