@llblab/pi-actors 0.34.1 → 0.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/BACKLOG.md CHANGED
@@ -53,24 +53,6 @@ No open hotfix items.
53
53
 
54
54
  The backlog is intentionally pruned to the 20% of work most likely to deliver 80% of value for `pi-actors` as a local actor kernel. Bias toward consolidation, smaller public surface area, and reliability over new feature breadth.
55
55
 
56
- ### M-18 Draft Recipe Promotion UX
57
-
58
- - Priority: High.
59
- - Status: Planned.
60
- - Goal: Make successful ad hoc actor patterns easy to promote manually from draft memory into active user recipe memory.
61
- - Why now: Draft recipes under `~/.pi/agent/recipes/drafts` are replayable but intentionally not active tools, and the two-stage memory model needs an explicit operator-gated promotion path.
62
- - Direction:
63
- - List draft recipes with source run, timestamp, fingerprint, description/template preview, and validation status.
64
- - Promote a selected draft to `~/.pi/agent/recipes/<name>.json` only through an explicit action or explicit tool argument.
65
- - Run recipe validation/doctor before writing and expose collision/shadowing diagnostics.
66
- - Preserve draft files unless deletion is explicitly requested.
67
- - Prefer extending existing registry/tool surfaces over adding a new public noun.
68
- - Acceptance:
69
- - Draft recipes remain non-tools until promotion.
70
- - Promotion writes atomically and never auto-promotes.
71
- - Tests cover valid promotion, invalid draft, name collision, and packaged-recipe shadowing.
72
- - Docs explain draft memory vs active tool memory in one compact section.
73
-
74
56
  ### M-19 Recipe Doctor Risk Labels v2
75
57
 
76
58
  - Priority: Medium.
@@ -154,7 +136,7 @@ These are valid ideas but not current focus. Reintroduce only with concrete evid
154
136
  ## Suggested Milestone Order
155
137
 
156
138
  ```text
157
- Next milestone: M-18 Draft Recipe Promotion UX.
158
- Then: M-19 Recipe Doctor Risk Labels v2.
139
+ Next milestone: M-19 Recipe Doctor Risk Labels v2.
140
+ Then: M-20 Runtime Recipe Triage.
159
141
  Small cleanup lane: continue opportunistic domain polish only when a real ownership boundary appears.
160
142
  ```
package/CHANGELOG.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.35.0: Draft Recipe Promotion UX
6
+
7
+ - `[Recipes]` Completed M-18 Draft Recipe Promotion UX: `inspect target=recipes view=summary verbose=true` now exposes draft timestamps, fingerprints, validation state, source run when known, and template previews, while `register_tool name=<tool> draft=<path>` promotes a validated draft into active recipe memory without deleting the draft and rejects collisions unless `update=true` is explicit.
8
+
5
9
  ## 0.34.1: Message Delivery Outcome Hotfix
6
10
 
7
11
  - `[Dogfood]` Added a deterministic actor-worker stale-claim smoke covering an intentionally claimed branch inbox record; the worker now reports stale claim counts in both `worker-status.json` and its awaiting-assignment room event without adding auto-recovery or scheduler policy.
package/README.md CHANGED
@@ -207,7 +207,8 @@ Rules:
207
207
  - User recipes override same-name lower-priority recipes;
208
208
  - Same-id JSON recipes shadow Markdown recipes in the same priority layer;
209
209
  - Packaged recipes are standard-library components, not automatically installed operator policy;
210
- - `register_tool` creates, updates, lists, or deletes user recipe files through the normal agent interface.
210
+ - Draft recipes in `~/.pi/agent/recipes/drafts/` are replayable memory, not active tools;
211
+ - `register_tool` creates, updates, lists, deletes, or explicitly promotes draft recipe files through the normal agent interface.
211
212
 
212
213
  Example foreground tool:
213
214
 
@@ -226,6 +227,14 @@ register_tool name=docs_review \
226
227
  args="scope:path,model:string"
227
228
  ```
228
229
 
230
+ Promote a successful captured draft only after an explicit operator decision:
231
+
232
+ ```text
233
+ register_tool name=docs_review draft=~/.pi/agent/recipes/drafts/spawned-run.json
234
+ ```
235
+
236
+ Promotion validates the draft, writes `~/.pi/agent/recipes/<name>.json`, preserves the draft, and rejects collisions unless `update=true` is supplied.
237
+
229
238
  Inspect the discovered registry:
230
239
 
231
240
  ```text
@@ -10,6 +10,7 @@ export declare const ONBOARDING_SYSTEM_PROMPT = "pi-actors quick model:\n- Local
10
10
  export declare const REGISTER_TOOL_PARAM_DESCRIPTIONS: {
11
11
  readonly name: "Tool name in snake_case (e.g., 'transcribe')";
12
12
  readonly description: "Describe what the tool does for the LLM. Required unless deleting; omitted updates keep the old description.";
13
+ readonly draft: "Promote a draft recipe path from ~/.pi/agent/recipes/drafts into an active named recipe under ~/.pi/agent/recipes. Requires name; use update=true to overwrite.";
13
14
  readonly async: "Set true for a co-located async template recipe. Omit for ordinary command templates or file-backed recipe references.";
14
15
  readonly state_dir: "Optional async run state directory for a co-located template recipe.";
15
16
  readonly template: "Command template with {arg} or {arg=default} placeholders, or a template recipe JSON path/name. With async, this is the co-located recipe body. Bare recipe names resolve under ~/.pi/agent/recipes. Omitted updates keep the old template. Empty string deletes the tool.";
@@ -30,6 +30,7 @@ export const ONBOARDING_SYSTEM_PROMPT = `pi-actors quick model:
30
30
  export const REGISTER_TOOL_PARAM_DESCRIPTIONS = {
31
31
  name: "Tool name in snake_case (e.g., 'transcribe')",
32
32
  description: "Describe what the tool does for the LLM. Required unless deleting; omitted updates keep the old description.",
33
+ draft: "Promote a draft recipe path from ~/.pi/agent/recipes/drafts into an active named recipe under ~/.pi/agent/recipes. Requires name; use update=true to overwrite.",
33
34
  async: "Set true for a co-located async template recipe. Omit for ordinary command templates or file-backed recipe references.",
34
35
  state_dir: "Optional async run state directory for a co-located template recipe.",
35
36
  template: "Command template with {arg} or {arg=default} placeholders, or a template recipe JSON path/name. With async, this is the co-located recipe body. Bare recipe names resolve under ~/.pi/agent/recipes. Omitted updates keep the old template. Empty string deletes the tool.",
@@ -425,14 +425,39 @@ export function getShadowedLaunchDiagnostic(result, id) {
425
425
  reason: active.invalid ? "shadowed_invalid" : "shadowed_disabled",
426
426
  };
427
427
  }
428
+ function templatePreview(value) {
429
+ const rendered = typeof value === "string"
430
+ ? value
431
+ : value === undefined
432
+ ? undefined
433
+ : JSON.stringify(value);
434
+ return rendered && rendered.length > 120
435
+ ? `${rendered.slice(0, 117)}...`
436
+ : rendered;
437
+ }
428
438
  export function listDraftRecipes(root) {
429
439
  return listRecipeFiles(root).map((path) => {
430
440
  const id = RecipesReferences.getRecipeIdFromPath(path);
441
+ const bytes = readFileSync(path);
442
+ const stat = statSync(path);
431
443
  const config = RecipesReferences.readRawRecipeConfig(path);
444
+ const resolved = RecipesReferences.readResolvedRecipeConfig(path);
445
+ const diagnostics = getRecipeConfigDiagnostics(path, resolved);
446
+ const sourceRun = String(config?.description ?? "").match(/spawn run ([^\s]+)/)?.[1];
447
+ const preview = templatePreview(config?.template);
432
448
  return {
433
449
  id,
434
450
  path,
451
+ sha256: createHash("sha256").update(bytes).digest("hex"),
452
+ size: bytes.byteLength,
453
+ created_at: stat.birthtime.toISOString(),
454
+ modified_at: stat.mtime.toISOString(),
455
+ valid: Boolean(resolved),
456
+ diagnostics,
435
457
  ...(config?.description ? { description: config.description } : {}),
458
+ ...(sourceRun ? { source_run: sourceRun } : {}),
459
+ ...(config?.async !== undefined ? { async: config.async } : {}),
460
+ ...(preview ? { template_preview: preview } : {}),
436
461
  };
437
462
  });
438
463
  }
@@ -11,6 +11,7 @@ export interface RegisterToolInput {
11
11
  async?: boolean;
12
12
  state_dir?: string;
13
13
  template?: CommandTemplates.CommandTemplateValue | null;
14
+ draft?: string;
14
15
  args?: string;
15
16
  update?: boolean;
16
17
  values?: Record<string, unknown>;
@@ -20,6 +21,8 @@ export interface RegisterToolResultDetails {
20
21
  async?: boolean;
21
22
  config?: string;
22
23
  defaults?: Record<string, string>;
24
+ draft?: string;
25
+ promoted?: boolean;
23
26
  recipeName?: string;
24
27
  state_dir?: string;
25
28
  template?: CommandTemplates.CommandTemplateValue;
@@ -4,7 +4,7 @@
4
4
  * Owns register/update/delete validation, persistence, runtime side effects, and result payloads
5
5
  */
6
6
  import { existsSync, mkdirSync, unlinkSync } from "node:fs";
7
- import { dirname, join } from "node:path";
7
+ import { dirname, join, relative, resolve } from "node:path";
8
8
  import * as CommandTemplates from "./command-templates.js";
9
9
  import * as ExecutionOutput from "./execution-output.js";
10
10
  import { writeJsonAtomic } from "./file-state.js";
@@ -32,6 +32,62 @@ function getRecipeRoot(deps) {
32
32
  function getToolRecipePath(deps, name) {
33
33
  return join(getRecipeRoot(deps), `${name}.json`);
34
34
  }
35
+ function assertDraftPath(deps, draft) {
36
+ const draftRoot = resolve(getRecipeRoot(deps), "drafts");
37
+ const path = resolve(draft);
38
+ const relation = relative(draftRoot, path);
39
+ if (relation === "" ||
40
+ relation.startsWith("..") ||
41
+ resolve(relation) === relation) {
42
+ throw new Error(ExecutionOutput.formatToolText(`Draft must be under ${draftRoot}. Use inspect target=recipes view=summary to list drafts.`));
43
+ }
44
+ if (!existsSync(path)) {
45
+ throw new Error(ExecutionOutput.formatToolText(`Draft not found: ${path}`));
46
+ }
47
+ return path;
48
+ }
49
+ function promoteDraftRecipe(name, input, ctx, deps) {
50
+ const draftPath = assertDraftPath(deps, String(input.draft ?? ""));
51
+ const targetPath = getToolRecipePath(deps, name);
52
+ const tools = deps.getTools();
53
+ const existing = tools.get(name);
54
+ const conflict = deps.getExternalToolConflict(name);
55
+ if (conflict)
56
+ throw new Error(ExecutionOutput.formatToolText(conflict));
57
+ if ((existing || existsSync(targetPath)) && !input.update) {
58
+ throw new Error(ExecutionOutput.formatToolText(`Tool "${name}" already registered. Use update=true to overwrite.`));
59
+ }
60
+ const config = RecipesReferences.readResolvedRecipeConfig(draftPath);
61
+ if (!config) {
62
+ const reason = RecipesReferences.diagnoseRawRecipeConfigFailure(draftPath);
63
+ throw new Error(ExecutionOutput.formatToolText(`Draft recipe is invalid${reason ? `: ${reason}` : "."}`));
64
+ }
65
+ const promoted = buildConfig(name, {
66
+ ...input,
67
+ description: input.description ?? config.description,
68
+ template: draftPath,
69
+ }, existing);
70
+ const raw = RecipesReferences.readRawRecipeConfig(draftPath);
71
+ mkdirSync(dirname(targetPath), { recursive: true });
72
+ writeJsonAtomic(targetPath, raw);
73
+ promoted.template = targetPath;
74
+ promoted.sourcePath = targetPath;
75
+ tools.set(name, promoted);
76
+ deps.registerRuntimeTool(promoted);
77
+ deps.notify(ctx, `Promoted draft recipe: ${name}`, "info");
78
+ return {
79
+ content: [
80
+ textContent(ExecutionOutput.formatToolText(`${existing ? "Updated" : "Registered"} tool "${name}" from draft recipe.`)),
81
+ ],
82
+ details: {
83
+ args: promoted.args,
84
+ config: targetPath,
85
+ draft: draftPath,
86
+ promoted: true,
87
+ tool: name,
88
+ },
89
+ };
90
+ }
35
91
  function persistToolRecipe(deps, cfg) {
36
92
  const path = getToolRecipePath(deps, cfg.name);
37
93
  mkdirSync(dirname(path), { recursive: true });
@@ -173,6 +229,9 @@ export async function executeRegisterTool(params, ctx, deps) {
173
229
  if (deps.reservedToolNames.has(name)) {
174
230
  throw new Error(ExecutionOutput.formatToolText(`Reserved tool name: ${name}`));
175
231
  }
232
+ if (typeof input.draft === "string" && input.draft.trim()) {
233
+ return promoteDraftRecipe(name, input, ctx, deps);
234
+ }
176
235
  const templateProvided = Object.hasOwn(input, "template");
177
236
  const template = getInputTemplate(input.template);
178
237
  if (templateProvided && (template === null || template === ""))
@@ -24,6 +24,7 @@ export function createRegisterToolDefinition(deps) {
24
24
  args: stringSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.args),
25
25
  async: booleanSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.async),
26
26
  description: stringSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.description),
27
+ draft: stringSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.draft),
27
28
  name: stringSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.name),
28
29
  state_dir: stringSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.state_dir),
29
30
  template: unionSchema([
@@ -2,7 +2,7 @@
2
2
  name: actors
3
3
  description: Required practical guide for non-trivial pi-actors use. Read before using or changing spawn, message, inspect, actor runs, tools, recipes, command templates, async lifecycle, mailboxes, artifacts, and local orchestration mechanics.
4
4
  metadata:
5
- version: 0.34.1
5
+ version: 0.35.0
6
6
  ---
7
7
 
8
8
  # Actors (pi-actors)
@@ -2,7 +2,7 @@
2
2
  name: swarm
3
3
  description: Subagent orchestration with scoped locks and quorum consensus. Use for multi-model review, parallel scoped work, delegated audit, and coordinated subagent execution.
4
4
  metadata:
5
- version: 0.34.1
5
+ version: 0.35.0
6
6
  ---
7
7
 
8
8
  # Swarm
@@ -10,7 +10,7 @@ The registry source is location-discovered recipes, not a live tool-only JSON fi
10
10
 
11
11
  - `~/.pi/agent/recipes/*.json` and `*.md` are the highest-priority user recipe root and the operator-managed tool set.
12
12
  - Recipes in that root are tools by location.
13
- - `~/.pi/agent/recipes/drafts/*.json` stores captured inline-spawn draft recipes, not registered tools. Promote one by moving or copying it up one level into `~/.pi/agent/recipes`. `inspect target=recipes view=summary` reports their count, and verbose output lists their paths/descriptions for explicit replay by file path.
13
+ - `~/.pi/agent/recipes/drafts/*.json` stores captured inline-spawn draft recipes, not registered tools. Promote one with `register_tool name=<tool_name> draft=<draft_path>` or by manually moving/copying it up one level into `~/.pi/agent/recipes`. `inspect target=recipes view=summary` reports their count, and verbose output lists their paths, timestamps, fingerprints, validation state, source run when known, descriptions, and template previews for explicit replay or promotion.
14
14
  - Packaged pi-actors recipes are the lower-priority standard library of declarative actor config components, not automatically registered tools.
15
15
  - Ad hoc recipe files outside the user recipe root are components unless explicitly registered/copied into `~/.pi/agent/recipes`.
16
16
  - Recipe identity is the filename basename; `~/.pi/agent/recipes/docs_review.json` and `docs_review.md` both have id/tool name `docs_review`.
@@ -51,7 +51,7 @@ register_tool name=call_subagent \
51
51
  template="pi -p --model {model} --no-tools {prompt}" args="prompt:string,model:string"
52
52
  ```
53
53
 
54
- Use `update=true` to overwrite an existing tool. Omit `template` and co-located recipe fields during update to keep the previous execution binding.
54
+ Use `update=true` to overwrite an existing tool. Omit `template` and co-located recipe fields during update to keep the previous execution binding. To promote a captured draft, pass `name` plus `draft` with a path under `~/.pi/agent/recipes/drafts`; promotion validates the draft before writing `~/.pi/agent/recipes/<name>.json`, preserves the draft file, rejects name collisions unless `update=true`, and leaves shadowing evidence visible through `inspect target=recipes view=summary` or `view=doctor`.
55
55
 
56
56
  `template` may also be a standard command-template sequence for multi-step tools. Timeout is disabled by default; add explicit positive `timeout` values when individual steps should fail closed:
57
57
 
package/lib/prompts.ts CHANGED
@@ -38,6 +38,8 @@ export const REGISTER_TOOL_PARAM_DESCRIPTIONS = {
38
38
  name: "Tool name in snake_case (e.g., 'transcribe')",
39
39
  description:
40
40
  "Describe what the tool does for the LLM. Required unless deleting; omitted updates keep the old description.",
41
+ draft:
42
+ "Promote a draft recipe path from ~/.pi/agent/recipes/drafts into an active named recipe under ~/.pi/agent/recipes. Requires name; use update=true to overwrite.",
41
43
  async:
42
44
  "Set true for a co-located async template recipe. Omit for ordinary command templates or file-backed recipe references.",
43
45
  state_dir:
@@ -574,14 +574,43 @@ export function getShadowedLaunchDiagnostic(
574
574
  };
575
575
  }
576
576
 
577
+ function templatePreview(value: unknown): string | undefined {
578
+ const rendered =
579
+ typeof value === "string"
580
+ ? value
581
+ : value === undefined
582
+ ? undefined
583
+ : JSON.stringify(value);
584
+ return rendered && rendered.length > 120
585
+ ? `${rendered.slice(0, 117)}...`
586
+ : rendered;
587
+ }
588
+
577
589
  export function listDraftRecipes(root: string): Array<Record<string, unknown>> {
578
590
  return listRecipeFiles(root).map((path) => {
579
591
  const id = RecipesReferences.getRecipeIdFromPath(path);
592
+ const bytes = readFileSync(path);
593
+ const stat = statSync(path);
580
594
  const config = RecipesReferences.readRawRecipeConfig(path);
595
+ const resolved = RecipesReferences.readResolvedRecipeConfig(path);
596
+ const diagnostics = getRecipeConfigDiagnostics(path, resolved);
597
+ const sourceRun = String(config?.description ?? "").match(
598
+ /spawn run ([^\s]+)/,
599
+ )?.[1];
600
+ const preview = templatePreview(config?.template);
581
601
  return {
582
602
  id,
583
603
  path,
604
+ sha256: createHash("sha256").update(bytes).digest("hex"),
605
+ size: bytes.byteLength,
606
+ created_at: stat.birthtime.toISOString(),
607
+ modified_at: stat.mtime.toISOString(),
608
+ valid: Boolean(resolved),
609
+ diagnostics,
584
610
  ...(config?.description ? { description: config.description } : {}),
611
+ ...(sourceRun ? { source_run: sourceRun } : {}),
612
+ ...(config?.async !== undefined ? { async: config.async } : {}),
613
+ ...(preview ? { template_preview: preview } : {}),
585
614
  };
586
615
  });
587
616
  }
package/lib/registry.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import { existsSync, mkdirSync, unlinkSync } from "node:fs";
8
- import { dirname, join } from "node:path";
8
+ import { dirname, join, relative, resolve } from "node:path";
9
9
 
10
10
  import * as CommandTemplates from "./command-templates.ts";
11
11
  import * as Config from "./config.ts";
@@ -22,6 +22,7 @@ export interface RegisterToolInput {
22
22
  async?: boolean;
23
23
  state_dir?: string;
24
24
  template?: CommandTemplates.CommandTemplateValue | null;
25
+ draft?: string;
25
26
  args?: string;
26
27
  update?: boolean;
27
28
  values?: Record<string, unknown>;
@@ -32,6 +33,8 @@ export interface RegisterToolResultDetails {
32
33
  async?: boolean;
33
34
  config?: string;
34
35
  defaults?: Record<string, string>;
36
+ draft?: string;
37
+ promoted?: boolean;
35
38
  recipeName?: string;
36
39
  state_dir?: string;
37
40
  template?: CommandTemplates.CommandTemplateValue;
@@ -95,6 +98,93 @@ function getToolRecipePath<TContext>(
95
98
  return join(getRecipeRoot(deps), `${name}.json`);
96
99
  }
97
100
 
101
+ function assertDraftPath<TContext>(
102
+ deps: RegisterToolRuntimeDeps<TContext>,
103
+ draft: string,
104
+ ): string {
105
+ const draftRoot = resolve(getRecipeRoot(deps), "drafts");
106
+ const path = resolve(draft);
107
+ const relation = relative(draftRoot, path);
108
+ if (
109
+ relation === "" ||
110
+ relation.startsWith("..") ||
111
+ resolve(relation) === relation
112
+ ) {
113
+ throw new Error(
114
+ ExecutionOutput.formatToolText(
115
+ `Draft must be under ${draftRoot}. Use inspect target=recipes view=summary to list drafts.`,
116
+ ),
117
+ );
118
+ }
119
+ if (!existsSync(path)) {
120
+ throw new Error(ExecutionOutput.formatToolText(`Draft not found: ${path}`));
121
+ }
122
+ return path;
123
+ }
124
+
125
+ function promoteDraftRecipe<TContext>(
126
+ name: string,
127
+ input: RegisterToolInput,
128
+ ctx: TContext,
129
+ deps: RegisterToolRuntimeDeps<TContext>,
130
+ ): RegisterToolResult {
131
+ const draftPath = assertDraftPath(deps, String(input.draft ?? ""));
132
+ const targetPath = getToolRecipePath(deps, name);
133
+ const tools = deps.getTools();
134
+ const existing = tools.get(name);
135
+ const conflict = deps.getExternalToolConflict(name);
136
+ if (conflict) throw new Error(ExecutionOutput.formatToolText(conflict));
137
+ if ((existing || existsSync(targetPath)) && !input.update) {
138
+ throw new Error(
139
+ ExecutionOutput.formatToolText(
140
+ `Tool "${name}" already registered. Use update=true to overwrite.`,
141
+ ),
142
+ );
143
+ }
144
+ const config = RecipesReferences.readResolvedRecipeConfig(draftPath);
145
+ if (!config) {
146
+ const reason = RecipesReferences.diagnoseRawRecipeConfigFailure(draftPath);
147
+ throw new Error(
148
+ ExecutionOutput.formatToolText(
149
+ `Draft recipe is invalid${reason ? `: ${reason}` : "."}`,
150
+ ),
151
+ );
152
+ }
153
+ const promoted = buildConfig(
154
+ name,
155
+ {
156
+ ...input,
157
+ description: input.description ?? config.description,
158
+ template: draftPath,
159
+ },
160
+ existing,
161
+ );
162
+ const raw = RecipesReferences.readRawRecipeConfig(draftPath)!;
163
+ mkdirSync(dirname(targetPath), { recursive: true });
164
+ writeJsonAtomic(targetPath, raw);
165
+ promoted.template = targetPath;
166
+ promoted.sourcePath = targetPath;
167
+ tools.set(name, promoted);
168
+ deps.registerRuntimeTool(promoted);
169
+ deps.notify(ctx, `Promoted draft recipe: ${name}`, "info");
170
+ return {
171
+ content: [
172
+ textContent(
173
+ ExecutionOutput.formatToolText(
174
+ `${existing ? "Updated" : "Registered"} tool "${name}" from draft recipe.`,
175
+ ),
176
+ ),
177
+ ],
178
+ details: {
179
+ args: promoted.args,
180
+ config: targetPath,
181
+ draft: draftPath,
182
+ promoted: true,
183
+ tool: name,
184
+ } as RegisterToolResultDetails & Record<string, unknown>,
185
+ };
186
+ }
187
+
98
188
  function persistToolRecipe<TContext>(
99
189
  deps: RegisterToolRuntimeDeps<TContext>,
100
190
  cfg: Config.RegisteredTool,
@@ -284,6 +374,9 @@ export async function executeRegisterTool<TContext>(
284
374
  ExecutionOutput.formatToolText(`Reserved tool name: ${name}`),
285
375
  );
286
376
  }
377
+ if (typeof input.draft === "string" && input.draft.trim()) {
378
+ return promoteDraftRecipe(name, input, ctx, deps);
379
+ }
287
380
  const templateProvided = Object.hasOwn(input, "template");
288
381
  const template = getInputTemplate(input.template);
289
382
  if (templateProvided && (template === null || template === ""))
@@ -36,6 +36,7 @@ export function createRegisterToolDefinition<TContext>(
36
36
  description: stringSchema(
37
37
  Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.description,
38
38
  ),
39
+ draft: stringSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.draft),
39
40
  name: stringSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.name),
40
41
  state_dir: stringSchema(
41
42
  Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.state_dir,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-actors",
3
- "version": "0.34.1",
3
+ "version": "0.35.0",
4
4
  "private": false,
5
5
  "description": "Local Actor Kernel for Pi",
6
6
  "keywords": [
@@ -2,7 +2,7 @@
2
2
  name: actors
3
3
  description: Required practical guide for non-trivial pi-actors use. Read before using or changing spawn, message, inspect, actor runs, tools, recipes, command templates, async lifecycle, mailboxes, artifacts, and local orchestration mechanics.
4
4
  metadata:
5
- version: 0.34.1
5
+ version: 0.35.0
6
6
  ---
7
7
 
8
8
  # Actors (pi-actors)
@@ -2,7 +2,7 @@
2
2
  name: swarm
3
3
  description: Subagent orchestration with scoped locks and quorum consensus. Use for multi-model review, parallel scoped work, delegated audit, and coordinated subagent execution.
4
4
  metadata:
5
- version: 0.34.1
5
+ version: 0.35.0
6
6
  ---
7
7
 
8
8
  # Swarm