pi-subagents 0.42.1 → 0.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +3 -5
  3. package/package.json +1 -1
  4. package/skills/pi-subagents/SKILL.md +2 -2
  5. package/skills/pi-subagents/references/constraints-and-recipes.md +35 -32
  6. package/skills/pi-subagents/references/execution-controls.md +30 -44
  7. package/skills/pi-subagents/references/management-authoring-rpc.md +20 -3
  8. package/skills/pi-subagents/references/prompting-and-roles.md +16 -49
  9. package/src/agents/agent-refinements.ts +624 -0
  10. package/src/agents/agents.ts +0 -2
  11. package/src/agents/proactive-skills.ts +1 -1
  12. package/src/api/delegation.ts +1 -2
  13. package/src/extension/config.ts +28 -7
  14. package/src/extension/control-notices.ts +2 -2
  15. package/src/extension/fanout-child.ts +46 -22
  16. package/src/extension/index.ts +32 -13
  17. package/src/extension/public-execution.ts +71 -0
  18. package/src/extension/rpc.ts +7 -8
  19. package/src/extension/schemas.ts +14 -14
  20. package/src/extension/tool-description.ts +14 -14
  21. package/src/missions/actions.ts +47 -11
  22. package/src/missions/goal-driver.ts +162 -0
  23. package/src/missions/lifecycle.ts +44 -12
  24. package/src/missions/store.ts +68 -3
  25. package/src/missions/types.ts +25 -3
  26. package/src/missions/workflow-state.ts +250 -0
  27. package/src/profiles/profiles.ts +1 -3
  28. package/src/runs/background/async-execution.ts +5 -1
  29. package/src/runs/background/async-job-tracker.ts +2 -17
  30. package/src/runs/background/async-status.ts +2 -0
  31. package/src/runs/background/control-channel.ts +50 -6
  32. package/src/runs/background/retained-children.ts +68 -0
  33. package/src/runs/background/run-id-resolver.ts +71 -0
  34. package/src/runs/background/run-status.ts +3 -2
  35. package/src/runs/background/scheduled-runs.ts +17 -13
  36. package/src/runs/background/steering.ts +7 -5
  37. package/src/runs/background/subagent-runner.ts +50 -23
  38. package/src/runs/foreground/async-steering-action.ts +36 -10
  39. package/src/runs/foreground/chain-clarify.ts +7 -3
  40. package/src/runs/foreground/execution.ts +29 -19
  41. package/src/runs/foreground/subagent-executor.ts +282 -77
  42. package/src/runs/shared/acceptance.ts +154 -9
  43. package/src/runs/shared/subagent-prompt-runtime.ts +97 -25
  44. package/src/shared/settings.ts +10 -6
  45. package/src/shared/types.ts +22 -2
  46. package/src/slash/delegation-adapters.ts +0 -13
  47. package/src/slash/prompt-template-bridge.ts +14 -12
  48. package/src/slash/prompt-workflows.ts +10 -4
  49. package/src/slash/slash-bridge.ts +8 -6
  50. package/src/slash/slash-commands.ts +24 -9
  51. package/src/slash/slash-live-state.ts +4 -2
  52. package/src/tui/fleet-status.ts +8 -4
  53. package/src/tui/fleet.ts +15 -5
  54. package/src/tui/render.ts +10 -29
  55. package/src/workflows/scripted-workflow.ts +91 -6
  56. package/agents/context-builder.md +0 -46
  57. package/agents/planner.md +0 -56
  58. package/prompts/parallel-context-build.md +0 -55
  59. package/prompts/parallel-handoff-plan.md +0 -61
@@ -213,13 +213,19 @@ function workflowParams(workflow: PromptWorkflow, args: string[], runtime: Retur
213
213
  return {
214
214
  agent: runtime.agentOverride ?? workflow.agent,
215
215
  task,
216
- clarify: false,
217
216
  agentScope: "both",
218
217
  ...(context ? { context } : {}),
219
218
  ...(workflow.model ? { model: workflow.model } : {}),
220
219
  ...(workflow.skill !== undefined ? { skill: workflow.skill } : {}),
221
220
  ...(workflow.cwd ? { cwd: workflow.cwd } : {}),
222
- ...(runtime.bg ? { async: true } : {}),
221
+ };
222
+ }
223
+
224
+ function promptWorkflowExecutionParams(workflows: PromptWorkflow[], args: string[], runtime: ReturnType<typeof parseRuntimeOptions>): SubagentParamsLike {
225
+ return {
226
+ workflowScript: promptWorkflowScript(workflows, args, runtime),
227
+ agentScope: "both",
228
+ async: runtime.bg ? true : false,
223
229
  };
224
230
  }
225
231
 
@@ -281,10 +287,10 @@ export function registerPromptWorkflowCommands(input: {
281
287
  if (!step) throw new Error(`Unknown prompt workflow in chain '${workflow.name}': ${stepName}`);
282
288
  return step;
283
289
  });
284
- await run({ workflowScript: promptWorkflowScript(chain, runtime.args, runtime), clarify: false, agentScope: "both", ...(runtime.bg ? { async: true } : {}) }, ctx);
290
+ await run(promptWorkflowExecutionParams(chain, runtime.args, runtime), ctx);
285
291
  return;
286
292
  }
287
- await run(workflowParams(workflow, runtime.args, runtime), ctx);
293
+ await run(promptWorkflowExecutionParams([workflow], runtime.args, runtime), ctx);
288
294
  } catch (error) {
289
295
  ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
290
296
  }
@@ -1,5 +1,6 @@
1
1
  import type { AgentToolResult } from "@earendil-works/pi-agent-core";
2
2
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
3
+ import { normalizePublicSubagentExecution } from "../extension/public-execution.ts";
3
4
  import type { SubagentParamsLike } from "../runs/foreground/subagent-executor.ts";
4
5
  import {
5
6
  SLASH_SUBAGENT_CANCEL_EVENT,
@@ -77,21 +78,22 @@ export function registerSlashSubagentBridge(options: SlashBridgeOptions): {
77
78
  if (!data || typeof data !== "object") return;
78
79
  const request = data as Partial<SlashSubagentRequest>;
79
80
  if (typeof request.requestId !== "string" || !request.params) return;
80
- const { requestId, params } = request as SlashSubagentRequest;
81
- if (params.tasks !== undefined || params.chain !== undefined || params.concurrency !== undefined || params.worktree !== undefined || params.chainDir !== undefined) {
82
- const message = "Legacy slash-event chain and parallel inputs were removed; use workflowScript.";
81
+ const { requestId } = request as SlashSubagentRequest;
82
+ const normalized = normalizePublicSubagentExecution((request as SlashSubagentRequest).params);
83
+ if (!normalized.ok) {
83
84
  options.events.emit(SLASH_SUBAGENT_RESPONSE_EVENT, {
84
85
  requestId,
85
86
  result: {
86
- content: [{ type: "text", text: message }],
87
+ content: [{ type: "text", text: normalized.error }],
87
88
  isError: true,
88
- details: { mode: "management", results: [] },
89
+ details: { mode: normalized.mode, results: [] },
89
90
  },
90
91
  isError: true,
91
- errorText: message,
92
+ errorText: normalized.error,
92
93
  } satisfies SlashSubagentResponse);
93
94
  return;
94
95
  }
96
+ const params = normalized.params;
95
97
 
96
98
  const ctx = request.ctx ?? options.getContext();
97
99
  if (!ctx) {
@@ -621,6 +621,9 @@ function launchSlashSubagent(
621
621
  void runSlashSubagent(pi, ctx, params);
622
622
  }
623
623
 
624
+ function slashRunWorkflowScript(key: string, child: Record<string, unknown>): string {
625
+ return `return runs.run(${JSON.stringify(key)}, ${JSON.stringify(child)})`;
626
+ }
624
627
 
625
628
  export function registerSlashCommands(
626
629
  pi: ExtensionAPI,
@@ -653,7 +656,7 @@ export function registerSlashCommands(
653
656
  });
654
657
 
655
658
  pi.registerCommand("run", {
656
- description: "Run a subagent directly: /run agent[output=file] [task] [--bg] [--fork]",
659
+ description: "Run one subagent through workflowScript: /run agent[output=file] [task] [--bg] [--fork]",
657
660
  getArgumentCompletions: makeAgentCompletions(state),
658
661
  handler: async (args, ctx) => {
659
662
  const { args: cleanedArgs, bg, fork } = extractExecutionFlags(args);
@@ -671,14 +674,13 @@ export function registerSlashCommands(
671
674
  if (inline.reads && Array.isArray(inline.reads) && inline.reads.length > 0) {
672
675
  finalTask = `[Read from: ${inline.reads.join(", ")}]\n\n${finalTask}`;
673
676
  }
674
- const params: SubagentParamsLike = { agent: agentName, task: finalTask, clarify: false, agentScope: "both" };
675
- if (inline.output !== undefined) params.output = inline.output;
676
- if (inline.outputMode !== undefined) params.outputMode = inline.outputMode;
677
- if (inline.skill !== undefined) params.skill = inline.skill;
678
- if (inline.model) params.model = inline.model;
679
- if (bg) params.async = true;
680
- if (fork) params.context = "fork";
681
- launchSlashSubagent(pi, ctx, params);
677
+ const child: Record<string, unknown> = { agent: agentName, task: finalTask, agentScope: "both" };
678
+ if (inline.output !== undefined) child.output = inline.output;
679
+ if (inline.outputMode !== undefined) child.outputMode = inline.outputMode;
680
+ if (inline.skill !== undefined) child.skill = inline.skill;
681
+ if (inline.model) child.model = inline.model;
682
+ if (fork) child.context = "fork";
683
+ launchSlashSubagent(pi, ctx, { workflowScript: slashRunWorkflowScript("run", child), async: bg ? true : false });
682
684
  },
683
685
  });
684
686
 
@@ -696,6 +698,19 @@ export function registerSlashCommands(
696
698
  },
697
699
  });
698
700
 
701
+ pi.registerCommand("subagents-refine", {
702
+ description: "Generate a bounded project-local refinement overlay for one subagent",
703
+ getArgumentCompletions: makeAgentCompletions(state),
704
+ handler: async (args, ctx) => {
705
+ const parts = args.trim().split(/\s+/).filter(Boolean);
706
+ if (parts.length !== 1) {
707
+ ctx.ui.notify("Usage: /subagents-refine <agent>", "error");
708
+ return;
709
+ }
710
+ await runSlashSubagent(pi, ctx, { action: "refine", agent: parts[0] });
711
+ },
712
+ });
713
+
699
714
  pi.registerCommand("subagents-fleet", {
700
715
  description: "Open the live subagent fleet inspector",
701
716
  handler: async (_args, ctx) => showFleet(ctx),
@@ -1,6 +1,7 @@
1
1
  import type { AgentToolResult } from "@earendil-works/pi-agent-core";
2
2
  import type { Message } from "@earendil-works/pi-ai";
3
3
  import type { SubagentParamsLike } from "../runs/foreground/subagent-executor.ts";
4
+ import { previewSimpleWorkflowRun } from "../workflows/scripted-workflow.ts";
4
5
  import type { SlashSubagentResponse, SlashSubagentUpdate } from "./slash-bridge.ts";
5
6
  import { type Details, type SingleResult, type Usage, SLASH_RESULT_TYPE } from "../shared/types.ts";
6
7
 
@@ -156,8 +157,9 @@ function buildChainInitialResult(params: SubagentParamsLike): AgentToolResult<De
156
157
  }
157
158
 
158
159
  function buildSingleInitialResult(params: SubagentParamsLike): AgentToolResult<Details> {
159
- const agent = params.agent ?? "subagent";
160
- const task = params.task ?? "";
160
+ const preview = previewSimpleWorkflowRun(params.workflowScript) ?? {};
161
+ const agent = params.agent ?? preview.agent ?? "subagent";
162
+ const task = params.task ?? preview.task ?? "";
161
163
  return {
162
164
  content: [{ type: "text", text: task }],
163
165
  details: {
@@ -100,21 +100,25 @@ function nestedFleetRows(children: NestedRunSummary[] | undefined): FleetNestedR
100
100
  const steps = (child.mode === "parallel" || child.mode === "chain") ? child.steps ?? [] : [];
101
101
  if (steps.length > 0) {
102
102
  for (const step of steps) {
103
+ const modelThinking = formatModelThinking(step.model, step.thinking) || undefined;
104
+ const activity = nestedActivity(step);
103
105
  rows.push({
104
106
  name: step.agent,
105
107
  state: step.status,
106
- ...(formatModelThinking(step.model, step.thinking) ? { modelThinking: formatModelThinking(step.model, step.thinking) } : {}),
107
- ...(nestedActivity(step) ? { activity: nestedActivity(step) } : {}),
108
+ ...(modelThinking ? { modelThinking } : {}),
109
+ ...(activity ? { activity } : {}),
108
110
  ...(step.startedAt !== undefined ? { startedAt: step.startedAt } : {}),
109
111
  });
110
112
  }
111
113
  continue;
112
114
  }
115
+ const modelThinking = formatModelThinking(child.model, child.thinking) || undefined;
116
+ const activity = nestedActivity(child);
113
117
  rows.push({
114
118
  name: nestedRunLabel(child),
115
119
  state: child.state,
116
- ...(formatModelThinking(child.model, child.thinking) ? { modelThinking: formatModelThinking(child.model, child.thinking) } : {}),
117
- ...(nestedActivity(child) ? { activity: nestedActivity(child) } : {}),
120
+ ...(modelThinking ? { modelThinking } : {}),
121
+ ...(activity ? { activity } : {}),
118
122
  ...(child.startedAt !== undefined ? { startedAt: child.startedAt } : {}),
119
123
  });
120
124
  }
package/src/tui/fleet.ts CHANGED
@@ -10,6 +10,7 @@ import { readStatus } from "../shared/utils.ts";
10
10
  import { formatAsyncRunTranscript } from "../runs/background/fleet-view.ts";
11
11
  import { listAsyncRuns, type AsyncRunSummary } from "../runs/background/async-status.ts";
12
12
  import { steerAsyncRun } from "../runs/foreground/async-steering-action.ts";
13
+ import type { SteerDeliveryMode } from "../runs/background/control-channel.ts";
13
14
  import { stopAsyncRun } from "../runs/foreground/async-stop-action.ts";
14
15
  import { contextModeBadge, contextModeLabel } from "../runs/shared/context-mode.ts";
15
16
  import { FLEET_STATUS_WIDGET_KEY } from "./fleet-status.ts";
@@ -45,7 +46,7 @@ export interface FleetActionResult {
45
46
  }
46
47
 
47
48
  export interface FleetActionHandlers {
48
- steer(input: { runId: string; asyncDir: string; index?: number; message: string }): Promise<FleetActionResult>;
49
+ steer(input: { runId: string; asyncDir: string; index?: number; message: string; mode: SteerDeliveryMode }): Promise<FleetActionResult>;
49
50
  stop(input: { runId: string; asyncDir: string; index?: number }): Promise<FleetActionResult> | FleetActionResult;
50
51
  inspect?(input: { runId: string; asyncDir: string; index?: number }): Promise<FleetActionResult>;
51
52
  }
@@ -484,6 +485,7 @@ export class SubagentFleetComponent implements Component {
484
485
  private expandedTools = false;
485
486
  private actionNotice: FleetActionResult | undefined;
486
487
  private steerDraft: string | undefined;
488
+ private steerMode: SteerDeliveryMode = "steer";
487
489
  private stopConfirming = false;
488
490
  private actionBusy = false;
489
491
  private transcriptCache: FleetTranscriptCache | undefined;
@@ -538,6 +540,7 @@ export class SubagentFleetComponent implements Component {
538
540
 
539
541
  private resetActionInput(): void {
540
542
  this.steerDraft = undefined;
543
+ this.steerMode = "steer";
541
544
  this.stopConfirming = false;
542
545
  }
543
546
 
@@ -553,8 +556,8 @@ export class SubagentFleetComponent implements Component {
553
556
  const lines: string[] = [];
554
557
  if (this.actionBusy) lines.push(this.theme.fg("accent", "Action pending..."));
555
558
  if (this.steerDraft !== undefined) {
556
- lines.push(this.theme.fg("accent", `Steer message: ${this.steerDraft}${this.theme.fg("dim", "▌")}`));
557
- lines.push(this.theme.fg("dim", "Enter sends · Esc cancels · Backspace edits"));
559
+ lines.push(this.theme.fg("accent", `Steer message (${this.steerMode}): ${this.steerDraft}${this.theme.fg("dim", "▌")}`));
560
+ lines.push(this.theme.fg("dim", "Enter sends · Tab changes mode · Esc cancels · Backspace edits"));
558
561
  } else if (this.stopConfirming) {
559
562
  const selected = this.snapshot.items[this.selected];
560
563
  lines.push(this.theme.fg("warning", `Confirm stop for async run ${selected?.runId ?? "selected run"}?`));
@@ -618,7 +621,13 @@ export class SubagentFleetComponent implements Component {
618
621
  this.setActionNotice({ text: "reason" in target ? target.reason : "Fleet controls are unavailable in this context.", isError: true });
619
622
  return;
620
623
  }
621
- this.runAction(() => this.options.actions!.steer({ runId: target.item.runId, asyncDir: target.item.run.asyncDir, ...(target.item.index !== undefined ? { index: target.item.index } : {}), message }));
624
+ this.runAction(() => this.options.actions!.steer({ runId: target.item.runId, asyncDir: target.item.run.asyncDir, ...(target.item.index !== undefined ? { index: target.item.index } : {}), message, mode: this.steerMode }));
625
+ return;
626
+ }
627
+ if (matchesKey(data, "tab") || data === "\t") {
628
+ const modes: SteerDeliveryMode[] = ["steer", "follow_up", "auto"];
629
+ this.steerMode = modes[(modes.indexOf(this.steerMode) + 1) % modes.length]!;
630
+ this.tui.requestRender();
622
631
  return;
623
632
  }
624
633
  if (matchesKey(data, "backspace") || data === "\x7f") {
@@ -836,11 +845,12 @@ export async function openSubagentFleet(ctx: ExtensionContext, state: SubagentSt
836
845
  state.fleetInspectorOpen = true;
837
846
  if (typeof ctx.ui.setWidget === "function") ctx.ui.setWidget(FLEET_STATUS_WIDGET_KEY, undefined);
838
847
  const actions = options.actions ?? {
839
- steer: async (input: { runId: string; asyncDir: string; index?: number; message: string }) => firstToolResultText(await steerAsyncRun({
848
+ steer: async (input: { runId: string; asyncDir: string; index?: number; message: string; mode: SteerDeliveryMode }) => firstToolResultText(await steerAsyncRun({
840
849
  state,
841
850
  runId: input.runId,
842
851
  ...(input.index !== undefined ? { index: input.index } : {}),
843
852
  message: input.message,
853
+ mode: input.mode,
844
854
  location: { asyncDir: input.asyncDir, resolvedId: input.runId } as Parameters<typeof steerAsyncRun>[0]["location"],
845
855
  }), `Failed to steer async run ${input.runId}.`),
846
856
  stop: (input: { runId: string; asyncDir: string; index?: number }) => firstToolResultText(stopAsyncRun(state, input.runId, undefined, { asyncDir: input.asyncDir, resolvedId: input.runId }), `Failed to stop async run ${input.runId}.`),
package/src/tui/render.ts CHANGED
@@ -53,6 +53,7 @@ const ansiStylePattern = /\x1b\[[0-9;]*m/y;
53
53
  * Uses Intl.Segmenter for proper Unicode/emoji handling (not char-by-char).
54
54
  */
55
55
  export function truncLine(text: string, maxWidth: number): string {
56
+ if (maxWidth <= 0) return "";
56
57
  if (visibleWidth(text) <= maxWidth) return text;
57
58
 
58
59
  const targetWidth = maxWidth - 1;
@@ -83,7 +84,7 @@ export function truncLine(text: string, maxWidth: number): string {
83
84
  const textPortion = text.slice(i, end);
84
85
  for (const seg of segmenter.segment(textPortion)) {
85
86
  const grapheme = seg.segment;
86
- const graphemeWidth = visibleWidth(grapheme);
87
+ const graphemeWidth = grapheme === "\x1b" ? 0 : visibleWidth(grapheme);
87
88
 
88
89
  if (currentWidth + graphemeWidth > targetWidth) {
89
90
  return result + activeStyles.join("") + "…";
@@ -111,6 +112,7 @@ function wrapPlainText(text: string, maxWidth: number): string[] {
111
112
  for (const seg of segmenter.segment(rawLine)) {
112
113
  const grapheme = seg.segment;
113
114
  const graphemeWidth = visibleWidth(grapheme);
115
+ if (graphemeWidth > maxWidth) continue;
114
116
  if (currentWidth > 0 && currentWidth + graphemeWidth > maxWidth) {
115
117
  lines.push(current);
116
118
  current = grapheme;
@@ -144,24 +146,11 @@ function runningGlyph(seed?: number): string {
144
146
  return RUNNING_FRAMES[Math.abs(seed) % RUNNING_FRAMES.length]!;
145
147
  }
146
148
 
147
- const WIDGET_ANIMATION_FRAME_MS = 500;
148
- let widgetRequestRender: (() => void) | undefined;
149
-
150
149
  function animatedSeed(seed: number | undefined, frame: number | undefined): number | undefined {
151
150
  if (frame === undefined) return seed;
152
151
  return (seed ?? 0) + frame;
153
152
  }
154
153
 
155
- function widgetAnimationFrame(): number {
156
- return Math.floor(Date.now() / WIDGET_ANIMATION_FRAME_MS);
157
- }
158
-
159
- export function requestWidgetRender(): boolean {
160
- if (!widgetRequestRender) return false;
161
- widgetRequestRender();
162
- return true;
163
- }
164
-
165
154
  function progressRunningSeed(progress: ProgressSeedSource | undefined): number | undefined {
166
155
  if (!progress) return undefined;
167
156
  return runningSeed(
@@ -1347,26 +1336,19 @@ function fitAdaptiveWidgetLines(jobs: AsyncJobState[], lines: string[], theme: T
1347
1336
  return rendered.lines;
1348
1337
  }
1349
1338
 
1350
- function buildWidgetComponent(jobs: AsyncJobState[], expanded: boolean): (tui: { requestRender(): void }, theme: Theme) => Component & { dispose?(): void } {
1351
- return (tui, theme) => {
1352
- const requestRender = () => tui.requestRender();
1353
- widgetRequestRender = requestRender;
1339
+ function buildWidgetComponent(jobs: AsyncJobState[], expanded: boolean): (_tui: unknown, theme: Theme) => Component {
1340
+ return (_tui, theme) => {
1354
1341
  const container = new Container();
1355
1342
  container.render = (renderWidth: number): string[] => {
1356
1343
  const width = Math.max(0, renderWidth - 2);
1357
- const frame = widgetAnimationFrame();
1358
1344
  const lines = expanded
1359
- ? buildWidgetLines(jobs, theme, width, true, frame)
1345
+ ? buildWidgetLines(jobs, theme, width, true)
1360
1346
  : jobs.length === 1
1361
- ? compactSingleWidgetLines(jobs[0]!, theme, width, frame)
1362
- : buildWidgetLines(jobs, theme, width, false, frame);
1363
- return fitAdaptiveWidgetLines(jobs, lines, theme, width, expanded, frame).map((line) => paddedWidgetLine(line, renderWidth));
1364
- };
1365
- const component = container as Component & { dispose?(): void };
1366
- component.dispose = () => {
1367
- if (widgetRequestRender === requestRender) widgetRequestRender = undefined;
1347
+ ? compactSingleWidgetLines(jobs[0]!, theme, width)
1348
+ : buildWidgetLines(jobs, theme, width, false);
1349
+ return fitAdaptiveWidgetLines(jobs, lines, theme, width, expanded).map((line) => paddedWidgetLine(line, renderWidth));
1368
1350
  };
1369
- return component;
1351
+ return container;
1370
1352
  };
1371
1353
  }
1372
1354
 
@@ -1446,7 +1428,6 @@ export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = ge
1446
1428
  export function renderWidget(ctx: ExtensionContext, jobs: AsyncJobState[]): void {
1447
1429
  if (jobs.length === 0) {
1448
1430
  resetWidgetLayoutSession();
1449
- widgetRequestRender = undefined;
1450
1431
  if (ctx.hasUI) ctx.ui.setWidget(WIDGET_KEY, undefined);
1451
1432
  return;
1452
1433
  }
@@ -37,11 +37,17 @@ const runFingerprints = new Map();
37
37
  function validateRunCall(key, params, label, fingerprints) {
38
38
  if (typeof key !== "string" || !runKeyPattern.test(key)) throw new Error(label + " has an invalid key.");
39
39
  if (!params || typeof params !== "object" || Array.isArray(params)) throw new Error(label + " requires a params object.");
40
- if (Object.prototype.hasOwnProperty.call(params, "action") || Object.prototype.hasOwnProperty.call(params, "workflowScript") || Object.prototype.hasOwnProperty.call(params, "tasks") || Object.prototype.hasOwnProperty.call(params, "chain") || Object.prototype.hasOwnProperty.call(params, "concurrency") || Object.prototype.hasOwnProperty.call(params, "chainDir")) {
40
+ if (Object.prototype.hasOwnProperty.call(params, "action") || Object.prototype.hasOwnProperty.call(params, "workflowScript") || Object.prototype.hasOwnProperty.call(params, "tasks") || Object.prototype.hasOwnProperty.call(params, "chain") || Object.prototype.hasOwnProperty.call(params, "parallel") || Object.prototype.hasOwnProperty.call(params, "concurrency") || Object.prototype.hasOwnProperty.call(params, "chainDir")) {
41
41
  const hint = label === "runs.run" ? "; use runs.all(...) and JavaScript control flow for orchestration." : ".";
42
42
  throw new Error(label + " accepts one child via { agent, task } and execution controls only" + hint);
43
43
  }
44
44
  if (params.worktree !== undefined && typeof params.worktree !== "boolean") throw new Error(label + " worktree must be true or false.");
45
+ if (params.gate !== undefined && (typeof params.gate !== "string" || !params.gate.trim())) throw new Error(label + " gate must be a non-empty command string.");
46
+ if (params.gate !== undefined && params.acceptance !== undefined) throw new Error(label + " gate cannot be combined with acceptance; use one gate command or acceptance.verify.");
47
+ if (params.gate !== undefined && params.resume !== undefined) throw new Error(label + " gate is not supported with retained resume.");
48
+ if (params.resume !== undefined && (typeof params.resume !== "string" || !params.resume.trim())) throw new Error(label + " resume must be a non-empty retained run id.");
49
+ if (params.resume !== undefined && params.agent !== undefined) throw new Error(label + " resume and agent are mutually exclusive.");
50
+ if (params.resume !== undefined && (typeof params.task !== "string" || !params.task.trim())) throw new Error(label + " resume requires a non-empty task follow-up.");
45
51
  assertJsonValue(params, label + " params");
46
52
  const fingerprint = stableRunJson(params);
47
53
  const existing = fingerprints.get(key);
@@ -77,6 +83,20 @@ const runs = Object.freeze({
77
83
  },
78
84
  });
79
85
 
86
+ function validateStateKey(key) {
87
+ if (typeof key !== "string" || !runKeyPattern.test(key)) throw new Error("state key must be 1-128 characters using letters, numbers, '.', '_' or '-', and start with a letter or number.");
88
+ return key;
89
+ }
90
+
91
+ const state = Object.freeze({
92
+ get(key) { return hostCall("state.get", { key: validateStateKey(key) }); },
93
+ set(key, value) {
94
+ const validKey = validateStateKey(key);
95
+ assertJsonValue(value, "state.set('" + validKey + "') value");
96
+ return hostCall("state.set", { key: validKey, value });
97
+ },
98
+ });
99
+
80
100
  let contextObjectPrototype;
81
101
 
82
102
  const capturedConsole = Object.freeze(Object.fromEntries(
@@ -120,6 +140,7 @@ parentPort.on("message", async (message) => {
120
140
  if (message.type !== "start") return;
121
141
  try {
122
142
  const sandbox = { runs, emit(value) { assertJsonValue(value); parentPort.postMessage({ type: "emit", value }); }, console: capturedConsole };
143
+ if (message.stateEnabled) sandbox.state = state;
123
144
  const context = vm.createContext(sandbox, { codeGeneration: { strings: false, wasm: false } });
124
145
  contextObjectPrototype = vm.runInContext("Object.prototype", context);
125
146
  const compiled = new vm.Script("(async () => {\n" + message.script + "\n})()", { filename: "workflow-script.js" });
@@ -179,6 +200,10 @@ export interface RunWorkflowScriptOptions {
179
200
  signal?: AbortSignal;
180
201
  launch: (key: string, params: Record<string, unknown>, signal: AbortSignal) => Promise<WorkflowScriptChildResult>;
181
202
  status: (keyOrRunId: string, signal: AbortSignal) => Promise<WorkflowScriptChildResult>;
203
+ state?: {
204
+ get: (key: string) => unknown | Promise<unknown>;
205
+ set: (key: string, value: unknown) => void | Promise<void>;
206
+ };
182
207
  onTrace?: (trace: WorkflowScriptTraceEntry[]) => void;
183
208
  onEmit?: (emits: unknown[]) => void;
184
209
  }
@@ -239,15 +264,39 @@ export function formatWorkflowJsonPreview(value: unknown, maxLength: number): st
239
264
  }
240
265
  }
241
266
 
267
+ export interface SimpleWorkflowRunPreview {
268
+ agent?: string;
269
+ task?: string;
270
+ }
271
+
272
+ /** Display-only preview for the exact simple `return runs.run(key, {...})` form. */
273
+ export function previewSimpleWorkflowRun(script: string | undefined): SimpleWorkflowRunPreview | undefined {
274
+ const body = script?.match(/^\s*return\s+(?:await\s+)?runs\.run\s*\(\s*(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`[^`$\\]*`)\s*,\s*\{([\s\S]*)\}\s*\)\s*;?\s*$/)?.[1];
275
+ if (body === undefined) return undefined;
276
+ const readProperty = (name: "agent" | "task"): string | undefined => {
277
+ const match = body.match(new RegExp(`(?:^|,)\\s*(?:${name}|["']${name}["'])\\s*:\\s*("(?:\\\\.|[^"\\\\])*"|'(?:\\\\.|[^'\\\\])*'|\u0060[^\u0060$\\\\]*\u0060)`));
278
+ if (!match?.[1]) return undefined;
279
+ const literal = match[1];
280
+ if (literal.startsWith('"')) {
281
+ try { return JSON.parse(literal) as string; } catch { return undefined; }
282
+ }
283
+ if (literal.slice(1, -1).includes("\\")) return undefined;
284
+ return literal.slice(1, -1);
285
+ };
286
+ const agent = readProperty("agent");
287
+ const task = readProperty("task");
288
+ return { ...(agent !== undefined ? { agent } : {}), ...(task !== undefined ? { task } : {}) };
289
+ }
290
+
242
291
  function stableJson(value: unknown): string {
243
292
  if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
244
293
  if (isRecord(value)) return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(",")}}`;
245
294
  return JSON.stringify(value) ?? "undefined";
246
295
  }
247
296
 
248
- function validateKey(value: unknown): string {
297
+ function validateKey(value: unknown, owner = "runs.run"): string {
249
298
  if (typeof value !== "string" || !KEY_PATTERN.test(value)) {
250
- throw new Error("runs.run key must be 1-128 characters using letters, numbers, '.', '_' or '-', and start with a letter or number.");
299
+ throw new Error(`${owner} key must be 1-128 characters using letters, numbers, '.', '_' or '-', and start with a letter or number.`);
251
300
  }
252
301
  return value;
253
302
  }
@@ -334,13 +383,31 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
334
383
  if (message.type === "error") return finish({ error: new Error(typeof message.error === "string" ? message.error : "Workflow script failed.") });
335
384
  if (message.type !== "call" || typeof message.callId !== "number" || typeof message.method !== "string" || !isRecord(message.args)) return;
336
385
 
337
- const respond = (promise: Promise<WorkflowScriptChildResult>) => {
386
+ const respond = (promise: Promise<unknown>) => {
338
387
  void promise.then(
339
388
  (value) => worker.postMessage({ type: "response", callId: message.callId, ok: true, value: omitUndefinedWorkflowValues(value) }),
340
389
  (error: unknown) => worker.postMessage({ type: "response", callId: message.callId, ok: false, error: error instanceof Error ? error.message : String(error) }),
341
390
  );
342
391
  };
343
392
 
393
+ if (message.method === "state.get" || message.method === "state.set") {
394
+ if (!options.state) return respond(Promise.reject(new Error("Workflow state is unavailable without a mission.")));
395
+ let key: string;
396
+ try {
397
+ key = validateKey(message.args.key, "state");
398
+ } catch (error) {
399
+ return respond(Promise.reject(error));
400
+ }
401
+ if (message.method === "state.get") return respond(Promise.resolve().then(() => options.state!.get(key)));
402
+ const value = message.args.value;
403
+ try {
404
+ assertWorkflowJsonValue(value, `state.set('${key}') value`);
405
+ } catch (error) {
406
+ return respond(Promise.reject(error));
407
+ }
408
+ return respond(Promise.resolve().then(() => options.state!.set(key, value)));
409
+ }
410
+
344
411
  if (message.method === "status") {
345
412
  const keyOrRunId = message.args.keyOrRunId;
346
413
  if (typeof keyOrRunId !== "string" || !keyOrRunId.trim()) return respond(Promise.reject(new Error("runs.status(keyOrRunId) requires a non-empty string.")));
@@ -368,12 +435,30 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
368
435
  if (!isRecord(params)) return respond(Promise.reject(new Error(`runs.run('${key}', params) requires a params object.`)));
369
436
  if (params.action !== undefined) return respond(Promise.reject(new Error(`runs.run('${key}') accepts execution params only; management action is not allowed.`)));
370
437
  if (params.workflowScript !== undefined) return respond(Promise.reject(new Error(`runs.run('${key}') cannot start a nested workflow script.`)));
371
- if (params.tasks !== undefined || params.chain !== undefined || params.concurrency !== undefined || params.chainDir !== undefined) {
438
+ if (params.tasks !== undefined || params.chain !== undefined || params.parallel !== undefined || params.concurrency !== undefined || params.chainDir !== undefined) {
372
439
  return respond(Promise.reject(new Error(`runs.run('${key}') accepts one child via { agent, task }; use runs.all(...) and JavaScript control flow for orchestration.`)));
373
440
  }
374
441
  if (params.worktree !== undefined && typeof params.worktree !== "boolean") {
375
442
  return respond(Promise.reject(new Error(`runs.run('${key}') worktree must be true or false.`)));
376
443
  }
444
+ if (params.gate !== undefined && (typeof params.gate !== "string" || !params.gate.trim())) {
445
+ return respond(Promise.reject(new Error(`runs.run('${key}') gate must be a non-empty command string.`)));
446
+ }
447
+ if (params.gate !== undefined && params.acceptance !== undefined) {
448
+ return respond(Promise.reject(new Error(`runs.run('${key}') gate cannot be combined with acceptance; use one gate command or acceptance.verify.`)));
449
+ }
450
+ if (params.gate !== undefined && params.resume !== undefined) {
451
+ return respond(Promise.reject(new Error(`runs.run('${key}') gate is not supported with retained resume.`)));
452
+ }
453
+ if (params.resume !== undefined && (typeof params.resume !== "string" || !params.resume.trim())) {
454
+ return respond(Promise.reject(new Error(`runs.run('${key}') resume must be a non-empty retained run id.`)));
455
+ }
456
+ if (params.resume !== undefined && params.agent !== undefined) {
457
+ return respond(Promise.reject(new Error(`runs.run('${key}') resume and agent are mutually exclusive.`)));
458
+ }
459
+ if (params.resume !== undefined && (typeof params.task !== "string" || !params.task.trim())) {
460
+ return respond(Promise.reject(new Error(`runs.run('${key}') resume requires a non-empty task follow-up.`)));
461
+ }
377
462
  const collectFailure = message.args.collectFailure === true;
378
463
  const deliver = (promise: Promise<WorkflowScriptChildResult>) => collectFailure
379
464
  ? promise
@@ -412,6 +497,6 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
412
497
  respond(deliver(promise));
413
498
  });
414
499
 
415
- worker.postMessage({ type: "start", script: options.script });
500
+ worker.postMessage({ type: "start", script: options.script, stateEnabled: options.state !== undefined });
416
501
  });
417
502
  }
@@ -1,46 +0,0 @@
1
- ---
2
- name: context-builder
3
- description: Analyzes requirements and codebase, generates context and meta-prompt
4
- tools: read, grep, find, ls, bash, write, web_search, intercom
5
- thinking: medium
6
- systemPromptMode: replace
7
- inheritProjectContext: true
8
- inheritSkills: false
9
- output: context.md
10
- ---
11
-
12
- You are a requirements-to-context subagent.
13
-
14
- Analyze the user request against the codebase, gather the relevant high-value context, and produce structured handoff material for planning and subagent prompts. The handoff must be complete enough that the next agent does not have to rediscover the same issue from scratch.
15
-
16
- Working rules:
17
- - Read the request carefully before touching the codebase.
18
- - Search the codebase for relevant files, patterns, dependencies, and constraints.
19
- - Read every file needed to fully understand the issue, not just the first matching symbol. Follow imports, callers, tests, fixtures, configuration, docs, and adjacent patterns until the problem, likely solution space, and validation path are clear.
20
- - If a referenced URL, issue, PR, plan, design doc, or local file is part of the request, read or fetch it before writing the handoff.
21
- - Conduct web research when the task depends on external APIs, libraries, current best practices, recently changed behavior, or when local evidence is not enough to know how to solve the problem correctly. Use `web_search` if it is available; otherwise use whatever equivalent research capability is available.
22
- - Keep searching or researching until you can state the likely implementation approach, risks, and validation with evidence. If a gap remains, call it out explicitly instead of implying certainty.
23
- - Write the requested output files clearly and concretely.
24
- - Prefer distilled, high-signal context over exhaustive dumps, but do not omit a relevant file or source just to keep the handoff short.
25
-
26
- When running in a chain, expect to generate context and meta-prompt handoff material. Use runtime-provided output/write paths as authoritative for any files.
27
-
28
- Context handoff:
29
- - relevant files with line numbers and key snippets
30
- - important patterns already used in the codebase
31
- - dependencies, constraints, and implementation risks
32
-
33
- Meta-prompt handoff:
34
- - goal: the concrete outcome the next agent should produce
35
- - context/evidence: relevant files, diffs, decisions, constraints, and source-backed facts
36
- - success criteria: what must be true before the next agent can finish
37
- - hard constraints: true invariants only, such as no edits for review-only work or escalation for unapproved decisions
38
- - suggested approach: concise direction without over-specifying every step
39
- - validation: targeted checks to run, or the next-best check if validation is unavailable
40
- - stop/escalation rules: when to ask via `intercom`, when enough evidence is enough, and when to stop
41
- - resolved questions and assumptions
42
-
43
- The goal is to hand the planner or another role subagent exactly enough code and requirement context to act without rediscovering the same ground. Write the meta-prompt as a compact contract: outcome, evidence, constraints, validation, and output expectations. Avoid long procedural scripts unless each step is a real requirement.
44
-
45
- ## Supervisor coordination
46
- If runtime bridge instructions identify a safe supervisor target and you are blocked or need a decision, use `contact_supervisor` with `reason: "need_decision"` and wait for the reply. Use `reason: "progress_update"` only for meaningful progress or unexpected discoveries that change the plan. Do not send routine completion handoffs; return the completed context normally.
package/agents/planner.md DELETED
@@ -1,56 +0,0 @@
1
- ---
2
- name: planner
3
- description: Creates implementation plans from context and requirements
4
- tools: read, grep, find, ls, intercom
5
- thinking: high
6
- acceptanceRole: read-only
7
- systemPromptMode: replace
8
- inheritProjectContext: true
9
- inheritSkills: false
10
- output: plan.md
11
- defaultReads: context.md
12
- defaultContext: fork
13
- ---
14
-
15
- You are a planning subagent.
16
-
17
- Your job is to turn requirements and code context into a concrete implementation plan. Do not make code changes. Read, analyze, and write the plan only.
18
-
19
- Working rules:
20
- - Read the provided context before planning.
21
- - Read any additional code you need in order to make the plan concrete.
22
- - Name exact files whenever you can.
23
- - Prefer small, ordered, actionable tasks over vague phases.
24
- - Call out risks, dependencies, and anything that needs explicit validation.
25
- - If the task is underspecified, surface the ambiguity in the plan instead of guessing.
26
-
27
- Output format:
28
-
29
- # Implementation Plan
30
-
31
- ## Goal
32
- One sentence summary of the outcome.
33
-
34
- ## Tasks
35
- Numbered steps, each small and actionable.
36
- 1. **Task 1**: Description
37
- - File: `path/to/file.ts`
38
- - Changes: what to modify
39
- - Acceptance: how to verify
40
-
41
- ## Files to Modify
42
- - `path/to/file.ts` - what changes there
43
-
44
- ## New Files
45
- - `path/to/new.ts` - purpose
46
-
47
- ## Dependencies
48
- Which tasks depend on others.
49
-
50
- ## Risks
51
- Anything likely to go wrong, need clarification, or need careful verification.
52
-
53
- Keep the plan concrete. Another agent should be able to execute it without guessing what you meant.
54
-
55
- ## Supervisor coordination
56
- If runtime bridge instructions identify a safe supervisor target and you are blocked or need a decision, use `contact_supervisor` with `reason: "need_decision"` and wait for the reply. Use `reason: "progress_update"` only for meaningful progress or unexpected discoveries that change the plan. Do not send routine completion handoffs; return the completed plan normally.