@llblab/pi-actors 0.44.0 → 0.45.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 (51) hide show
  1. package/AGENTS.md +2 -0
  2. package/CHANGELOG.md +13 -0
  3. package/README.md +5 -1
  4. package/dist/index.d.ts +1 -2
  5. package/dist/index.js +10 -106
  6. package/dist/lib/async-runs.d.ts +2 -0
  7. package/dist/lib/async-runs.js +33 -4
  8. package/dist/lib/config.js +1 -0
  9. package/dist/lib/extension-runtime.d.ts +21 -0
  10. package/dist/lib/extension-runtime.js +116 -0
  11. package/dist/lib/inspector.js +8 -2
  12. package/dist/lib/recipes-discovery.js +20 -3
  13. package/dist/lib/recipes-references.d.ts +10 -0
  14. package/dist/lib/recipes-references.js +168 -5
  15. package/dist/lib/schema.d.ts +3 -0
  16. package/dist/lib/schema.js +34 -1
  17. package/dist/lib/tools-inspect.js +10 -0
  18. package/dist/lib/tools-local.js +27 -10
  19. package/dist/lib/tools-spawn.js +2 -2
  20. package/dist/scripts/async-runner.mjs +0 -16
  21. package/dist/scripts/executable-block-style.d.mts +1 -0
  22. package/dist/scripts/executable-block-style.mjs +73 -0
  23. package/dist/scripts/locker.mjs +0 -21
  24. package/dist/scripts/recipe-utils.mjs +0 -1
  25. package/dist/scripts/release-gates.mjs +14 -11
  26. package/dist/scripts/validate-recipe.mjs +0 -1
  27. package/dist/skills/actors/SKILL.md +2 -2
  28. package/docs/command-templates.md +2 -2
  29. package/docs/recipe-library.md +2 -0
  30. package/docs/template-recipes.md +10 -4
  31. package/docs/tool-registry.md +1 -1
  32. package/index.ts +18 -107
  33. package/lib/async-runs.ts +43 -9
  34. package/lib/config.ts +4 -0
  35. package/lib/extension-runtime.ts +139 -0
  36. package/lib/inspector.ts +16 -2
  37. package/lib/recipes-discovery.ts +23 -3
  38. package/lib/recipes-references.ts +227 -7
  39. package/lib/schema.ts +53 -1
  40. package/lib/tools-inspect.ts +11 -0
  41. package/lib/tools-local.ts +49 -19
  42. package/lib/tools-spawn.ts +2 -2
  43. package/package.json +2 -2
  44. package/scripts/async-runner.mjs +0 -16
  45. package/scripts/executable-block-style.d.mts +1 -0
  46. package/scripts/executable-block-style.mjs +73 -0
  47. package/scripts/locker.mjs +0 -21
  48. package/scripts/recipe-utils.mjs +0 -1
  49. package/scripts/release-gates.mjs +14 -11
  50. package/scripts/validate-recipe.mjs +0 -1
  51. package/skills/actors/SKILL.md +2 -2
@@ -14,6 +14,7 @@ import * as Limits from "./limits.ts";
14
14
  import { computeRunControlCapacity } from "./run-evidence-policy.ts";
15
15
  import * as Paths from "./paths.ts";
16
16
  import * as RecipesDiscovery from "./recipes-discovery.ts";
17
+ import * as RecipesReferences from "./recipes-references.ts";
17
18
  import * as ReviewDiagnostics from "./review-diagnostics.ts";
18
19
  import * as RunsControls from "./runs-controls.ts";
19
20
  import * as RunControlDelivery from "./runs-control-delivery.ts";
@@ -214,8 +215,18 @@ function inspectRecipes(
214
215
  { root: recipeRoot, defaultTool: true, mutableUsage: true },
215
216
  { root: deps.packagedRecipeRoot ?? Paths.getPackagedRecipeRoot() },
216
217
  ]);
218
+ const skillRecipeNamespaces =
219
+ RecipesReferences.getActiveSkillRecipeNamespaces();
217
220
  const summary = {
218
221
  ...RecipesDiscovery.summarizeDiscovery(discovered),
222
+ skill_recipe_namespaces: skillRecipeNamespaces,
223
+ skill_recipe_namespace_diagnostics: Object.entries(skillRecipeNamespaces)
224
+ .filter(([, roots]) => roots.length > 1)
225
+ .map(([name, roots]) => ({
226
+ name,
227
+ roots,
228
+ error: `Ambiguous active Skill Recipe namespace ${name}`,
229
+ })),
219
230
  drafts: RecipesDiscovery.listDraftRecipes(join(recipeRoot, "drafts")),
220
231
  };
221
232
  return {
@@ -67,11 +67,16 @@ function formatRuntimeToolUsageHint(
67
67
  includeRunId: boolean,
68
68
  ): string {
69
69
  const optional = cfg.args.filter((arg) => !required.includes(arg));
70
+ const exampleDefaults = {
71
+ ...Schema.getExplicitToolArgDefaults(cfg.recipe?.args),
72
+ ...cfg.defaults,
73
+ ...(cfg.recipe?.values ?? {}),
74
+ };
70
75
  const example: Record<string, unknown> = {};
71
76
  for (const arg of required)
72
- example[arg] = sampleValueForArg(arg, cfg.argTypes?.[arg], cfg.defaults);
77
+ example[arg] = sampleValueForArg(arg, cfg.argTypes?.[arg], exampleDefaults);
73
78
  for (const arg of optional)
74
- example[arg] = sampleValueForArg(arg, cfg.argTypes?.[arg], cfg.defaults);
79
+ example[arg] = sampleValueForArg(arg, cfg.argTypes?.[arg], exampleDefaults);
75
80
  if (includeRunId) example.run_id = `${cfg.name}-1`;
76
81
  const lines = [
77
82
  `Expected call shape for ${cfg.name}:`,
@@ -85,6 +90,32 @@ function formatRuntimeToolUsageHint(
85
90
  return lines.join("\n");
86
91
  }
87
92
 
93
+ function resolveRecipeToolValues(
94
+ cfg: RegisteredTool,
95
+ callerValues: Record<string, unknown>,
96
+ ctx: RuntimeToolContext,
97
+ ): Record<string, unknown> {
98
+ const recipeRuntimeValues = {
99
+ ...(cfg.recipe?.recipe_dir
100
+ ? { recipe_dir: cfg.recipe.recipe_dir }
101
+ : {}),
102
+ ...(cfg.recipe?.skill_dir ? { skill_dir: cfg.recipe.skill_dir } : {}),
103
+ };
104
+ return Schema.normalizeRuntimeValues(
105
+ ModelContext.withCurrentModelValues(
106
+ {
107
+ ...Schema.getExplicitToolArgDefaults(cfg.recipe?.args),
108
+ ...cfg.defaults,
109
+ ...(cfg.recipe?.values ?? {}),
110
+ ...callerValues,
111
+ ...recipeRuntimeValues,
112
+ },
113
+ ctx,
114
+ ),
115
+ cfg.argTypes,
116
+ );
117
+ }
118
+
88
119
  function formatRuntimeToolArgumentError(
89
120
  cfg: RegisteredTool,
90
121
  error: unknown,
@@ -128,9 +159,18 @@ export function createRuntimeToolDefinition(
128
159
  defaults: cfg.defaults,
129
160
  template: requiredTemplate,
130
161
  };
162
+ const recipeInlineDefaults = Schema.getExplicitToolArgDefaults(
163
+ cfg.recipe?.args,
164
+ );
131
165
  const requiredArgs =
132
166
  isRecipe && cfg.storedArgs !== undefined
133
- ? new Set(cfg.args.filter((arg) => !Object.hasOwn(cfg.defaults, arg)))
167
+ ? new Set(
168
+ cfg.args.filter(
169
+ (arg) =>
170
+ !Object.hasOwn(cfg.defaults, arg) &&
171
+ !Object.hasOwn(recipeInlineDefaults, arg),
172
+ ),
173
+ )
134
174
  : RecipesReferences.isRecipeReference(cfg.template) && !recipeTemplate
135
175
  ? new Set(cfg.args.filter((arg) => !Object.hasOwn(cfg.defaults, arg)))
136
176
  : Schema.getRequiredToolArgNames(requiredTemplateConfig);
@@ -200,13 +240,7 @@ export function createRuntimeToolDefinition(
200
240
  { ...(cfg.recipe?.values ?? {}), ...values },
201
241
  ctx,
202
242
  ),
203
- values: Schema.normalizeRuntimeValues(
204
- ModelContext.withCurrentModelValues(
205
- { ...(cfg.recipe?.values ?? {}), ...cfg.defaults, ...values },
206
- ctx,
207
- ),
208
- cfg.argTypes,
209
- ),
243
+ values: resolveRecipeToolValues(cfg, values, ctx),
210
244
  },
211
245
  ctx.cwd,
212
246
  );
@@ -221,17 +255,13 @@ export function createRuntimeToolDefinition(
221
255
  };
222
256
  }
223
257
  if (isRecipe && recipeTemplate) {
224
- const paramsWithDefaults = ModelContext.withCurrentModelValues(
225
- {
226
- ...(cfg.recipe?.values ?? {}),
227
- ...cfg.defaults,
228
- ...(params as Record<string, unknown>),
229
- },
230
- ctx,
231
- );
232
258
  return await Execution.executeRegisteredTool(
233
259
  { ...cfg, template: recipeTemplate },
234
- Schema.normalizeRuntimeValues(paramsWithDefaults, cfg.argTypes),
260
+ resolveRecipeToolValues(
261
+ cfg,
262
+ params as Record<string, unknown>,
263
+ ctx,
264
+ ),
235
265
  exec,
236
266
  ctx.cwd,
237
267
  signal,
@@ -138,10 +138,10 @@ export function createSpawnToolDefinition<
138
138
  "Optional Run identity in exact run:<id> form.",
139
139
  ),
140
140
  file: Schema.stringSchema(
141
- "Optional template recipe JSON file. Bare names resolve under ~/.pi/agent/recipes.",
141
+ "Optional template Recipe reference. Supports paths, bare user/adjacent/std names, std:<name>, and skill:<skill>/<recipe>.",
142
142
  ),
143
143
  recipe: Schema.stringSchema(
144
- "Alias for file; template recipe JSON file/name to spawn.",
144
+ "Alias for file; qualified Skill/std or ordinary Recipe reference to spawn.",
145
145
  ),
146
146
  template: Schema.unionSchema([
147
147
  Schema.stringSchema("Inline command template string"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-actors",
3
- "version": "0.44.0",
3
+ "version": "0.45.1",
4
4
  "private": false,
5
5
  "description": "Local Actor Kernel for Pi",
6
6
  "keywords": [
@@ -65,7 +65,7 @@
65
65
  "sourceSkills": [
66
66
  "./skills"
67
67
  ],
68
- "image": "https://github.com/llblab/pi-actors/raw/main/banner.jpg"
68
+ "image": "https://raw.githubusercontent.com/llblab/pi-actors/main/banner.jpg"
69
69
  },
70
70
  "peerDependencies": {
71
71
  "@earendil-works/pi-coding-agent": "*",
@@ -74,7 +74,6 @@ function summarizeCommandDetail(commandDetail) {
74
74
 
75
75
  export async function runAsyncRunner(stateDir = process.argv[2]) {
76
76
  if (!stateDir) throw new Error("missing state dir");
77
-
78
77
  const runPath = join(stateDir, "run.json");
79
78
  const progressPath = join(stateDir, "progress.json");
80
79
  const resultPath = join(stateDir, "result.json");
@@ -83,11 +82,9 @@ export async function runAsyncRunner(stateDir = process.argv[2]) {
83
82
  const stderrPath = join(stateDir, "stderr.log");
84
83
  const executionPath = join(stateDir, "execution.json");
85
84
  const meta = JSON.parse(readFileSync(runPath, "utf8"));
86
-
87
85
  function event(kind, data = {}) {
88
86
  appendRunTraceEvent(stateDir, { kind, data });
89
87
  }
90
-
91
88
  function observation(kind, summary, data = {}, delivery = "log", level = "info") {
92
89
  appendRunTraceEvent(stateDir, {
93
90
  kind,
@@ -97,7 +94,6 @@ export async function runAsyncRunner(stateDir = process.argv[2]) {
97
94
  ...(level === "error" ? { level: "error" } : {}),
98
95
  });
99
96
  }
100
-
101
97
  function progress(phase, extra = {}) {
102
98
  writeJsonAtomic(progressPath, {
103
99
  ...(meta.model_policy ? { model_policy: meta.model_policy } : {}),
@@ -106,7 +102,6 @@ export async function runAsyncRunner(stateDir = process.argv[2]) {
106
102
  ...extra,
107
103
  });
108
104
  }
109
-
110
105
  let activeSubagents = 0;
111
106
  let completedSubagents = 0;
112
107
  let promptCounter = 0;
@@ -115,7 +110,6 @@ export async function runAsyncRunner(stateDir = process.argv[2]) {
115
110
  const evidenceRecords = [];
116
111
  const stageOccurrences = new Map();
117
112
  let reportEvidence;
118
-
119
113
  function writeExecutionManifest(status) {
120
114
  writeJsonAtomic(executionPath, {
121
115
  version: 1,
@@ -129,7 +123,6 @@ export async function runAsyncRunner(stateDir = process.argv[2]) {
129
123
  updated_at: new Date().toISOString(),
130
124
  });
131
125
  }
132
-
133
126
  function commandSessionFiles(sessionDir) {
134
127
  if (!sessionDir) return [];
135
128
  try {
@@ -141,7 +134,6 @@ export async function runAsyncRunner(stateDir = process.argv[2]) {
141
134
  return [];
142
135
  }
143
136
  }
144
-
145
137
  function commandEvidenceStartRecord({
146
138
  commandDetail,
147
139
  commandId,
@@ -181,7 +173,6 @@ export async function runAsyncRunner(stateDir = process.argv[2]) {
181
173
  : "not_required",
182
174
  };
183
175
  }
184
-
185
176
  function commandEvidenceRecord({
186
177
  captureDir,
187
178
  commandDetail,
@@ -273,7 +264,6 @@ export async function runAsyncRunner(stateDir = process.argv[2]) {
273
264
  : "rejected",
274
265
  };
275
266
  }
276
-
277
267
  function auditReviewReport(text) {
278
268
  const required = evidenceRecords
279
269
  .filter((record) =>
@@ -294,11 +284,9 @@ export async function runAsyncRunner(stateDir = process.argv[2]) {
294
284
  complete_allowed: missing.length === 0,
295
285
  };
296
286
  }
297
-
298
287
  function getCommandDoneDelivery(result) {
299
288
  return result.code !== 0 || activeSubagents > 0 ? "followup" : "log";
300
289
  }
301
-
302
290
  function progressRunning() {
303
291
  progress("running", {
304
292
  activeSubagents,
@@ -306,14 +294,12 @@ export async function runAsyncRunner(stateDir = process.argv[2]) {
306
294
  failures: subagentFailures,
307
295
  });
308
296
  }
309
-
310
297
  function promptFilePath() {
311
298
  promptCounter += 1;
312
299
  const dir = join(stateDir, "prompts");
313
300
  mkdirSync(dir, { recursive: true });
314
301
  return join(dir, `command-${String(promptCounter).padStart(3, "0")}.md`);
315
302
  }
316
-
317
303
  function readPromptText(promptFile) {
318
304
  if (!promptFile) return undefined;
319
305
  try {
@@ -322,7 +308,6 @@ export async function runAsyncRunner(stateDir = process.argv[2]) {
322
308
  return undefined;
323
309
  }
324
310
  }
325
-
326
311
  async function observedExec(command, args, options) {
327
312
  captureCounter += 1;
328
313
  const commandId = `command-${String(captureCounter).padStart(3, "0")}`;
@@ -485,7 +470,6 @@ export async function runAsyncRunner(stateDir = process.argv[2]) {
485
470
  progressRunning();
486
471
  return result;
487
472
  }
488
-
489
473
  try {
490
474
  event("run.runner.start", { pid: process.pid });
491
475
  progressRunning();
@@ -0,0 +1 @@
1
+ export function executableBlockBlankLines(source: string): number[];
@@ -0,0 +1,73 @@
1
+ /** Detect blank lines inside executable JavaScript and TypeScript blocks. */
2
+
3
+ export function executableBlockBlankLines(source) {
4
+ const lines = source.split(/\r?\n/u);
5
+ const blanks = [];
6
+ const stack = [];
7
+ let blockComment = false;
8
+ let currentExecutableDepth = 0;
9
+ let previousCode = "";
10
+ let quote;
11
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
12
+ const line = lines[lineIndex];
13
+ if (
14
+ /^\s*$/u.test(line) &&
15
+ currentExecutableDepth > 0 &&
16
+ quote !== "`"
17
+ )
18
+ blanks.push(lineIndex + 1);
19
+ let code = "";
20
+ let regularExpression = false;
21
+ for (let index = 0; index < line.length; index += 1) {
22
+ const char = line[index];
23
+ const next = line[index + 1];
24
+ if (blockComment) {
25
+ if (char === "*" && next === "/") {
26
+ blockComment = false;
27
+ index += 1;
28
+ }
29
+ continue;
30
+ }
31
+ if (quote) {
32
+ if (char === "\\") index += 1;
33
+ else if (char === quote) quote = undefined;
34
+ continue;
35
+ }
36
+ if (regularExpression) {
37
+ if (char === "\\") index += 1;
38
+ else if (char === "/") regularExpression = false;
39
+ continue;
40
+ }
41
+ if (char === "/" && next === "*") {
42
+ blockComment = true;
43
+ index += 1;
44
+ continue;
45
+ }
46
+ if (char === "/" && next === "/") break;
47
+ if (
48
+ char === "/" &&
49
+ /^(?:|.*(?:[=(,:;!&|?{}]|\b(?:return|case|throw)))\s*$/u.test(code)
50
+ ) {
51
+ regularExpression = true;
52
+ continue;
53
+ }
54
+ if (char === '"' || char === "'" || char === "`") {
55
+ quote = char;
56
+ continue;
57
+ }
58
+ if (char === "{") {
59
+ const prefix = code.trimEnd() || previousCode;
60
+ const executable =
61
+ /\)\s*$|=>\s*$|\b(?:else|try|finally|do)\s*$/u.test(prefix);
62
+ stack.push(executable);
63
+ if (executable) currentExecutableDepth += 1;
64
+ } else if (char === "}") {
65
+ if (stack.pop()) currentExecutableDepth -= 1;
66
+ }
67
+ code += char;
68
+ }
69
+ if (quote !== "`") quote = undefined;
70
+ if (code.trim()) previousCode = code.trim();
71
+ }
72
+ return blanks;
73
+ }
@@ -65,7 +65,6 @@ async function runLocker(argv = process.argv.slice(2)) {
65
65
  const controlPath = join(stateDir, "control.fifo");
66
66
  let runInstanceId;
67
67
  mkdirSync(stateDir, { recursive: true });
68
-
69
68
  function readJson(path, fallback) {
70
69
  if (!existsSync(path)) return fallback;
71
70
  try {
@@ -74,11 +73,9 @@ async function runLocker(argv = process.argv.slice(2)) {
74
73
  return fallback;
75
74
  }
76
75
  }
77
-
78
76
  function writeJson(path, value) {
79
77
  writeJsonAtomic(path, value);
80
78
  }
81
-
82
79
  function getControlEndpoint() {
83
80
  if (process.platform !== "win32") return { path: controlPath, type: "fifo" };
84
81
  const hash = createHash("sha256")
@@ -87,7 +84,6 @@ async function runLocker(argv = process.argv.slice(2)) {
87
84
  .slice(0, 20);
88
85
  return { path: `\\\\.\\pipe\\pi-actors-locker-${hash}`, type: "named-pipe" };
89
86
  }
90
-
91
87
  function writeControlEndpoint(endpoint) {
92
88
  if (!runInstanceId) throw new Error("Run generation unavailable for Control endpoint");
93
89
  writeJson(join(stateDir, "control-endpoint.json"), {
@@ -96,7 +92,6 @@ async function runLocker(argv = process.argv.slice(2)) {
96
92
  run_instance_id: runInstanceId,
97
93
  });
98
94
  }
99
-
100
95
  function journal(event, data = {}) {
101
96
  const release = acquireFileMutationLock(journalPath);
102
97
  try {
@@ -111,11 +106,9 @@ async function runLocker(argv = process.argv.slice(2)) {
111
106
  writeTextAtomic(journalPath, content);
112
107
  } finally { release(); }
113
108
  }
114
-
115
109
  function encodeJournal(records) {
116
110
  return records.length ? `${records.map((record) => JSON.stringify(record)).join("\n")}\n` : "";
117
111
  }
118
-
119
112
  function emitTrace(kind, summary, data = {}, level = "info") {
120
113
  appendRunTraceEvent(stateDir, {
121
114
  attention: "followup",
@@ -125,11 +118,9 @@ async function runLocker(argv = process.argv.slice(2)) {
125
118
  summary,
126
119
  });
127
120
  }
128
-
129
121
  function now() {
130
122
  return Date.now();
131
123
  }
132
-
133
124
  function cleanExpiredLocks(locks) {
134
125
  const current = now();
135
126
  const kept = {};
@@ -139,7 +130,6 @@ async function runLocker(argv = process.argv.slice(2)) {
139
130
  }
140
131
  return kept;
141
132
  }
142
-
143
133
  function normalizeControl(line) {
144
134
  const trimmed = line.trim();
145
135
  if (!trimmed) return undefined;
@@ -160,8 +150,6 @@ async function runLocker(argv = process.argv.slice(2)) {
160
150
  );
161
151
  }
162
152
  }
163
-
164
-
165
153
  function tailJournal(count) {
166
154
  if (!existsSync(journalPath)) return [];
167
155
  return readFileSync(journalPath, "utf8")
@@ -177,7 +165,6 @@ async function runLocker(argv = process.argv.slice(2)) {
177
165
  }
178
166
  });
179
167
  }
180
-
181
168
  function printSnapshot() {
182
169
  const locks = cleanExpiredLocks(readJson(locksPath, {}));
183
170
  writeJson(locksPath, locks);
@@ -195,7 +182,6 @@ async function runLocker(argv = process.argv.slice(2)) {
195
182
  ),
196
183
  );
197
184
  }
198
-
199
185
  function nextTask(queue, locks) {
200
186
  const items = Array.isArray(queue.items) ? queue.items : [];
201
187
  const index = items.findIndex((item) => {
@@ -205,7 +191,6 @@ async function runLocker(argv = process.argv.slice(2)) {
205
191
  if (index < 0) return undefined;
206
192
  return items.splice(index, 1)[0];
207
193
  }
208
-
209
194
  function handle(control) {
210
195
  const action = control.action;
211
196
  const body =
@@ -333,18 +318,15 @@ async function runLocker(argv = process.argv.slice(2)) {
333
318
  journal("lock.unknown", { action, input: body });
334
319
  emitTrace("lock.unknown", `Unknown Control ${action}`, { action, input: body }, "warning");
335
320
  }
336
-
337
321
  if (mode === "snapshot") {
338
322
  printSnapshot();
339
323
  process.exit(0);
340
324
  }
341
-
342
325
  const startupRun = readJson(join(stateDir, "run.json"), undefined);
343
326
  if (!startupRun || typeof startupRun.run_instance_id !== "string") {
344
327
  throw new Error("Run generation unavailable for Control service");
345
328
  }
346
329
  runInstanceId = startupRun.run_instance_id;
347
-
348
330
  function handleLine(line) {
349
331
  const control = normalizeControl(line);
350
332
  if (!control) return false;
@@ -376,7 +358,6 @@ async function runLocker(argv = process.argv.slice(2)) {
376
358
  return false;
377
359
  }
378
360
  }
379
-
380
361
  async function serveFifo(endpoint) {
381
362
  if (!existsSync(endpoint.path)) {
382
363
  const result = spawnSync("mkfifo", [endpoint.path]);
@@ -403,7 +384,6 @@ async function runLocker(argv = process.argv.slice(2)) {
403
384
  fs.closeSync(fd);
404
385
  }
405
386
  }
406
-
407
387
  async function serveNamedPipe(endpoint) {
408
388
  let resolveStopped;
409
389
  const stopped = new Promise((resolve) => {
@@ -428,7 +408,6 @@ async function runLocker(argv = process.argv.slice(2)) {
428
408
  });
429
409
  await stopped;
430
410
  }
431
-
432
411
  const endpoint = getControlEndpoint();
433
412
  writeJson(queuePath, readJson(queuePath, { items: [] }));
434
413
  writeJson(locksPath, cleanExpiredLocks(readJson(locksPath, {})));
@@ -369,7 +369,6 @@ if (!command) {
369
369
  usage();
370
370
  process.exit(1);
371
371
  }
372
-
373
372
  if (command === "run-summary")
374
373
  runSummary(args[0] ?? "~/.pi/agent/tmp/pi-actors/runs");
375
374
  else if (command === "run-ops-snapshot")
@@ -7,6 +7,8 @@ import { dirname, extname, join, normalize, relative, resolve } from "node:path"
7
7
  import { spawnSync } from "node:child_process";
8
8
  import { fileURLToPath } from "node:url";
9
9
 
10
+ import { executableBlockBlankLines } from "./executable-block-style.mjs";
11
+
10
12
  const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
11
13
  const temp = mkdtempSync(join(tmpdir(), "pi-actors-release-index-"));
12
14
  const indexPath = join(temp, "index");
@@ -71,7 +73,6 @@ try {
71
73
  .filter(Boolean);
72
74
  const fileSet = new Set(files);
73
75
  console.log(`[release] exact temporary index: ${files.length} files`);
74
-
75
76
  const secretPatterns = [
76
77
  [/-----BEGIN(?: [A-Z0-9]+)? PRIVATE KEY-----[\s\S]{80,}?-----END(?: [A-Z0-9]+)? PRIVATE KEY-----/u, "private-key block"],
77
78
  [/\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/u, "AWS access key"],
@@ -92,7 +93,17 @@ try {
92
93
  }
93
94
  }
94
95
  console.log("[release] bounded secret and public-path hygiene checked");
95
-
96
+ const executableSources = files.filter((path) =>
97
+ /\.(?:[cm]?js|ts)$/u.test(path) && !path.startsWith("dist/"),
98
+ );
99
+ for (const path of executableSources) {
100
+ const blankLines = executableBlockBlankLines(stagedText(path) ?? "");
101
+ check(
102
+ blankLines.length === 0,
103
+ `blank lines inside executable blocks: ${path}:${blankLines.join(",")}`,
104
+ );
105
+ }
106
+ console.log("[release] executable blocks contain no blank lines");
96
107
  const removedPaths = [
97
108
  "lib/messages.ts",
98
109
  "lib/rooms.ts",
@@ -113,7 +124,6 @@ try {
113
124
  "lib/run-inspector-overlay.ts",
114
125
  ];
115
126
  for (const path of removedPaths) check(!fileSet.has(path), `removed surface returned: ${path}`);
116
-
117
127
  const currentSurface = /^(?:lib\/|scripts\/|recipes\/|fixtures\/|skills\/|docs\/|README\.md|AGENTS\.md)/u;
118
128
  const forbiddenPatterns = [
119
129
  [/room:</u, "room address"],
@@ -144,7 +154,6 @@ try {
144
154
  }
145
155
  }
146
156
  console.log("[release] removed-surface and legacy-fallback allowlists checked");
147
-
148
157
  const directTraceWrite = /(?:appendFileSync|writeFileSync|writeText(?:Atomic)?)\s*\(\s*[A-Za-z0-9_.]*?(?:trace|event)(?:Path|File)/iu;
149
158
  for (const path of files.filter((candidate) =>
150
159
  (candidate.startsWith("lib/") && candidate.endsWith(".ts")) ||
@@ -172,7 +181,6 @@ try {
172
181
  );
173
182
  }
174
183
  console.log("[release] canonical Trace and Control writer residue checked");
175
-
176
184
  // owner | class | lock | record bound | byte bound | recovery
177
185
  const appendOnlyOwners = new Set([
178
186
  "lib/runs-trace.ts", // canonical Run evidence | token lock | 2048 | 4 MiB | compact valid suffix
@@ -189,7 +197,6 @@ try {
189
197
  check(appendOnlyOwners.has(path), `unregistered shipped append-only writer: ${path}`);
190
198
  for (const path of appendOnlyOwners) check(fileSet.has(path), `stale append-only owner inventory: ${path}`);
191
199
  console.log(`[release] append-only owner inventory checked: ${appendOnlyOwners.size} source owners`);
192
-
193
200
  check(
194
201
  (stagedText("scripts/validate-recipe.mjs") ?? "").includes(
195
202
  "qaReport.diagnostics.length === 0 && qaReport.warnings.length === 0",
@@ -197,15 +204,13 @@ try {
197
204
  "Recipe QA warnings are not release-blocking",
198
205
  );
199
206
  console.log("[release] zero-warning Recipe QA gate checked");
200
-
201
- const maximumShippedLines = 29_598;
207
+ const maximumShippedLines = 32_000;
202
208
  const shippedPath = /^(?:lib\/|scripts\/|recipes\/|docs\/|skills\/)/u;
203
209
  const shippedLines = files
204
210
  .filter((path) => shippedPath.test(path))
205
211
  .reduce((total, path) => total + (((stagedText(path) ?? "").match(/\n/gu) ?? []).length + 1), 0);
206
212
  check(shippedLines <= maximumShippedLines, `shipped lines ${shippedLines} exceed release maximum ${maximumShippedLines}`);
207
213
  console.log(`[release] shipped lines ${shippedLines} <= release maximum ${maximumShippedLines}`);
208
-
209
214
  const sources = files.filter((path) => path === "index.ts" || (path.startsWith("lib/") && path.endsWith(".ts")));
210
215
  const sourceSet = new Set(sources);
211
216
  const graph = new Map();
@@ -240,7 +245,6 @@ try {
240
245
  }
241
246
  for (const path of sources) visit(path);
242
247
  console.log(`[release] strict Domain DAG: ${sources.length} sources, acyclic`);
243
-
244
248
  for (const path of ["README.md", "AGENTS.md", "BACKLOG.md", "CHANGELOG.md", "docs/README.md"]) {
245
249
  check(fileSet.has(path), `missing ABCd root/context file: ${path}`);
246
250
  }
@@ -263,7 +267,6 @@ try {
263
267
  check(docsIndex.includes(relative("docs", path).replaceAll("\\", "/")), `docs/README.md omits ${path}`);
264
268
  }
265
269
  console.log("[release] ABCd context roots, routing, and Markdown links checked");
266
-
267
270
  if (failures.length > 0) {
268
271
  for (const failure of failures) console.error(`[FAIL] ${failure}`);
269
272
  process.exitCode = 1;
@@ -187,7 +187,6 @@ export function validateRecipes(argv) {
187
187
  if (!targetArg || argv.includes("--help") || argv.includes("-h")) {
188
188
  return { help: true, ok: Boolean(targetArg), usage: validateRecipeUsage() };
189
189
  }
190
-
191
190
  const files = recipeFiles(expandPath(targetArg), all);
192
191
  const results = files.map((file) => validateFile(file, qa));
193
192
  const failed = results.filter((result) => !result.ok).length;
@@ -25,11 +25,11 @@ A Run target exposes exactly three inspect views: `recipe`, `trace`, and `contro
25
25
 
26
26
  ## Recipe
27
27
 
28
- A Recipe defines execution. It may declare args, defaults, imports, artifacts, command-template flags, and `control: ["action"]` when a long-lived service actually consumes actor-local inputs.
28
+ A Recipe defines execution. It may declare named typed args, inline fallbacks, configuration `defaults`, composition `values`, imports, artifacts, command-template flags, and `control: ["action"]` when a long-lived service actually consumes actor-local inputs.
29
29
 
30
30
  Do not declare Control for ordinary one-shot work. Runtime lifecycle actions such as `kill` stay runtime-owned and must not appear in Recipe Control declarations. Imported Recipes act as local definitions inside one Run; they do not create nested Runs unless execution explicitly spawns them.
31
31
 
32
- Prefer maintained packaged Recipes over ad hoc wrappers. Keep model, thinking, mission, concurrency, quorum, and timeout choices caller-owned unless a Recipe documents a stable policy.
32
+ Prefer maintained packaged Recipes over ad hoc wrappers. Use `std:<recipe>` for exact packaged lookup and `skill:<skill>/<recipe-path>` for a component bundled with a Pi-active Skill. File-backed Recipes own `{recipe_dir}` and Skill Recipes own `{skill_dir}`; callers never pass or override these origins. Skill Recipes are components, not automatic tools. Keep model, thinking, mission, concurrency, quorum, and timeout choices caller-owned unless a Recipe documents a stable policy.
33
33
 
34
34
  ## Trace
35
35