@llblab/pi-actors 0.29.0 → 0.29.2
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.
- package/AGENTS.md +2 -2
- package/CHANGELOG.md +8 -0
- package/dist/lib/config.d.ts +1 -1
- package/dist/lib/config.js +1 -1
- package/dist/lib/coordinator.js +67 -8
- package/dist/lib/recipe-discovery.d.ts +1 -1
- package/dist/lib/recipe-discovery.js +2 -3
- package/dist/lib/runtime.js +0 -11
- package/dist/recipes/pipeline-room-swarm.json +3 -1
- package/dist/skills/actors/SKILL.md +2 -2
- package/dist/skills/swarm/SKILL.md +1 -1
- package/docs/recipe-library.md +1 -1
- package/lib/config.ts +1 -1
- package/lib/coordinator.ts +59 -10
- package/lib/recipe-discovery.ts +2 -3
- package/lib/runtime.ts +0 -15
- package/package.json +1 -1
- package/recipes/pipeline-room-swarm.json +3 -1
- package/skills/actors/SKILL.md +2 -2
- package/skills/swarm/SKILL.md +1 -1
- package/dist/lib/recipe-migration.d.ts +0 -21
- package/dist/lib/recipe-migration.js +0 -90
- package/lib/recipe-migration.ts +0 -121
package/AGENTS.md
CHANGED
|
@@ -45,7 +45,7 @@ Treat this extension as an experimental self-evolution membrane for the agent ha
|
|
|
45
45
|
|
|
46
46
|
## Operating Principles
|
|
47
47
|
|
|
48
|
-
- Prefer explicit
|
|
48
|
+
- Prefer explicit operator action over silent user-config rewrites.
|
|
49
49
|
- Keep published documentation portable: use `~`, `<repo>`, or relative paths instead of machine-local absolute paths.
|
|
50
50
|
- Preserve runtime output discipline because tool output flows directly into agent context.
|
|
51
51
|
- Keep the project lens local-first and cybernetic: agents wrap durable local capabilities as actors, then use semantic tools and messages instead of repeatedly reconstructing shell commands.
|
|
@@ -82,7 +82,7 @@ Treat this extension as an experimental self-evolution membrane for the agent ha
|
|
|
82
82
|
## Recipes And Registry
|
|
83
83
|
|
|
84
84
|
- `~/.pi/agent/recipes/*.json` is executable muscle memory: recipes there become persistent tools by location.
|
|
85
|
-
- Preserve filename identity, atomic writes, explicit operator-gated
|
|
85
|
+
- Preserve filename identity, atomic writes, explicit operator-gated changes, and local transportability.
|
|
86
86
|
- Packaged/ad hoc recipes outside the agent root are components, not user tools.
|
|
87
87
|
- Tool definitions use `template`, not `script`, and built-in/core tool names must not be shadowed.
|
|
88
88
|
- Packaged recipe growth is demand-driven: prefer reusable components over speculative scenario catalogs.
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.29.2: Legacy Migration Removal Hotfix
|
|
6
|
+
|
|
7
|
+
- `[Registry]` Removed the old legacy tool-registry migration path now that recipe-file storage is the only maintained persistence surface.
|
|
8
|
+
|
|
9
|
+
## 0.29.1: Subagent TTL Kill Hotfix
|
|
10
|
+
|
|
11
|
+
- `[Coordinator]` Added `subagent_ttl_ms` / `--subagent-ttl-ms` to the room-swarm adapter so timed-out subagent `pi -p` processes are terminated instead of only awaited.
|
|
12
|
+
|
|
5
13
|
## 0.29.0: Candidate Recipe Memory
|
|
6
14
|
|
|
7
15
|
- `[Spawn]` Capture inline spawn templates as non-registered candidate recipes under `~/.pi/agent/recipes/candidates`, making successful ad hoc actor patterns easy to replay by explicit path and promote manually.
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Persistent tool registry config helpers
|
|
3
|
-
* Zones: registry config, persistence,
|
|
3
|
+
* Zones: registry config, persistence, normalization
|
|
4
4
|
* Owns registered-tool config loading, normalization, unsupported-shape rejection, and serialization
|
|
5
5
|
*/
|
|
6
6
|
import type { CommandTemplateValue } from "./command-templates.ts";
|
package/dist/lib/config.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Persistent tool registry config helpers
|
|
3
|
-
* Zones: registry config, persistence,
|
|
3
|
+
* Zones: registry config, persistence, normalization
|
|
4
4
|
* Owns registered-tool config loading, normalization, unsupported-shape rejection, and serialization
|
|
5
5
|
*/
|
|
6
6
|
import { existsSync, readFileSync } from "node:fs";
|
package/dist/lib/coordinator.js
CHANGED
|
@@ -200,7 +200,25 @@ async function stopLocker(locker) {
|
|
|
200
200
|
locker.child.kill("SIGTERM");
|
|
201
201
|
}
|
|
202
202
|
}
|
|
203
|
-
function
|
|
203
|
+
function terminateProcessGroup(child, signal) {
|
|
204
|
+
if (!child.pid)
|
|
205
|
+
return;
|
|
206
|
+
try {
|
|
207
|
+
if (process.platform !== "win32")
|
|
208
|
+
process.kill(-child.pid, signal);
|
|
209
|
+
else
|
|
210
|
+
child.kill(signal);
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
try {
|
|
214
|
+
child.kill(signal);
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
// Process already exited.
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
function runPi(prompt, model, thinking, ttlMs = 0) {
|
|
204
222
|
return new Promise((resolve) => {
|
|
205
223
|
const args = [
|
|
206
224
|
"--tools",
|
|
@@ -216,17 +234,57 @@ function runPi(prompt, model, thinking) {
|
|
|
216
234
|
args.push("--thinking", thinking);
|
|
217
235
|
}
|
|
218
236
|
args.push("-p", prompt);
|
|
219
|
-
const child = spawn("pi", args, {
|
|
237
|
+
const child = spawn("pi", args, {
|
|
238
|
+
detached: process.platform !== "win32",
|
|
239
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
240
|
+
});
|
|
220
241
|
let stdout = "";
|
|
221
242
|
let stderr = "";
|
|
243
|
+
let settled = false;
|
|
244
|
+
let timedOut = false;
|
|
245
|
+
let ttlTimer;
|
|
246
|
+
let forceTimer;
|
|
247
|
+
const clearTimers = () => {
|
|
248
|
+
if (ttlTimer)
|
|
249
|
+
clearTimeout(ttlTimer);
|
|
250
|
+
if (forceTimer)
|
|
251
|
+
clearTimeout(forceTimer);
|
|
252
|
+
};
|
|
253
|
+
if (ttlMs > 0) {
|
|
254
|
+
ttlTimer = setTimeout(() => {
|
|
255
|
+
timedOut = true;
|
|
256
|
+
stderr += `\nSubagent TTL expired after ${ttlMs}ms; terminating process group.\n`;
|
|
257
|
+
terminateProcessGroup(child, "SIGTERM");
|
|
258
|
+
forceTimer = setTimeout(() => terminateProcessGroup(child, "SIGKILL"), 1000);
|
|
259
|
+
}, ttlMs);
|
|
260
|
+
}
|
|
222
261
|
child.stdout.on("data", (chunk) => {
|
|
223
262
|
stdout += chunk;
|
|
224
263
|
});
|
|
225
264
|
child.stderr.on("data", (chunk) => {
|
|
226
265
|
stderr += chunk;
|
|
227
266
|
});
|
|
228
|
-
child.on("close", (code) =>
|
|
229
|
-
|
|
267
|
+
child.on("close", (code, signal) => {
|
|
268
|
+
if (settled)
|
|
269
|
+
return;
|
|
270
|
+
settled = true;
|
|
271
|
+
clearTimers();
|
|
272
|
+
resolve({
|
|
273
|
+
code: timedOut ? 124 : (code ?? (signal ? 1 : 0)),
|
|
274
|
+
killed: timedOut,
|
|
275
|
+
signal,
|
|
276
|
+
stderr,
|
|
277
|
+
stdout,
|
|
278
|
+
timed_out: timedOut,
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
child.on("error", (error) => {
|
|
282
|
+
if (settled)
|
|
283
|
+
return;
|
|
284
|
+
settled = true;
|
|
285
|
+
clearTimers();
|
|
286
|
+
resolve({ code: 1, stdout, stderr: String(error) });
|
|
287
|
+
});
|
|
230
288
|
});
|
|
231
289
|
}
|
|
232
290
|
async function readRoomTranscript(config) {
|
|
@@ -268,7 +326,7 @@ async function synthesize(config, locker) {
|
|
|
268
326
|
}
|
|
269
327
|
const transcript = await readRoomTranscript(config);
|
|
270
328
|
const prompt = `Synthesize this transcript into a concise Markdown artifact. Mission: ${config.mission}. Include: Title, Consensus, Roles, Protocol, Final Artifact Shape, Next Actions, Open Questions. Use only the transcript evidence below.\n\nTRANSCRIPT:\n${transcript.slice(-24000)}`;
|
|
271
|
-
const result = await runPi(prompt, config.model, config.thinking);
|
|
329
|
+
const result = await runPi(prompt, config.model, config.thinking, config.subagentTtlMs);
|
|
272
330
|
const output = result.stdout.trim();
|
|
273
331
|
const diagnostics = config.stats
|
|
274
332
|
? `\n\n## Diagnostics\n\nparticipant_attempts=${config.stats.participantAttempts}\nparticipant_success=${config.stats.participantSuccess}\nparticipant_failures=${config.stats.participantFailures}\nsynthesis_code=${result.code}\ntranscript_messages=${transcript.trim() ? transcript.split("\n").length : 0}\n`
|
|
@@ -371,7 +429,7 @@ async function executeParticipantPrompt(role, basePrompt, config) {
|
|
|
371
429
|
"\nPlease acknowledge and address these direct messages in your response.\n";
|
|
372
430
|
finalPrompt += inboxSection;
|
|
373
431
|
}
|
|
374
|
-
const result = await runPi(finalPrompt, config.model, config.thinking);
|
|
432
|
+
const result = await runPi(finalPrompt, config.model, config.thinking, config.subagentTtlMs);
|
|
375
433
|
if (claimedIds.length > 0) {
|
|
376
434
|
const finalStatus = result.code === 0 ? "handled" : "failed";
|
|
377
435
|
await updateInboxMessagesStatus(config.runId, branchName, claimedIds, finalStatus);
|
|
@@ -399,13 +457,13 @@ async function participantJoin(role, config) {
|
|
|
399
457
|
const displayName = role.name;
|
|
400
458
|
const address = `branch:${config.runId}/${role.name}`;
|
|
401
459
|
const joinPrompt = `You are ${displayName}, ${role.persona}. Mission: ${config.mission}. Call tool message exactly once with to=${shellQuote(config.room)}, from=${shellQuote(address)}, type='actor.join', summary='${displayName} joined', body JSON {"role":${JSON.stringify(role.persona)},"display":${JSON.stringify(displayName)},"caps":["coordination","synthesis"],"claim":"coordinate on mission"}. Then print one short line.`;
|
|
402
|
-
await runPi(joinPrompt, config.model, config.thinking);
|
|
460
|
+
await runPi(joinPrompt, config.model, config.thinking, config.subagentTtlMs);
|
|
403
461
|
}
|
|
404
462
|
async function participantLeave(role, config) {
|
|
405
463
|
const displayName = role.name;
|
|
406
464
|
const address = `branch:${config.runId}/${role.name}`;
|
|
407
465
|
const leavePrompt = `Call tool message exactly once with to=${shellQuote(config.room)}, from=${shellQuote(address)}, type='actor.leave', summary='${displayName} left', body='finished coordinated work'. Then print goodbye.`;
|
|
408
|
-
await runPi(leavePrompt, config.model, config.thinking);
|
|
466
|
+
await runPi(leavePrompt, config.model, config.thinking, config.subagentTtlMs);
|
|
409
467
|
}
|
|
410
468
|
// 1. consensus / swarm mode: iterative chat in a room
|
|
411
469
|
async function runConsensus(config, locker) {
|
|
@@ -559,6 +617,7 @@ export async function runCoordinator(argv = process.argv.slice(2)) {
|
|
|
559
617
|
artifactPath: arg("artifact-path", ""),
|
|
560
618
|
locker: boolArg("locker", false),
|
|
561
619
|
lockerLeaseMs: numberArg("locker-lease-ms", 600000),
|
|
620
|
+
subagentTtlMs: numberArg("subagent-ttl-ms", 0),
|
|
562
621
|
stats: {
|
|
563
622
|
participantAttempts: 0,
|
|
564
623
|
participantSuccess: 0,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* File-discovered recipe registry helpers
|
|
3
|
-
* Zones: recipe discovery, tool exposure,
|
|
3
|
+
* Zones: recipe discovery, tool exposure, registry diagnostics
|
|
4
4
|
* Owns filename identity discovery across prioritized recipe roots
|
|
5
5
|
*/
|
|
6
6
|
import type { RegisteredTool } from "./config.ts";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* File-discovered recipe registry helpers
|
|
3
|
-
* Zones: recipe discovery, tool exposure,
|
|
3
|
+
* Zones: recipe discovery, tool exposure, registry diagnostics
|
|
4
4
|
* Owns filename identity discovery across prioritized recipe roots
|
|
5
5
|
*/
|
|
6
6
|
import { createHash } from "node:crypto";
|
|
@@ -38,8 +38,7 @@ function listRecipeFiles(root) {
|
|
|
38
38
|
return [];
|
|
39
39
|
return readdirSync(root, { withFileTypes: true })
|
|
40
40
|
.filter((entry) => entry.isFile() &&
|
|
41
|
-
(entry.name.endsWith(".json") || entry.name.endsWith(".md"))
|
|
42
|
-
entry.name !== "legacy-tool-registry-migration-report.json")
|
|
41
|
+
(entry.name.endsWith(".json") || entry.name.endsWith(".md")))
|
|
43
42
|
.map((entry) => join(root, entry.name))
|
|
44
43
|
.sort((a, b) => a
|
|
45
44
|
.replace(/\.md$/, ".json")
|
package/dist/lib/runtime.js
CHANGED
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import * as Paths from "./paths.js";
|
|
7
7
|
import * as RecipeDiscovery from "./recipe-discovery.js";
|
|
8
|
-
import * as RecipeMigration from "./recipe-migration.js";
|
|
9
8
|
import * as Tools from "./tools.js";
|
|
10
9
|
export function createAutoToolsRuntime(deps) {
|
|
11
10
|
const tools = new Map();
|
|
@@ -86,16 +85,6 @@ export function createAutoToolsRuntime(deps) {
|
|
|
86
85
|
const warnings = [];
|
|
87
86
|
const recipeRoot = deps.recipeRoot ?? Paths.getRecipeRoot();
|
|
88
87
|
const packagedRecipeRoot = deps.packagedRecipeRoot ?? Paths.getPackagedRecipeRoot();
|
|
89
|
-
const migration = RecipeMigration.migrateLegacyToolRegistry({
|
|
90
|
-
configPath: deps.configPath,
|
|
91
|
-
recipeRoot,
|
|
92
|
-
reservedToolNames: deps.reservedToolNames,
|
|
93
|
-
});
|
|
94
|
-
warnings.push(...migration.warnings);
|
|
95
|
-
if (migration.conflicts.length > 0)
|
|
96
|
-
warnings.push(`Recipe migration conflicts: ${migration.conflicts.join(", ")}`);
|
|
97
|
-
if (migration.invalid.length > 0)
|
|
98
|
-
warnings.push(`Recipe migration invalid entries: ${migration.invalid.join(", ")}`);
|
|
99
88
|
const discovered = RecipeDiscovery.discoverRecipeSources([
|
|
100
89
|
{ root: recipeRoot, defaultTool: true, mutableUsage: true },
|
|
101
90
|
{ root: packagedRecipeRoot },
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"delay:int",
|
|
12
12
|
"locker:bool",
|
|
13
13
|
"locker_lease_ms:int",
|
|
14
|
+
"subagent_ttl_ms:int",
|
|
14
15
|
"artifact_path:path",
|
|
15
16
|
"repo:path"
|
|
16
17
|
],
|
|
@@ -23,6 +24,7 @@
|
|
|
23
24
|
"delay": "10",
|
|
24
25
|
"locker": "false",
|
|
25
26
|
"locker_lease_ms": "600000",
|
|
27
|
+
"subagent_ttl_ms": "0",
|
|
26
28
|
"artifact_path": "{state_dir}/room-swarm-artifact.md",
|
|
27
29
|
"repo": "~/.pi/agent/extensions/pi-actors"
|
|
28
30
|
},
|
|
@@ -44,5 +46,5 @@
|
|
|
44
46
|
"run.failed"
|
|
45
47
|
]
|
|
46
48
|
},
|
|
47
|
-
"template": "{repo}/scripts/coordinator.mjs --run-id={run_id} --mode={mode} --mission={mission} --model={model} --thinking={thinking} --roles={roles} --roles-path={roles_path} --rounds={rounds} --delay={delay} --locker={locker} --locker-lease-ms={locker_lease_ms} --artifact-path={artifact_path}"
|
|
49
|
+
"template": "{repo}/scripts/coordinator.mjs --run-id={run_id} --mode={mode} --mission={mission} --model={model} --thinking={thinking} --roles={roles} --roles-path={roles_path} --rounds={rounds} --delay={delay} --locker={locker} --locker-lease-ms={locker_lease_ms} --subagent-ttl-ms={subagent_ttl_ms} --artifact-path={artifact_path}"
|
|
48
50
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: actors
|
|
3
3
|
description: Highest-density practical guide for pi-actors. Read this skill whenever prompt and tools are not enough for spawn, message, inspect, actor runs, tools, recipes, command templates, async lifecycle, mailboxes, artifacts, and local orchestration mechanics.
|
|
4
4
|
metadata:
|
|
5
|
-
version: 0.29.
|
|
5
|
+
version: 0.29.2
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Actors (pi-actors)
|
|
@@ -302,7 +302,7 @@ Use packaged recipes by name with `spawn file=<name>` for async actors, or regis
|
|
|
302
302
|
- [`pipeline-docs-maintenance`](../../recipes/pipeline-docs-maintenance.json): docs index/review/planning → maintenance artifact.
|
|
303
303
|
- Artifacts: [`pipeline-artifact-report`](../../recipes/pipeline-artifact-report.json), [`pipeline-artifact-write`](../../recipes/pipeline-artifact-write.json), [`pipeline-artifact-bundle`](../../recipes/pipeline-artifact-bundle.json).
|
|
304
304
|
- Review gates: [`pipeline-quorum-review`](../../recipes/pipeline-quorum-review.json), [`pipeline-review-readiness`](../../recipes/pipeline-review-readiness.json).
|
|
305
|
-
- Task-first workflows: [`pipeline-architect-coordinator`](../../recipes/pipeline-architect-coordinator.json), [`pipeline-research-synthesis`](../../recipes/pipeline-research-synthesis.json), [`pipeline-development-tasking`](../../recipes/pipeline-development-tasking.json), [`pipeline-checkpoint-continuation`](../../recipes/pipeline-checkpoint-continuation.json), [`pipeline-media-library`](../../recipes/pipeline-media-library.json), [`pipeline-room-swarm`](../../recipes/pipeline-room-swarm.json). For room swarms, choose `mode` from `consensus`, `pipeline`, `fanout`, or `pool`; prefer `roles_path` for custom role JSON and keep role `name` ASCII-safe for branch addresses. Use `locker=true` when the swarm needs a coordinator-locker-backed artifact lock and journal.
|
|
305
|
+
- Task-first workflows: [`pipeline-architect-coordinator`](../../recipes/pipeline-architect-coordinator.json), [`pipeline-research-synthesis`](../../recipes/pipeline-research-synthesis.json), [`pipeline-development-tasking`](../../recipes/pipeline-development-tasking.json), [`pipeline-checkpoint-continuation`](../../recipes/pipeline-checkpoint-continuation.json), [`pipeline-media-library`](../../recipes/pipeline-media-library.json), [`pipeline-room-swarm`](../../recipes/pipeline-room-swarm.json). For room swarms, choose `mode` from `consensus`, `pipeline`, `fanout`, or `pool`; prefer `roles_path` for custom role JSON and keep role `name` ASCII-safe for branch addresses. Set `subagent_ttl_ms` when participant processes need a hard kill budget. Use `locker=true` when the swarm needs a coordinator-locker-backed artifact lock and journal.
|
|
306
306
|
|
|
307
307
|
### Utilities
|
|
308
308
|
|
package/docs/recipe-library.md
CHANGED
|
@@ -84,7 +84,7 @@ Pipeline recipes demonstrate second-order composition:
|
|
|
84
84
|
- `recipes/pipeline-development-tasking.json`: Plan → task card → critique → integrator handoff.
|
|
85
85
|
- `recipes/pipeline-docs-maintenance.json`: Docs index → documentation review → maintenance plan → artifact report.
|
|
86
86
|
- `recipes/pipeline-media-library.json`: Playlist build → media-library artifact report.
|
|
87
|
-
- `recipes/pipeline-room-swarm.json`: Room participants join `room:<run>`, coordinate over repeated room-visible rounds, leave cleanly, and synthesize the room transcript into a caller-provided artifact path. Supported coordinator modes are `consensus`, `pipeline`, `fanout`, and `pool`; unknown modes fail closed instead of silently running consensus. Keep model/thinking/mission policy caller-owned. Custom roles can be supplied with `roles_path` as a JSON array of `{ "name", "persona" }` objects; `name` stays ASCII-safe for `branch:<run>/<name>` addresses and debugger output remains plain and name-driven. The packaged swarm uses contacts for peer awareness but does not rely on direct branch delivery unless a caller-specific worker protocol consumes branch envelopes. Set `locker=true` to compose a local `coordinator-locker` cell under `{state_dir}/locker` for artifact ownership, resource lease locks, and a decision journal without merging locker policy into the room-participant script.
|
|
87
|
+
- `recipes/pipeline-room-swarm.json`: Room participants join `room:<run>`, coordinate over repeated room-visible rounds, leave cleanly, and synthesize the room transcript into a caller-provided artifact path. Supported coordinator modes are `consensus`, `pipeline`, `fanout`, and `pool`; unknown modes fail closed instead of silently running consensus. Keep model/thinking/mission policy caller-owned. Custom roles can be supplied with `roles_path` as a JSON array of `{ "name", "persona" }` objects; `name` stays ASCII-safe for `branch:<run>/<name>` addresses and debugger output remains plain and name-driven. The packaged swarm uses contacts for peer awareness but does not rely on direct branch delivery unless a caller-specific worker protocol consumes branch envelopes. Set `subagent_ttl_ms` to a positive millisecond budget when participant `pi -p` processes must be killed instead of awaited indefinitely. Set `locker=true` to compose a local `coordinator-locker` cell under `{state_dir}/locker` for artifact ownership, resource lease locks, and a decision journal without merging locker policy into the room-participant script.
|
|
88
88
|
- `recipes/pipeline-artifact-report.json`: Normalize → artifact-shaped output → actor-message-shaped record. This pipeline prepares a candidate artifact and emits `artifact.prepared`/`artifact.blocked`; the `artifact_path` is a target path, not a guarantee that the file was written.
|
|
89
89
|
- `recipes/pipeline-artifact-write.json`: Normalize → artifact-shaped output → deterministic artifact write → actor-message-shaped record. Use only when the caller explicitly wants filesystem writes; `write_mode` is `create`, `overwrite`, or `append`.
|
|
90
90
|
- `recipes/pipeline-artifact-bundle.json`: Optional validation → deterministic artifact write → machine-readable manifest generation → deterministic manifest write → actor-message-shaped record. Use when the caller explicitly wants a filesystem handoff bundle with both artifact and manifest paths.
|
package/lib/config.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Persistent tool registry config helpers
|
|
3
|
-
* Zones: registry config, persistence,
|
|
3
|
+
* Zones: registry config, persistence, normalization
|
|
4
4
|
* Owns registered-tool config loading, normalization, unsupported-shape rejection, and serialization
|
|
5
5
|
*/
|
|
6
6
|
|
package/lib/coordinator.ts
CHANGED
|
@@ -220,7 +220,21 @@ async function stopLocker(locker) {
|
|
|
220
220
|
}
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
-
function
|
|
223
|
+
function terminateProcessGroup(child, signal) {
|
|
224
|
+
if (!child.pid) return;
|
|
225
|
+
try {
|
|
226
|
+
if (process.platform !== "win32") process.kill(-child.pid, signal);
|
|
227
|
+
else child.kill(signal);
|
|
228
|
+
} catch {
|
|
229
|
+
try {
|
|
230
|
+
child.kill(signal);
|
|
231
|
+
} catch {
|
|
232
|
+
// Process already exited.
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function runPi(prompt, model, thinking, ttlMs = 0) {
|
|
224
238
|
return new Promise((resolve) => {
|
|
225
239
|
const args = [
|
|
226
240
|
"--tools",
|
|
@@ -237,19 +251,53 @@ function runPi(prompt, model, thinking) {
|
|
|
237
251
|
}
|
|
238
252
|
args.push("-p", prompt);
|
|
239
253
|
|
|
240
|
-
const child = spawn("pi", args, {
|
|
254
|
+
const child = spawn("pi", args, {
|
|
255
|
+
detached: process.platform !== "win32",
|
|
256
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
257
|
+
});
|
|
241
258
|
let stdout = "";
|
|
242
259
|
let stderr = "";
|
|
260
|
+
let settled = false;
|
|
261
|
+
let timedOut = false;
|
|
262
|
+
let ttlTimer;
|
|
263
|
+
let forceTimer;
|
|
264
|
+
const clearTimers = () => {
|
|
265
|
+
if (ttlTimer) clearTimeout(ttlTimer);
|
|
266
|
+
if (forceTimer) clearTimeout(forceTimer);
|
|
267
|
+
};
|
|
268
|
+
if (ttlMs > 0) {
|
|
269
|
+
ttlTimer = setTimeout(() => {
|
|
270
|
+
timedOut = true;
|
|
271
|
+
stderr += `\nSubagent TTL expired after ${ttlMs}ms; terminating process group.\n`;
|
|
272
|
+
terminateProcessGroup(child, "SIGTERM");
|
|
273
|
+
forceTimer = setTimeout(() => terminateProcessGroup(child, "SIGKILL"), 1000);
|
|
274
|
+
}, ttlMs);
|
|
275
|
+
}
|
|
243
276
|
child.stdout.on("data", (chunk) => {
|
|
244
277
|
stdout += chunk;
|
|
245
278
|
});
|
|
246
279
|
child.stderr.on("data", (chunk) => {
|
|
247
280
|
stderr += chunk;
|
|
248
281
|
});
|
|
249
|
-
child.on("close", (code) =>
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
282
|
+
child.on("close", (code, signal) => {
|
|
283
|
+
if (settled) return;
|
|
284
|
+
settled = true;
|
|
285
|
+
clearTimers();
|
|
286
|
+
resolve({
|
|
287
|
+
code: timedOut ? 124 : (code ?? (signal ? 1 : 0)),
|
|
288
|
+
killed: timedOut,
|
|
289
|
+
signal,
|
|
290
|
+
stderr,
|
|
291
|
+
stdout,
|
|
292
|
+
timed_out: timedOut,
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
child.on("error", (error) => {
|
|
296
|
+
if (settled) return;
|
|
297
|
+
settled = true;
|
|
298
|
+
clearTimers();
|
|
299
|
+
resolve({ code: 1, stdout, stderr: String(error) });
|
|
300
|
+
});
|
|
253
301
|
});
|
|
254
302
|
}
|
|
255
303
|
|
|
@@ -294,7 +342,7 @@ async function synthesize(config, locker) {
|
|
|
294
342
|
}
|
|
295
343
|
const transcript = await readRoomTranscript(config);
|
|
296
344
|
const prompt = `Synthesize this transcript into a concise Markdown artifact. Mission: ${config.mission}. Include: Title, Consensus, Roles, Protocol, Final Artifact Shape, Next Actions, Open Questions. Use only the transcript evidence below.\n\nTRANSCRIPT:\n${transcript.slice(-24000)}`;
|
|
297
|
-
const result = await runPi(prompt, config.model, config.thinking);
|
|
345
|
+
const result = await runPi(prompt, config.model, config.thinking, config.subagentTtlMs);
|
|
298
346
|
const output = result.stdout.trim();
|
|
299
347
|
const diagnostics = config.stats
|
|
300
348
|
? `\n\n## Diagnostics\n\nparticipant_attempts=${config.stats.participantAttempts}\nparticipant_success=${config.stats.participantSuccess}\nparticipant_failures=${config.stats.participantFailures}\nsynthesis_code=${result.code}\ntranscript_messages=${transcript.trim() ? transcript.split("\n").length : 0}\n`
|
|
@@ -417,7 +465,7 @@ async function executeParticipantPrompt(role, basePrompt, config) {
|
|
|
417
465
|
finalPrompt += inboxSection;
|
|
418
466
|
}
|
|
419
467
|
|
|
420
|
-
const result = await runPi(finalPrompt, config.model, config.thinking);
|
|
468
|
+
const result = await runPi(finalPrompt, config.model, config.thinking, config.subagentTtlMs);
|
|
421
469
|
|
|
422
470
|
if (claimedIds.length > 0) {
|
|
423
471
|
const finalStatus = result.code === 0 ? "handled" : "failed";
|
|
@@ -454,14 +502,14 @@ async function participantJoin(role, config) {
|
|
|
454
502
|
const displayName = role.name;
|
|
455
503
|
const address = `branch:${config.runId}/${role.name}`;
|
|
456
504
|
const joinPrompt = `You are ${displayName}, ${role.persona}. Mission: ${config.mission}. Call tool message exactly once with to=${shellQuote(config.room)}, from=${shellQuote(address)}, type='actor.join', summary='${displayName} joined', body JSON {"role":${JSON.stringify(role.persona)},"display":${JSON.stringify(displayName)},"caps":["coordination","synthesis"],"claim":"coordinate on mission"}. Then print one short line.`;
|
|
457
|
-
await runPi(joinPrompt, config.model, config.thinking);
|
|
505
|
+
await runPi(joinPrompt, config.model, config.thinking, config.subagentTtlMs);
|
|
458
506
|
}
|
|
459
507
|
|
|
460
508
|
async function participantLeave(role, config) {
|
|
461
509
|
const displayName = role.name;
|
|
462
510
|
const address = `branch:${config.runId}/${role.name}`;
|
|
463
511
|
const leavePrompt = `Call tool message exactly once with to=${shellQuote(config.room)}, from=${shellQuote(address)}, type='actor.leave', summary='${displayName} left', body='finished coordinated work'. Then print goodbye.`;
|
|
464
|
-
await runPi(leavePrompt, config.model, config.thinking);
|
|
512
|
+
await runPi(leavePrompt, config.model, config.thinking, config.subagentTtlMs);
|
|
465
513
|
}
|
|
466
514
|
|
|
467
515
|
// 1. consensus / swarm mode: iterative chat in a room
|
|
@@ -646,6 +694,7 @@ const config = {
|
|
|
646
694
|
artifactPath: arg("artifact-path", ""),
|
|
647
695
|
locker: boolArg("locker", false),
|
|
648
696
|
lockerLeaseMs: numberArg("locker-lease-ms", 600000),
|
|
697
|
+
subagentTtlMs: numberArg("subagent-ttl-ms", 0),
|
|
649
698
|
stats: {
|
|
650
699
|
participantAttempts: 0,
|
|
651
700
|
participantSuccess: 0,
|
package/lib/recipe-discovery.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* File-discovered recipe registry helpers
|
|
3
|
-
* Zones: recipe discovery, tool exposure,
|
|
3
|
+
* Zones: recipe discovery, tool exposure, registry diagnostics
|
|
4
4
|
* Owns filename identity discovery across prioritized recipe roots
|
|
5
5
|
*/
|
|
6
6
|
|
|
@@ -98,8 +98,7 @@ function listRecipeFiles(root: string): string[] {
|
|
|
98
98
|
.filter(
|
|
99
99
|
(entry) =>
|
|
100
100
|
entry.isFile() &&
|
|
101
|
-
(entry.name.endsWith(".json") || entry.name.endsWith(".md"))
|
|
102
|
-
entry.name !== "legacy-tool-registry-migration-report.json",
|
|
101
|
+
(entry.name.endsWith(".json") || entry.name.endsWith(".md")),
|
|
103
102
|
)
|
|
104
103
|
.map((entry) => join(root, entry.name))
|
|
105
104
|
.sort(
|
package/lib/runtime.ts
CHANGED
|
@@ -8,7 +8,6 @@ import * as Config from "./config.ts";
|
|
|
8
8
|
import type { RegisteredToolExec } from "./execution.ts";
|
|
9
9
|
import * as Paths from "./paths.ts";
|
|
10
10
|
import * as RecipeDiscovery from "./recipe-discovery.ts";
|
|
11
|
-
import * as RecipeMigration from "./recipe-migration.ts";
|
|
12
11
|
import * as Tools from "./tools.ts";
|
|
13
12
|
|
|
14
13
|
export interface RuntimeContext {
|
|
@@ -142,20 +141,6 @@ export function createAutoToolsRuntime(
|
|
|
142
141
|
const recipeRoot = deps.recipeRoot ?? Paths.getRecipeRoot();
|
|
143
142
|
const packagedRecipeRoot =
|
|
144
143
|
deps.packagedRecipeRoot ?? Paths.getPackagedRecipeRoot();
|
|
145
|
-
const migration = RecipeMigration.migrateLegacyToolRegistry({
|
|
146
|
-
configPath: deps.configPath,
|
|
147
|
-
recipeRoot,
|
|
148
|
-
reservedToolNames: deps.reservedToolNames,
|
|
149
|
-
});
|
|
150
|
-
warnings.push(...migration.warnings);
|
|
151
|
-
if (migration.conflicts.length > 0)
|
|
152
|
-
warnings.push(
|
|
153
|
-
`Recipe migration conflicts: ${migration.conflicts.join(", ")}`,
|
|
154
|
-
);
|
|
155
|
-
if (migration.invalid.length > 0)
|
|
156
|
-
warnings.push(
|
|
157
|
-
`Recipe migration invalid entries: ${migration.invalid.join(", ")}`,
|
|
158
|
-
);
|
|
159
144
|
const discovered = RecipeDiscovery.discoverRecipeSources([
|
|
160
145
|
{ root: recipeRoot, defaultTool: true, mutableUsage: true },
|
|
161
146
|
{ root: packagedRecipeRoot },
|
package/package.json
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"delay:int",
|
|
12
12
|
"locker:bool",
|
|
13
13
|
"locker_lease_ms:int",
|
|
14
|
+
"subagent_ttl_ms:int",
|
|
14
15
|
"artifact_path:path",
|
|
15
16
|
"repo:path"
|
|
16
17
|
],
|
|
@@ -23,6 +24,7 @@
|
|
|
23
24
|
"delay": "10",
|
|
24
25
|
"locker": "false",
|
|
25
26
|
"locker_lease_ms": "600000",
|
|
27
|
+
"subagent_ttl_ms": "0",
|
|
26
28
|
"artifact_path": "{state_dir}/room-swarm-artifact.md",
|
|
27
29
|
"repo": "~/.pi/agent/extensions/pi-actors"
|
|
28
30
|
},
|
|
@@ -44,5 +46,5 @@
|
|
|
44
46
|
"run.failed"
|
|
45
47
|
]
|
|
46
48
|
},
|
|
47
|
-
"template": "{repo}/scripts/coordinator.mjs --run-id={run_id} --mode={mode} --mission={mission} --model={model} --thinking={thinking} --roles={roles} --roles-path={roles_path} --rounds={rounds} --delay={delay} --locker={locker} --locker-lease-ms={locker_lease_ms} --artifact-path={artifact_path}"
|
|
49
|
+
"template": "{repo}/scripts/coordinator.mjs --run-id={run_id} --mode={mode} --mission={mission} --model={model} --thinking={thinking} --roles={roles} --roles-path={roles_path} --rounds={rounds} --delay={delay} --locker={locker} --locker-lease-ms={locker_lease_ms} --subagent-ttl-ms={subagent_ttl_ms} --artifact-path={artifact_path}"
|
|
48
50
|
}
|
package/skills/actors/SKILL.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: actors
|
|
3
3
|
description: Highest-density practical guide for pi-actors. Read this skill whenever prompt and tools are not enough for spawn, message, inspect, actor runs, tools, recipes, command templates, async lifecycle, mailboxes, artifacts, and local orchestration mechanics.
|
|
4
4
|
metadata:
|
|
5
|
-
version: 0.29.
|
|
5
|
+
version: 0.29.2
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Actors (pi-actors)
|
|
@@ -302,7 +302,7 @@ Use packaged recipes by name with `spawn file=<name>` for async actors, or regis
|
|
|
302
302
|
- [`pipeline-docs-maintenance`](../../recipes/pipeline-docs-maintenance.json): docs index/review/planning → maintenance artifact.
|
|
303
303
|
- Artifacts: [`pipeline-artifact-report`](../../recipes/pipeline-artifact-report.json), [`pipeline-artifact-write`](../../recipes/pipeline-artifact-write.json), [`pipeline-artifact-bundle`](../../recipes/pipeline-artifact-bundle.json).
|
|
304
304
|
- Review gates: [`pipeline-quorum-review`](../../recipes/pipeline-quorum-review.json), [`pipeline-review-readiness`](../../recipes/pipeline-review-readiness.json).
|
|
305
|
-
- Task-first workflows: [`pipeline-architect-coordinator`](../../recipes/pipeline-architect-coordinator.json), [`pipeline-research-synthesis`](../../recipes/pipeline-research-synthesis.json), [`pipeline-development-tasking`](../../recipes/pipeline-development-tasking.json), [`pipeline-checkpoint-continuation`](../../recipes/pipeline-checkpoint-continuation.json), [`pipeline-media-library`](../../recipes/pipeline-media-library.json), [`pipeline-room-swarm`](../../recipes/pipeline-room-swarm.json). For room swarms, choose `mode` from `consensus`, `pipeline`, `fanout`, or `pool`; prefer `roles_path` for custom role JSON and keep role `name` ASCII-safe for branch addresses. Use `locker=true` when the swarm needs a coordinator-locker-backed artifact lock and journal.
|
|
305
|
+
- Task-first workflows: [`pipeline-architect-coordinator`](../../recipes/pipeline-architect-coordinator.json), [`pipeline-research-synthesis`](../../recipes/pipeline-research-synthesis.json), [`pipeline-development-tasking`](../../recipes/pipeline-development-tasking.json), [`pipeline-checkpoint-continuation`](../../recipes/pipeline-checkpoint-continuation.json), [`pipeline-media-library`](../../recipes/pipeline-media-library.json), [`pipeline-room-swarm`](../../recipes/pipeline-room-swarm.json). For room swarms, choose `mode` from `consensus`, `pipeline`, `fanout`, or `pool`; prefer `roles_path` for custom role JSON and keep role `name` ASCII-safe for branch addresses. Set `subagent_ttl_ms` when participant processes need a hard kill budget. Use `locker=true` when the swarm needs a coordinator-locker-backed artifact lock and journal.
|
|
306
306
|
|
|
307
307
|
### Utilities
|
|
308
308
|
|
package/skills/swarm/SKILL.md
CHANGED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Legacy tool-registry to recipe-file migration helpers
|
|
3
|
-
* Zones: registry migration, recipe persistence, compatibility diagnostics
|
|
4
|
-
* Owns one-way migration from legacy tool registry entries into user recipe files
|
|
5
|
-
*/
|
|
6
|
-
export interface LegacyRegistryMigrationResult {
|
|
7
|
-
migrated: string[];
|
|
8
|
-
skipped: string[];
|
|
9
|
-
conflicts: string[];
|
|
10
|
-
invalid: string[];
|
|
11
|
-
archive?: string;
|
|
12
|
-
report?: string;
|
|
13
|
-
warnings: string[];
|
|
14
|
-
}
|
|
15
|
-
export interface LegacyRegistryMigrationOptions {
|
|
16
|
-
configPath: string;
|
|
17
|
-
recipeRoot: string;
|
|
18
|
-
reservedToolNames: Set<string>;
|
|
19
|
-
archive?: boolean;
|
|
20
|
-
}
|
|
21
|
-
export declare function migrateLegacyToolRegistry(options: LegacyRegistryMigrationOptions): LegacyRegistryMigrationResult;
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Legacy tool-registry to recipe-file migration helpers
|
|
3
|
-
* Zones: registry migration, recipe persistence, compatibility diagnostics
|
|
4
|
-
* Owns one-way migration from legacy tool registry entries into user recipe files
|
|
5
|
-
*/
|
|
6
|
-
import { existsSync, mkdirSync, renameSync } from "node:fs";
|
|
7
|
-
import { basename, join } from "node:path";
|
|
8
|
-
import * as Config from "./config.js";
|
|
9
|
-
import { writeJsonAtomic } from "./file-state.js";
|
|
10
|
-
import * as RecipeReferences from "./recipe-references.js";
|
|
11
|
-
function recipePathForTool(recipeRoot, name) {
|
|
12
|
-
return join(recipeRoot, `${name}.json`);
|
|
13
|
-
}
|
|
14
|
-
function toRecipeConfig(tool) {
|
|
15
|
-
return {
|
|
16
|
-
description: tool.description,
|
|
17
|
-
...(tool.recipe?.async !== undefined ? { async: tool.recipe.async } : {}),
|
|
18
|
-
...(tool.recipe?.state_dir ? { state_dir: tool.recipe.state_dir } : {}),
|
|
19
|
-
...(tool.storedArgs ? { args: tool.storedArgs } : {}),
|
|
20
|
-
...(tool.storedDefaults ? { defaults: tool.storedDefaults } : {}),
|
|
21
|
-
template: tool.template,
|
|
22
|
-
...(tool.recipe?.values ? { values: tool.recipe.values } : {}),
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
function nextArchivePath(configPath) {
|
|
26
|
-
const base = `${configPath}.migrated`;
|
|
27
|
-
if (!existsSync(base))
|
|
28
|
-
return base;
|
|
29
|
-
for (let index = 1; index < 1000; index += 1) {
|
|
30
|
-
const path = `${base}.${index}`;
|
|
31
|
-
if (!existsSync(path))
|
|
32
|
-
return path;
|
|
33
|
-
}
|
|
34
|
-
return `${base}.${Date.now()}`;
|
|
35
|
-
}
|
|
36
|
-
export function migrateLegacyToolRegistry(options) {
|
|
37
|
-
const { configPath, recipeRoot, reservedToolNames } = options;
|
|
38
|
-
const result = {
|
|
39
|
-
migrated: [],
|
|
40
|
-
skipped: [],
|
|
41
|
-
conflicts: [],
|
|
42
|
-
invalid: [],
|
|
43
|
-
warnings: [],
|
|
44
|
-
};
|
|
45
|
-
if (!existsSync(configPath))
|
|
46
|
-
return result;
|
|
47
|
-
const loaded = Config.loadToolConfig(configPath, reservedToolNames);
|
|
48
|
-
result.warnings.push(...loaded.warnings);
|
|
49
|
-
mkdirSync(recipeRoot, { recursive: true });
|
|
50
|
-
for (const [name, tool] of loaded.tools) {
|
|
51
|
-
const path = recipePathForTool(recipeRoot, name);
|
|
52
|
-
if (existsSync(path)) {
|
|
53
|
-
result.conflicts.push(name);
|
|
54
|
-
continue;
|
|
55
|
-
}
|
|
56
|
-
const recipe = toRecipeConfig(tool);
|
|
57
|
-
try {
|
|
58
|
-
writeJsonAtomic(path, recipe);
|
|
59
|
-
const parsed = RecipeReferences.readResolvedRecipeConfig(path);
|
|
60
|
-
if (!parsed) {
|
|
61
|
-
result.invalid.push(name);
|
|
62
|
-
continue;
|
|
63
|
-
}
|
|
64
|
-
result.migrated.push(name);
|
|
65
|
-
}
|
|
66
|
-
catch (error) {
|
|
67
|
-
result.invalid.push(`${name}: ${error instanceof Error ? error.message : String(error)}`);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
if (loaded.tools.size === 0)
|
|
71
|
-
result.skipped.push(basename(configPath));
|
|
72
|
-
const reportPath = join(recipeRoot, "legacy-tool-registry-migration-report.json");
|
|
73
|
-
writeJsonAtomic(reportPath, {
|
|
74
|
-
source: configPath,
|
|
75
|
-
migrated: result.migrated,
|
|
76
|
-
skipped: result.skipped,
|
|
77
|
-
conflicts: result.conflicts,
|
|
78
|
-
invalid: result.invalid,
|
|
79
|
-
warnings: result.warnings,
|
|
80
|
-
});
|
|
81
|
-
result.report = reportPath;
|
|
82
|
-
if (options.archive !== false &&
|
|
83
|
-
result.conflicts.length === 0 &&
|
|
84
|
-
result.invalid.length === 0) {
|
|
85
|
-
const archive = nextArchivePath(configPath);
|
|
86
|
-
renameSync(configPath, archive);
|
|
87
|
-
result.archive = archive;
|
|
88
|
-
}
|
|
89
|
-
return result;
|
|
90
|
-
}
|
package/lib/recipe-migration.ts
DELETED
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Legacy tool-registry to recipe-file migration helpers
|
|
3
|
-
* Zones: registry migration, recipe persistence, compatibility diagnostics
|
|
4
|
-
* Owns one-way migration from legacy tool registry entries into user recipe files
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { existsSync, mkdirSync, renameSync } from "node:fs";
|
|
8
|
-
import { basename, join } from "node:path";
|
|
9
|
-
|
|
10
|
-
import * as Config from "./config.ts";
|
|
11
|
-
import { writeJsonAtomic } from "./file-state.ts";
|
|
12
|
-
import type { TemplateRecipeConfig } from "./recipe-references.ts";
|
|
13
|
-
import * as RecipeReferences from "./recipe-references.ts";
|
|
14
|
-
|
|
15
|
-
export interface LegacyRegistryMigrationResult {
|
|
16
|
-
migrated: string[];
|
|
17
|
-
skipped: string[];
|
|
18
|
-
conflicts: string[];
|
|
19
|
-
invalid: string[];
|
|
20
|
-
archive?: string;
|
|
21
|
-
report?: string;
|
|
22
|
-
warnings: string[];
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export interface LegacyRegistryMigrationOptions {
|
|
26
|
-
configPath: string;
|
|
27
|
-
recipeRoot: string;
|
|
28
|
-
reservedToolNames: Set<string>;
|
|
29
|
-
archive?: boolean;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function recipePathForTool(recipeRoot: string, name: string): string {
|
|
33
|
-
return join(recipeRoot, `${name}.json`);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function toRecipeConfig(tool: Config.RegisteredTool): TemplateRecipeConfig {
|
|
37
|
-
return {
|
|
38
|
-
description: tool.description,
|
|
39
|
-
...(tool.recipe?.async !== undefined ? { async: tool.recipe.async } : {}),
|
|
40
|
-
...(tool.recipe?.state_dir ? { state_dir: tool.recipe.state_dir } : {}),
|
|
41
|
-
...(tool.storedArgs ? { args: tool.storedArgs } : {}),
|
|
42
|
-
...(tool.storedDefaults ? { defaults: tool.storedDefaults } : {}),
|
|
43
|
-
template: tool.template!,
|
|
44
|
-
...(tool.recipe?.values ? { values: tool.recipe.values } : {}),
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function nextArchivePath(configPath: string): string {
|
|
49
|
-
const base = `${configPath}.migrated`;
|
|
50
|
-
if (!existsSync(base)) return base;
|
|
51
|
-
for (let index = 1; index < 1000; index += 1) {
|
|
52
|
-
const path = `${base}.${index}`;
|
|
53
|
-
if (!existsSync(path)) return path;
|
|
54
|
-
}
|
|
55
|
-
return `${base}.${Date.now()}`;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export function migrateLegacyToolRegistry(
|
|
59
|
-
options: LegacyRegistryMigrationOptions,
|
|
60
|
-
): LegacyRegistryMigrationResult {
|
|
61
|
-
const { configPath, recipeRoot, reservedToolNames } = options;
|
|
62
|
-
const result: LegacyRegistryMigrationResult = {
|
|
63
|
-
migrated: [],
|
|
64
|
-
skipped: [],
|
|
65
|
-
conflicts: [],
|
|
66
|
-
invalid: [],
|
|
67
|
-
warnings: [],
|
|
68
|
-
};
|
|
69
|
-
if (!existsSync(configPath)) return result;
|
|
70
|
-
|
|
71
|
-
const loaded = Config.loadToolConfig(configPath, reservedToolNames);
|
|
72
|
-
result.warnings.push(...loaded.warnings);
|
|
73
|
-
mkdirSync(recipeRoot, { recursive: true });
|
|
74
|
-
|
|
75
|
-
for (const [name, tool] of loaded.tools) {
|
|
76
|
-
const path = recipePathForTool(recipeRoot, name);
|
|
77
|
-
if (existsSync(path)) {
|
|
78
|
-
result.conflicts.push(name);
|
|
79
|
-
continue;
|
|
80
|
-
}
|
|
81
|
-
const recipe = toRecipeConfig(tool);
|
|
82
|
-
try {
|
|
83
|
-
writeJsonAtomic(path, recipe);
|
|
84
|
-
const parsed = RecipeReferences.readResolvedRecipeConfig(path);
|
|
85
|
-
if (!parsed) {
|
|
86
|
-
result.invalid.push(name);
|
|
87
|
-
continue;
|
|
88
|
-
}
|
|
89
|
-
result.migrated.push(name);
|
|
90
|
-
} catch (error) {
|
|
91
|
-
result.invalid.push(
|
|
92
|
-
`${name}: ${error instanceof Error ? error.message : String(error)}`,
|
|
93
|
-
);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if (loaded.tools.size === 0) result.skipped.push(basename(configPath));
|
|
98
|
-
|
|
99
|
-
const reportPath = join(recipeRoot, "legacy-tool-registry-migration-report.json");
|
|
100
|
-
writeJsonAtomic(reportPath, {
|
|
101
|
-
source: configPath,
|
|
102
|
-
migrated: result.migrated,
|
|
103
|
-
skipped: result.skipped,
|
|
104
|
-
conflicts: result.conflicts,
|
|
105
|
-
invalid: result.invalid,
|
|
106
|
-
warnings: result.warnings,
|
|
107
|
-
});
|
|
108
|
-
result.report = reportPath;
|
|
109
|
-
|
|
110
|
-
if (
|
|
111
|
-
options.archive !== false &&
|
|
112
|
-
result.conflicts.length === 0 &&
|
|
113
|
-
result.invalid.length === 0
|
|
114
|
-
) {
|
|
115
|
-
const archive = nextArchivePath(configPath);
|
|
116
|
-
renameSync(configPath, archive);
|
|
117
|
-
result.archive = archive;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
return result;
|
|
121
|
-
}
|