pi-subagents 0.42.0 → 0.43.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 (55) hide show
  1. package/CHANGELOG.md +35 -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 +27 -43
  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/control-notices.ts +2 -2
  14. package/src/extension/fanout-child.ts +46 -22
  15. package/src/extension/index.ts +29 -12
  16. package/src/extension/public-execution.ts +71 -0
  17. package/src/extension/rpc.ts +7 -8
  18. package/src/extension/schemas.ts +17 -17
  19. package/src/extension/tool-description.ts +14 -14
  20. package/src/missions/actions.ts +47 -11
  21. package/src/missions/goal-driver.ts +162 -0
  22. package/src/missions/lifecycle.ts +44 -12
  23. package/src/missions/store.ts +68 -3
  24. package/src/missions/types.ts +25 -3
  25. package/src/missions/workflow-state.ts +77 -0
  26. package/src/profiles/profiles.ts +1 -3
  27. package/src/runs/background/async-execution.ts +3 -0
  28. package/src/runs/background/async-job-tracker.ts +2 -17
  29. package/src/runs/background/control-channel.ts +50 -6
  30. package/src/runs/background/retained-children.ts +68 -0
  31. package/src/runs/background/scheduled-runs.ts +17 -13
  32. package/src/runs/background/steering.ts +7 -5
  33. package/src/runs/background/subagent-runner.ts +10 -4
  34. package/src/runs/foreground/async-steering-action.ts +36 -10
  35. package/src/runs/foreground/chain-clarify.ts +13 -16
  36. package/src/runs/foreground/execution.ts +4 -0
  37. package/src/runs/foreground/subagent-executor.ts +200 -59
  38. package/src/runs/shared/acceptance.ts +154 -9
  39. package/src/runs/shared/subagent-prompt-runtime.ts +94 -25
  40. package/src/shared/types.ts +17 -3
  41. package/src/slash/delegation-adapters.ts +0 -13
  42. package/src/slash/prompt-template-bridge.ts +14 -12
  43. package/src/slash/prompt-workflows.ts +10 -4
  44. package/src/slash/slash-bridge.ts +8 -6
  45. package/src/slash/slash-commands.ts +27 -12
  46. package/src/slash/slash-live-state.ts +4 -2
  47. package/src/tui/fleet-status.ts +8 -4
  48. package/src/tui/fleet.ts +15 -5
  49. package/src/tui/render.ts +13 -31
  50. package/src/workflows/chat-progress.ts +2 -2
  51. package/src/workflows/scripted-workflow.ts +97 -10
  52. package/agents/context-builder.md +0 -46
  53. package/agents/planner.md +0 -56
  54. package/prompts/parallel-context-build.md +0 -55
  55. package/prompts/parallel-handoff-plan.md +0 -61
@@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto";
2
2
  import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
4
  import { keyText, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
5
- import { Key, matchesKey, type Component, type TUI } from "@earendil-works/pi-tui";
5
+ import { Key, matchesKey, truncateToWidth, type Component, type TUI } from "@earendil-works/pi-tui";
6
6
  import { BUILTIN_AGENT_NAMES, discoverAgents } from "../agents/agents.ts";
7
7
  import {
8
8
  DEFAULT_PROVIDER_MODELS_MAX_AGE_DAYS,
@@ -281,7 +281,7 @@ class SubagentsStopSelector implements Component {
281
281
  invalidate(): void {}
282
282
 
283
283
  render(width: number): string[] {
284
- const contentWidth = Math.max(40, Math.min(this.width, width || this.width));
284
+ const contentWidth = Math.max(0, Math.min(this.width, Math.floor(width)));
285
285
  const lines = [this.theme.bold("Stop subagent run"), this.theme.fg("dim", "Select a current-session async run to stop, or a scheduled run to cancel."), ""];
286
286
  const maxRows = 10;
287
287
  const start = Math.max(0, Math.min(this.selected - maxRows + 1, Math.max(0, this.targets.length - maxRows)));
@@ -305,7 +305,7 @@ class SubagentsStopSelector implements Component {
305
305
  } else {
306
306
  lines.push(this.theme.fg("dim", "↑↓/jk select · Enter confirm · Esc cancel"));
307
307
  }
308
- return lines;
308
+ return lines.map((line) => truncateToWidth(line, contentWidth));
309
309
  }
310
310
  }
311
311
 
@@ -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(
@@ -1291,7 +1280,8 @@ function collapsedWidgetLineBudget(rows: number): number {
1291
1280
  }
1292
1281
 
1293
1282
  function paddedWidgetLine(line: string, width: number): string {
1294
- const text = ` ${line} `;
1283
+ if (width <= 2) return " ".repeat(Math.max(0, width));
1284
+ const text = ` ${truncLine(line, width - 2)} `;
1295
1285
  return `${text}${" ".repeat(Math.max(0, width - visibleWidth(text)))}`;
1296
1286
  }
1297
1287
 
@@ -1346,26 +1336,19 @@ function fitAdaptiveWidgetLines(jobs: AsyncJobState[], lines: string[], theme: T
1346
1336
  return rendered.lines;
1347
1337
  }
1348
1338
 
1349
- function buildWidgetComponent(jobs: AsyncJobState[], expanded: boolean): (tui: { requestRender(): void }, theme: Theme) => Component & { dispose?(): void } {
1350
- return (tui, theme) => {
1351
- const requestRender = () => tui.requestRender();
1352
- widgetRequestRender = requestRender;
1339
+ function buildWidgetComponent(jobs: AsyncJobState[], expanded: boolean): (_tui: unknown, theme: Theme) => Component {
1340
+ return (_tui, theme) => {
1353
1341
  const container = new Container();
1354
1342
  container.render = (renderWidth: number): string[] => {
1355
- const width = getTermWidth();
1356
- const frame = widgetAnimationFrame();
1343
+ const width = Math.max(0, renderWidth - 2);
1357
1344
  const lines = expanded
1358
- ? buildWidgetLines(jobs, theme, width, true, frame)
1345
+ ? buildWidgetLines(jobs, theme, width, true)
1359
1346
  : jobs.length === 1
1360
- ? compactSingleWidgetLines(jobs[0]!, theme, width, frame)
1361
- : buildWidgetLines(jobs, theme, width, false, frame);
1362
- return fitAdaptiveWidgetLines(jobs, lines, theme, width, expanded, frame).map((line) => paddedWidgetLine(line, renderWidth));
1363
- };
1364
- const component = container as Component & { dispose?(): void };
1365
- component.dispose = () => {
1366
- 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));
1367
1350
  };
1368
- return component;
1351
+ return container;
1369
1352
  };
1370
1353
  }
1371
1354
 
@@ -1445,7 +1428,6 @@ export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = ge
1445
1428
  export function renderWidget(ctx: ExtensionContext, jobs: AsyncJobState[]): void {
1446
1429
  if (jobs.length === 0) {
1447
1430
  resetWidgetLayoutSession();
1448
- widgetRequestRender = undefined;
1449
1431
  if (ctx.hasUI) ctx.ui.setWidget(WIDGET_KEY, undefined);
1450
1432
  return;
1451
1433
  }
@@ -3,7 +3,7 @@ import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
4
  import type { Details } from "../shared/types.ts";
5
5
 
6
- export const WORKFLOW_CHAT_PROGRESS_MODES = ["auto", "off", "terminal", "milestones", "live-card"] as const;
6
+ export const WORKFLOW_CHAT_PROGRESS_MODES = ["auto", "off", "live-card"] as const;
7
7
  export type WorkflowChatProgressMode = typeof WORKFLOW_CHAT_PROGRESS_MODES[number];
8
8
  export type ResolvedWorkflowChatProgressMode = Exclude<WorkflowChatProgressMode, "auto">;
9
9
 
@@ -86,7 +86,7 @@ export function resolveWorkflowChatProgress(input: ResolveWorkflowChatProgressIn
86
86
 
87
87
  const requestedMode = requested.mode ?? "auto";
88
88
  let mode: ResolvedWorkflowChatProgressMode;
89
- if (requestedMode === "auto") mode = sameRepo ? (input.background ? "milestones" : "live-card") : "terminal";
89
+ if (requestedMode === "auto") mode = sameRepo && !input.background ? "live-card" : "off";
90
90
  else mode = requestedMode;
91
91
 
92
92
  if (mode === "live-card" && !sameRepo) return { error: "chatProgress: 'live-card' is only available for workflowScript runs in the same Git repository." };
@@ -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" });
@@ -175,10 +196,14 @@ export class WorkflowScriptError extends Error {
175
196
 
176
197
  export interface RunWorkflowScriptOptions {
177
198
  script: string;
178
- timeoutMs: number;
199
+ timeoutMs?: number;
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
  }
@@ -261,7 +310,7 @@ function workflowStringMetadata(params: Record<string, unknown>): Pick<WorkflowS
261
310
 
262
311
  export async function runWorkflowScript(options: RunWorkflowScriptOptions): Promise<WorkflowScriptResult> {
263
312
  if (!options.script.trim()) throw new Error("workflowScript must not be empty.");
264
- if (!Number.isInteger(options.timeoutMs) || options.timeoutMs < 1) throw new Error("workflow script timeout must be a positive integer.");
313
+ if (options.timeoutMs !== undefined && (!Number.isInteger(options.timeoutMs) || options.timeoutMs < 1)) throw new Error("workflow script timeout must be a positive integer.");
265
314
 
266
315
  const worker = new Worker(WORKER_SOURCE, { eval: true });
267
316
  const emits: unknown[] = [];
@@ -283,7 +332,7 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
283
332
  const finish = (outcome: { value: unknown } | { error: Error }) => {
284
333
  if (settled) return;
285
334
  settled = true;
286
- clearTimeout(timer);
335
+ if (timer) clearTimeout(timer);
287
336
  options.signal?.removeEventListener("abort", onAbort);
288
337
  void worker.terminate();
289
338
  childController.abort("error" in outcome ? outcome.error : new Error("Workflow script completed; unawaited child launches are aborted."));
@@ -291,7 +340,9 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
291
340
  else resolve({ value: outcome.value, ...partial() });
292
341
  };
293
342
  const onAbort = () => finish({ error: new Error("Workflow script aborted.") });
294
- const timer = setTimeout(() => finish({ error: new Error(`Workflow script timed out after ${options.timeoutMs}ms.`) }), options.timeoutMs);
343
+ const timer = options.timeoutMs === undefined
344
+ ? undefined
345
+ : setTimeout(() => finish({ error: new Error(`Workflow script timed out after ${options.timeoutMs}ms.`) }), options.timeoutMs);
295
346
  options.signal?.addEventListener("abort", onAbort, { once: true });
296
347
  if (options.signal?.aborted) return onAbort();
297
348
 
@@ -332,13 +383,31 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
332
383
  if (message.type === "error") return finish({ error: new Error(typeof message.error === "string" ? message.error : "Workflow script failed.") });
333
384
  if (message.type !== "call" || typeof message.callId !== "number" || typeof message.method !== "string" || !isRecord(message.args)) return;
334
385
 
335
- const respond = (promise: Promise<WorkflowScriptChildResult>) => {
386
+ const respond = (promise: Promise<unknown>) => {
336
387
  void promise.then(
337
388
  (value) => worker.postMessage({ type: "response", callId: message.callId, ok: true, value: omitUndefinedWorkflowValues(value) }),
338
389
  (error: unknown) => worker.postMessage({ type: "response", callId: message.callId, ok: false, error: error instanceof Error ? error.message : String(error) }),
339
390
  );
340
391
  };
341
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
+
342
411
  if (message.method === "status") {
343
412
  const keyOrRunId = message.args.keyOrRunId;
344
413
  if (typeof keyOrRunId !== "string" || !keyOrRunId.trim()) return respond(Promise.reject(new Error("runs.status(keyOrRunId) requires a non-empty string.")));
@@ -366,12 +435,30 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
366
435
  if (!isRecord(params)) return respond(Promise.reject(new Error(`runs.run('${key}', params) requires a params object.`)));
367
436
  if (params.action !== undefined) return respond(Promise.reject(new Error(`runs.run('${key}') accepts execution params only; management action is not allowed.`)));
368
437
  if (params.workflowScript !== undefined) return respond(Promise.reject(new Error(`runs.run('${key}') cannot start a nested workflow script.`)));
369
- 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) {
370
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.`)));
371
440
  }
372
441
  if (params.worktree !== undefined && typeof params.worktree !== "boolean") {
373
442
  return respond(Promise.reject(new Error(`runs.run('${key}') worktree must be true or false.`)));
374
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
+ }
375
462
  const collectFailure = message.args.collectFailure === true;
376
463
  const deliver = (promise: Promise<WorkflowScriptChildResult>) => collectFailure
377
464
  ? promise
@@ -410,6 +497,6 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
410
497
  respond(deliver(promise));
411
498
  });
412
499
 
413
- worker.postMessage({ type: "start", script: options.script });
500
+ worker.postMessage({ type: "start", script: options.script, stateEnabled: options.state !== undefined });
414
501
  });
415
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.