pi-subagents 0.32.0 → 0.33.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 (52) hide show
  1. package/CHANGELOG.md +30 -3
  2. package/README.md +147 -58
  3. package/install.mjs +2 -1
  4. package/package.json +3 -2
  5. package/skills/pi-subagents/SKILL.md +70 -14
  6. package/src/agents/agent-management.ts +177 -5
  7. package/src/agents/agent-memory.ts +254 -0
  8. package/src/agents/agent-serializer.ts +11 -0
  9. package/src/agents/agents.ts +142 -12
  10. package/src/agents/chain-serializer.ts +27 -2
  11. package/src/extension/doctor.ts +1 -9
  12. package/src/extension/fanout-child.ts +2 -2
  13. package/src/extension/index.ts +65 -90
  14. package/src/extension/rpc.ts +369 -0
  15. package/src/extension/schemas.ts +52 -8
  16. package/src/extension/tool-description.ts +200 -0
  17. package/src/intercom/intercom-bridge.ts +21 -253
  18. package/src/intercom/native-supervisor-channel.ts +510 -0
  19. package/src/runs/background/async-execution.ts +51 -7
  20. package/src/runs/background/async-job-tracker.ts +12 -2
  21. package/src/runs/background/async-status.ts +27 -2
  22. package/src/runs/background/completion-batcher.ts +166 -0
  23. package/src/runs/background/control-channel.ts +106 -1
  24. package/src/runs/background/fleet-view.ts +515 -0
  25. package/src/runs/background/notify.ts +161 -44
  26. package/src/runs/background/result-watcher.ts +1 -2
  27. package/src/runs/background/run-id-resolver.ts +3 -2
  28. package/src/runs/background/run-status.ts +166 -6
  29. package/src/runs/background/scheduled-runs.ts +514 -0
  30. package/src/runs/background/subagent-runner.ts +409 -35
  31. package/src/runs/background/wait.ts +353 -0
  32. package/src/runs/foreground/chain-execution.ts +95 -21
  33. package/src/runs/foreground/execution.ts +150 -21
  34. package/src/runs/foreground/subagent-executor.ts +378 -64
  35. package/src/runs/shared/dynamic-fanout.ts +1 -1
  36. package/src/runs/shared/model-fallback.ts +167 -20
  37. package/src/runs/shared/model-scope.ts +128 -0
  38. package/src/runs/shared/nested-events.ts +31 -0
  39. package/src/runs/shared/parallel-utils.ts +1 -0
  40. package/src/runs/shared/pi-args.ts +30 -1
  41. package/src/runs/shared/subagent-prompt-runtime.ts +108 -2
  42. package/src/runs/shared/tool-budget.ts +74 -0
  43. package/src/runs/shared/turn-budget.ts +52 -0
  44. package/src/shared/artifacts.ts +1 -0
  45. package/src/shared/atomic-json.ts +15 -2
  46. package/src/shared/child-transcript.ts +212 -0
  47. package/src/shared/settings.ts +3 -1
  48. package/src/shared/types.ts +134 -19
  49. package/src/slash/prompt-workflows.ts +330 -0
  50. package/src/slash/slash-commands.ts +16 -2
  51. package/src/tui/render.ts +16 -8
  52. package/src/extension/companion-suggestions.ts +0 -359
@@ -7,6 +7,7 @@ import * as path from "node:path";
7
7
  import type { Message } from "@earendil-works/pi-ai";
8
8
  import type { FSWatcher } from "node:fs";
9
9
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
10
+ import type { ModelScopeConfig } from "../runs/shared/model-scope.ts";
10
11
 
11
12
  // ============================================================================
12
13
  // Basic Types
@@ -88,6 +89,47 @@ export interface Usage {
88
89
  turns: number;
89
90
  }
90
91
 
92
+ export interface TurnBudgetConfig {
93
+ maxTurns: number;
94
+ graceTurns?: number;
95
+ }
96
+
97
+ export interface ResolvedTurnBudget {
98
+ maxTurns: number;
99
+ graceTurns: number;
100
+ }
101
+
102
+ export interface ToolBudgetConfig {
103
+ soft?: number;
104
+ hard: number;
105
+ block?: string[] | "*";
106
+ }
107
+
108
+ export interface ResolvedToolBudget {
109
+ soft?: number;
110
+ hard: number;
111
+ block: string[] | "*";
112
+ }
113
+
114
+ export type ToolBudgetOutcome = "within-budget" | "soft-reached" | "hard-blocked";
115
+
116
+ export interface ToolBudgetState extends ResolvedToolBudget {
117
+ outcome: ToolBudgetOutcome;
118
+ toolCount: number;
119
+ softReachedAt?: number;
120
+ hardReachedAt?: number;
121
+ blockedTool?: string;
122
+ }
123
+
124
+ export type TurnBudgetOutcome = "within-budget" | "wrap-up-requested" | "exceeded";
125
+
126
+ export interface TurnBudgetState extends ResolvedTurnBudget {
127
+ outcome: TurnBudgetOutcome;
128
+ turnCount: number;
129
+ wrapUpRequestedAtTurn?: number;
130
+ exceededAtTurn?: number;
131
+ }
132
+
91
133
  export interface TokenUsage {
92
134
  input: number;
93
135
  output: number;
@@ -120,6 +162,25 @@ export interface ResolvedControlConfig {
120
162
  notifyChannels: ControlNotificationChannel[];
121
163
  }
122
164
 
165
+ /**
166
+ * Smart completion batching for async-completion notifications. Successful
167
+ * sibling completions are held briefly so they arrive as one grouped message;
168
+ * failure and attention signals bypass grouping and always fire immediately.
169
+ */
170
+ export interface CompletionBatchConfig {
171
+ enabled?: boolean;
172
+ /** Idle window after each arrival; resets on every new item. */
173
+ debounceMs?: number;
174
+ /** Hard cap measured from the first item in a group. */
175
+ maxWaitMs?: number;
176
+ /** Shorter idle window for straggler groups. */
177
+ stragglerDebounceMs?: number;
178
+ /** Shorter hard cap for straggler groups. */
179
+ stragglerMaxWaitMs?: number;
180
+ /** Arrivals within this window after an emit join a straggler group. */
181
+ stragglerWindowMs?: number;
182
+ }
183
+
123
184
  export interface ControlEvent {
124
185
  type: ControlEventType;
125
186
  from?: ActivityState;
@@ -149,7 +210,7 @@ export type SubagentLifecycleArtifactVersion = typeof SUBAGENT_LIFECYCLE_ARTIFAC
149
210
 
150
211
  export type PublicNestedStepSummary = Pick<
151
212
  NestedStepSummary,
152
- "agent" | "status" | "sessionFile" | "activityState" | "lastActivityAt" | "currentTool" | "currentToolStartedAt" | "currentPath" | "turnCount" | "toolCount" | "startedAt" | "endedAt" | "error" | "timedOut"
213
+ "agent" | "status" | "sessionFile" | "transcriptPath" | "transcriptError" | "activityState" | "lastActivityAt" | "currentTool" | "currentToolStartedAt" | "currentPath" | "turnCount" | "toolCount" | "toolBudget" | "toolBudgetBlocked" | "startedAt" | "endedAt" | "error" | "timedOut"
153
214
  > & {
154
215
  children?: PublicNestedRunSummary[];
155
216
  };
@@ -162,7 +223,7 @@ export type CostSummary = {
162
223
 
163
224
  export type PublicNestedRunSummary = Pick<
164
225
  NestedRunSummary,
165
- "id" | "parentRunId" | "parentStepIndex" | "parentAgent" | "depth" | "path" | "asyncDir" | "sessionId" | "sessionFile" | "intercomTarget" | "ownerIntercomTarget" | "leafIntercomTarget" | "ownerState" | "mode" | "state" | "agent" | "agents" | "currentStep" | "chainStepCount" | "parallelGroups" | "activityState" | "lastActivityAt" | "currentTool" | "currentToolStartedAt" | "currentPath" | "turnCount" | "toolCount" | "totalTokens" | "totalCost" | "startedAt" | "endedAt" | "lastUpdate" | "error" | "timeoutMs" | "deadlineAt" | "timedOut"
226
+ "id" | "parentRunId" | "parentStepIndex" | "parentAgent" | "depth" | "path" | "asyncDir" | "sessionId" | "sessionFile" | "intercomTarget" | "ownerIntercomTarget" | "leafIntercomTarget" | "ownerState" | "mode" | "state" | "agent" | "agents" | "currentStep" | "chainStepCount" | "parallelGroups" | "activityState" | "lastActivityAt" | "currentTool" | "currentToolStartedAt" | "currentPath" | "turnCount" | "toolCount" | "toolBudget" | "toolBudgetBlocked" | "totalTokens" | "totalCost" | "startedAt" | "endedAt" | "lastUpdate" | "error" | "timeoutMs" | "deadlineAt" | "timedOut" | "turnBudget" | "turnBudgetExceeded" | "wrapUpRequested"
166
227
  > & {
167
228
  steps?: PublicNestedStepSummary[];
168
229
  children?: PublicNestedRunSummary[];
@@ -400,6 +461,11 @@ export interface SingleResult {
400
461
  detachedReason?: string;
401
462
  interrupted?: boolean;
402
463
  timedOut?: boolean;
464
+ turnBudget?: TurnBudgetState;
465
+ turnBudgetExceeded?: boolean;
466
+ wrapUpRequested?: boolean;
467
+ toolBudget?: ToolBudgetState;
468
+ toolBudgetBlocked?: boolean;
403
469
  messages?: Message[];
404
470
  usage: Usage;
405
471
  model?: string;
@@ -424,6 +490,8 @@ export interface SingleResult {
424
490
  structuredOutputPath?: string;
425
491
  structuredOutputSchemaPath?: string;
426
492
  acceptance?: AcceptanceLedger;
493
+ transcriptPath?: string;
494
+ transcriptError?: string;
427
495
  children?: NestedRunSummary[];
428
496
  }
429
497
 
@@ -438,6 +506,8 @@ export interface Details {
438
506
  timeoutMs?: number;
439
507
  deadlineAt?: number;
440
508
  timedOut?: boolean;
509
+ turnBudget?: ResolvedTurnBudget;
510
+ toolBudget?: ResolvedToolBudget;
441
511
  progress?: AgentProgress[];
442
512
  progressSummary?: ProgressSummary;
443
513
  artifacts?: {
@@ -470,6 +540,7 @@ export interface ArtifactPaths {
470
540
  inputPath: string;
471
541
  outputPath: string;
472
542
  jsonlPath: string;
543
+ transcriptPath: string;
473
544
  metadataPath: string;
474
545
  }
475
546
 
@@ -478,6 +549,7 @@ export interface ArtifactConfig {
478
549
  includeInput: boolean;
479
550
  includeOutput: boolean;
480
551
  includeJsonl: boolean;
552
+ includeTranscript?: boolean;
481
553
  includeMetadata: boolean;
482
554
  cleanupDays: number;
483
555
  }
@@ -508,6 +580,8 @@ export interface NestedStepSummary {
508
580
  agent: string;
509
581
  status: "pending" | "running" | "complete" | "completed" | "failed" | "paused";
510
582
  sessionFile?: string;
583
+ transcriptPath?: string;
584
+ transcriptError?: string;
511
585
  activityState?: ActivityState;
512
586
  lastActivityAt?: number;
513
587
  currentTool?: string;
@@ -519,6 +593,11 @@ export interface NestedStepSummary {
519
593
  endedAt?: number;
520
594
  error?: string;
521
595
  timedOut?: boolean;
596
+ turnBudget?: TurnBudgetState;
597
+ turnBudgetExceeded?: boolean;
598
+ wrapUpRequested?: boolean;
599
+ toolBudget?: ToolBudgetState;
600
+ toolBudgetBlocked?: boolean;
522
601
  children?: NestedRunSummary[];
523
602
  }
524
603
 
@@ -557,6 +636,11 @@ export interface NestedRunSummary extends NestedRunAddress {
557
636
  timeoutMs?: number;
558
637
  deadlineAt?: number;
559
638
  timedOut?: boolean;
639
+ turnBudget?: TurnBudgetState;
640
+ turnBudgetExceeded?: boolean;
641
+ wrapUpRequested?: boolean;
642
+ toolBudget?: ToolBudgetState;
643
+ toolBudgetBlocked?: boolean;
560
644
  error?: string;
561
645
  }
562
646
 
@@ -582,6 +666,7 @@ export interface AsyncStartedEvent {
582
666
  workflowGraph?: WorkflowGraphSnapshot;
583
667
  timeoutMs?: number;
584
668
  deadlineAt?: number;
669
+ turnBudget?: TurnBudgetState;
585
670
  nestedRoute?: NestedRouteInfo;
586
671
  }
587
672
 
@@ -599,12 +684,19 @@ export interface AsyncStatus {
599
684
  currentPath?: string;
600
685
  turnCount?: number;
601
686
  toolCount?: number;
687
+ steerCount?: number;
688
+ lastSteerAt?: number;
602
689
  startedAt: number;
603
690
  endedAt?: number;
604
691
  lastUpdate?: number;
605
692
  timeoutMs?: number;
606
693
  deadlineAt?: number;
607
694
  timedOut?: boolean;
695
+ turnBudget?: TurnBudgetState;
696
+ turnBudgetExceeded?: boolean;
697
+ wrapUpRequested?: boolean;
698
+ toolBudget?: ToolBudgetState;
699
+ toolBudgetBlocked?: boolean;
608
700
  pid?: number;
609
701
  cwd?: string;
610
702
  currentStep?: number;
@@ -621,6 +713,8 @@ export interface AsyncStatus {
621
713
  status: "pending" | "running" | "complete" | "completed" | "failed" | "paused";
622
714
  children?: NestedRunSummary[];
623
715
  sessionFile?: string;
716
+ transcriptPath?: string;
717
+ transcriptError?: string;
624
718
  activityState?: ActivityState;
625
719
  lastActivityAt?: number;
626
720
  currentTool?: string;
@@ -636,6 +730,11 @@ export interface AsyncStatus {
636
730
  durationMs?: number;
637
731
  exitCode?: number | null;
638
732
  timedOut?: boolean;
733
+ turnBudget?: TurnBudgetState;
734
+ turnBudgetExceeded?: boolean;
735
+ wrapUpRequested?: boolean;
736
+ toolBudget?: ToolBudgetState;
737
+ toolBudgetBlocked?: boolean;
639
738
  tokens?: TokenUsage;
640
739
  skills?: string[];
641
740
  model?: string;
@@ -643,6 +742,8 @@ export interface AsyncStatus {
643
742
  attemptedModels?: string[];
644
743
  modelAttempts?: ModelAttempt[];
645
744
  totalCost?: CostSummary;
745
+ steerCount?: number;
746
+ lastSteerAt?: number;
646
747
  error?: string;
647
748
  structuredOutput?: unknown;
648
749
  structuredOutputPath?: string;
@@ -674,6 +775,8 @@ export interface AsyncJobState {
674
775
  currentPath?: string;
675
776
  turnCount?: number;
676
777
  toolCount?: number;
778
+ steerCount?: number;
779
+ lastSteerAt?: number;
677
780
  mode?: SubagentRunMode;
678
781
  agents?: string[];
679
782
  currentStep?: number;
@@ -690,6 +793,11 @@ export interface AsyncJobState {
690
793
  timeoutMs?: number;
691
794
  deadlineAt?: number;
692
795
  timedOut?: boolean;
796
+ turnBudget?: TurnBudgetState;
797
+ turnBudgetExceeded?: boolean;
798
+ wrapUpRequested?: boolean;
799
+ toolBudget?: ToolBudgetState;
800
+ toolBudgetBlocked?: boolean;
693
801
  sessionDir?: string;
694
802
  outputFile?: string;
695
803
  totalTokens?: TokenUsage;
@@ -704,6 +812,13 @@ export interface ForegroundResumeChild {
704
812
  index: number;
705
813
  sessionFile?: string;
706
814
  status: SubagentResultStatus;
815
+ exitCode?: number;
816
+ finalOutput?: string;
817
+ artifactPaths?: ArtifactPaths;
818
+ transcriptPath?: string;
819
+ transcriptError?: string;
820
+ detachedReason?: string;
821
+ updatedAt?: number;
707
822
  }
708
823
 
709
824
  export interface ForegroundResumeRun {
@@ -748,8 +863,6 @@ export interface SubagentState {
748
863
  completionSeen: Map<string, number>;
749
864
  watcher: FSWatcher | null;
750
865
  watcherRestartTimer: ReturnType<typeof setTimeout> | null;
751
- companionSuggestionStartupShown?: boolean;
752
- companionSuggestionListShown?: boolean;
753
866
  resultFileCoalescer: {
754
867
  schedule(file: string, delayMs?: number): boolean;
755
868
  clear(): void;
@@ -801,10 +914,13 @@ export interface RunSyncOptions {
801
914
  interruptSignal?: AbortSignal;
802
915
  timeoutMs?: number;
803
916
  deadlineAt?: number;
917
+ turnBudget?: ResolvedTurnBudget;
918
+ toolBudget?: ResolvedToolBudget;
804
919
  allowIntercomDetach?: boolean;
805
920
  intercomEvents?: IntercomEventBus;
806
921
  onUpdate?: (r: import("@earendil-works/pi-agent-core").AgentToolResult<Details>) => void;
807
922
  onControlEvent?: (event: ControlEvent) => void;
923
+ onDetachedExit?: (result: SingleResult) => void;
808
924
  controlConfig?: ResolvedControlConfig;
809
925
  intercomSessionName?: string;
810
926
  orchestratorIntercomTarget?: string;
@@ -828,6 +944,8 @@ export interface RunSyncOptions {
828
944
  availableModels?: Array<{ provider: string; id: string; fullId: string }>;
829
945
  /** Current parent-session provider to prefer for ambiguous bare model ids */
830
946
  preferredModelProvider?: string;
947
+ /** Optional subagent model-scope enforcement for fallback candidates */
948
+ modelScope?: ModelScopeConfig;
831
949
  /** Skills to make available (overrides agent default if provided) */
832
950
  skills?: string[];
833
951
  structuredOutput?: {
@@ -869,25 +987,18 @@ export interface ProactiveSkillSubagentsConfig {
869
987
  preferredAgent?: string;
870
988
  }
871
989
 
872
- export type CompanionSuggestionPackage = "pi-prompt-template-model" | "pi-intercom";
873
- export type CompanionSuggestionSurface = "session_start" | "list" | "doctor";
874
-
875
- export interface CompanionSuggestionPackageConfig {
876
- enabled?: boolean;
877
- surfaces?: CompanionSuggestionSurface[];
878
- dismissed?: {
879
- user?: boolean;
880
- workspaces?: string[];
881
- };
882
- }
990
+ export type ToolDescriptionMode = "full" | "compact" | "custom";
883
991
 
884
- export interface CompanionSuggestionsConfig {
992
+ export interface ScheduledRunsConfig {
885
993
  enabled?: boolean;
886
- packages?: Partial<Record<CompanionSuggestionPackage, CompanionSuggestionPackageConfig>>;
994
+ maxLatenessMs?: number;
995
+ maxPending?: number;
887
996
  }
888
997
 
889
998
  export interface ExtensionConfig {
890
999
  asyncByDefault?: boolean;
1000
+ /** Tool description variant registered for the parent-facing subagent tool. Defaults to full. */
1001
+ toolDescriptionMode?: ToolDescriptionMode;
891
1002
  forceTopLevelAsync?: boolean;
892
1003
  defaultSessionDir?: string;
893
1004
  singleRunOutputBaseDir?: string;
@@ -896,6 +1007,9 @@ export interface ExtensionConfig {
896
1007
  /** Global cap on simultaneously-running subagent tasks within a single run. Defaults to 20. */
897
1008
  globalConcurrencyLimit?: number;
898
1009
  control?: ControlConfig;
1010
+ completionBatch?: CompletionBatchConfig;
1011
+ turnBudget?: TurnBudgetConfig;
1012
+ toolBudget?: ToolBudgetConfig;
899
1013
  parallel?: TopLevelParallelConfig;
900
1014
  chain?: ExtensionChainConfig;
901
1015
  worktreeSetupHook?: string;
@@ -903,7 +1017,7 @@ export interface ExtensionConfig {
903
1017
  worktreeBaseDir?: string;
904
1018
  intercomBridge?: IntercomBridgeConfig;
905
1019
  proactiveSkillSubagents?: ProactiveSkillSubagentsConfig | false;
906
- companionSuggestions?: CompanionSuggestionsConfig | false;
1020
+ scheduledRuns?: ScheduledRunsConfig;
907
1021
  }
908
1022
 
909
1023
  // ============================================================================
@@ -920,6 +1034,7 @@ export const DEFAULT_ARTIFACT_CONFIG: ArtifactConfig = {
920
1034
  includeInput: true,
921
1035
  includeOutput: true,
922
1036
  includeJsonl: false,
1037
+ includeTranscript: true,
923
1038
  includeMetadata: true,
924
1039
  cleanupDays: 7,
925
1040
  };
@@ -996,7 +1111,7 @@ export const POLL_INTERVAL_MS = 250;
996
1111
  export const MAX_WIDGET_JOBS = 4;
997
1112
  export const DEFAULT_SUBAGENT_MAX_DEPTH = 2;
998
1113
  export const DEFAULT_MAX_SUBAGENT_SPAWNS_PER_SESSION = 40;
999
- export const SUBAGENT_ACTIONS = ["list", "get", "models", "create", "update", "delete", "status", "interrupt", "resume", "append-step", "doctor"] as const;
1114
+ export const SUBAGENT_ACTIONS = ["list", "get", "models", "create", "update", "delete", "eject", "disable", "enable", "reset", "status", "interrupt", "resume", "steer", "append-step", "doctor", "schedule", "schedule-list", "schedule-status", "schedule-cancel"] as const;
1000
1115
 
1001
1116
  export const DEFAULT_FORK_PREAMBLE =
1002
1117
  "You are a delegated subagent running from a fork of the parent session. " +
@@ -0,0 +1,330 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
5
+ import { parseFrontmatter } from "../agents/frontmatter.ts";
6
+ import type { SubagentParamsLike } from "../runs/foreground/subagent-executor.ts";
7
+ import type { ChainStep } from "../shared/settings.ts";
8
+ import { getAgentDir, getProjectConfigDir } from "../shared/utils.ts";
9
+
10
+ interface PromptWorkflow {
11
+ name: string;
12
+ description: string;
13
+ body: string;
14
+ filePath: string;
15
+ agent: string;
16
+ context?: "fresh" | "fork";
17
+ model?: string;
18
+ skill?: string | string[] | false;
19
+ cwd?: string;
20
+ worktree?: boolean;
21
+ chain?: string;
22
+ }
23
+
24
+ type PromptWorkflowRunner = (params: SubagentParamsLike, ctx: ExtensionContext) => Promise<void>;
25
+
26
+ const RESERVED_COMMAND_NAMES = new Set([
27
+ "chain-prompts",
28
+ "prompt-workflow",
29
+ "run",
30
+ "chain",
31
+ "parallel",
32
+ "run-chain",
33
+ "subagents-doctor",
34
+ "subagents-models",
35
+ ]);
36
+
37
+ function packagePromptsDir(): string {
38
+ return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "prompts");
39
+ }
40
+
41
+ function promptDirs(cwd: string): string[] {
42
+ return [
43
+ packagePromptsDir(),
44
+ path.join(getAgentDir(), "prompts"),
45
+ path.join(getProjectConfigDir(cwd), "prompts"),
46
+ ];
47
+ }
48
+
49
+ function readPromptFiles(cwd: string): string[] {
50
+ const files: string[] = [];
51
+ for (const dir of promptDirs(cwd)) {
52
+ let entries: fs.Dirent[];
53
+ try {
54
+ entries = fs.readdirSync(dir, { withFileTypes: true });
55
+ } catch {
56
+ continue;
57
+ }
58
+ for (const entry of entries) {
59
+ if (entry.isFile() && entry.name.endsWith(".md")) files.push(path.join(dir, entry.name));
60
+ }
61
+ }
62
+ return files;
63
+ }
64
+
65
+ function firstNonEmptyLine(value: string): string {
66
+ return value.split(/\r?\n/).map((line) => line.trim()).find(Boolean) ?? "Prompt workflow";
67
+ }
68
+
69
+ function stringField(frontmatter: Record<string, string>, key: string): string | undefined {
70
+ const value = frontmatter[key]?.trim();
71
+ return value ? value : undefined;
72
+ }
73
+
74
+ function booleanField(frontmatter: Record<string, string>, key: string): boolean | undefined {
75
+ const value = frontmatter[key]?.trim().toLowerCase();
76
+ if (value === "true" || value === "yes" || value === "1") return true;
77
+ if (value === "false" || value === "no" || value === "0") return false;
78
+ return undefined;
79
+ }
80
+
81
+ function parseSkill(value: string | undefined): string | string[] | false | undefined {
82
+ if (!value) return undefined;
83
+ if (value === "false") return false;
84
+ const parts = value.split(",").map((part) => part.trim()).filter(Boolean);
85
+ return parts.length > 1 ? parts : parts[0];
86
+ }
87
+
88
+ function parseAgent(frontmatter: Record<string, string>): string {
89
+ const subagent = stringField(frontmatter, "subagent");
90
+ if (!subagent || subagent === "true") return "delegate";
91
+ return subagent;
92
+ }
93
+
94
+ function loadPromptWorkflow(filePath: string): PromptWorkflow | undefined {
95
+ const content = fs.readFileSync(filePath, "utf-8");
96
+ const { frontmatter, body } = parseFrontmatter(content);
97
+ const name = path.basename(filePath, ".md");
98
+ if (!name || RESERVED_COMMAND_NAMES.has(name)) return undefined;
99
+ const model = stringField(frontmatter, "model");
100
+ const skill = parseSkill(stringField(frontmatter, "skill"));
101
+ const cwd = stringField(frontmatter, "cwd");
102
+ const chain = stringField(frontmatter, "chain");
103
+ return {
104
+ name,
105
+ description: stringField(frontmatter, "description") ?? firstNonEmptyLine(body),
106
+ body,
107
+ filePath,
108
+ agent: parseAgent(frontmatter),
109
+ ...(booleanField(frontmatter, "inheritContext") === true || booleanField(frontmatter, "fork") === true ? { context: "fork" as const } : {}),
110
+ ...(booleanField(frontmatter, "fresh") === true ? { context: "fresh" as const } : {}),
111
+ ...(model ? { model } : {}),
112
+ ...(skill !== undefined ? { skill } : {}),
113
+ ...(cwd ? { cwd } : {}),
114
+ ...(booleanField(frontmatter, "worktree") === true ? { worktree: true } : {}),
115
+ ...(chain ? { chain } : {}),
116
+ };
117
+ }
118
+
119
+ export function discoverPromptWorkflows(cwd: string): PromptWorkflow[] {
120
+ const workflows = new Map<string, PromptWorkflow>();
121
+ for (const file of readPromptFiles(cwd)) {
122
+ const workflow = loadPromptWorkflow(file);
123
+ if (workflow) workflows.set(workflow.name, workflow);
124
+ }
125
+ return [...workflows.values()].sort((a, b) => a.name.localeCompare(b.name));
126
+ }
127
+
128
+ function shellWords(input: string): string[] {
129
+ const words: string[] = [];
130
+ let current = "";
131
+ let quote: "'" | '"' | undefined;
132
+ let escaped = false;
133
+ for (const ch of input) {
134
+ if (escaped) {
135
+ current += ch;
136
+ escaped = false;
137
+ continue;
138
+ }
139
+ if (ch === "\\") {
140
+ escaped = true;
141
+ continue;
142
+ }
143
+ if (quote) {
144
+ if (ch === quote) quote = undefined;
145
+ else current += ch;
146
+ continue;
147
+ }
148
+ if (ch === "'" || ch === '"') {
149
+ quote = ch;
150
+ continue;
151
+ }
152
+ if (/\s/.test(ch)) {
153
+ if (current) {
154
+ words.push(current);
155
+ current = "";
156
+ }
157
+ continue;
158
+ }
159
+ current += ch;
160
+ }
161
+ if (current) words.push(current);
162
+ return words;
163
+ }
164
+
165
+ function substituteArgs(template: string, args: string[]): string {
166
+ const all = args.join(" ");
167
+ return template
168
+ .replace(/\$ARGUMENTS/g, all)
169
+ .replace(/\$@/g, all)
170
+ .replace(/\$\{(\d+):-([^}]*)\}/g, (_match, index: string, fallback: string) => args[Number(index) - 1] || fallback)
171
+ .replace(/\$(\d+)/g, (_match, index: string) => args[Number(index) - 1] ?? "");
172
+ }
173
+
174
+ function parseRuntimeOptions(words: string[]): { args: string[]; agentOverride?: string; fork?: boolean; fresh?: boolean; worktree?: boolean; bg?: boolean } {
175
+ const args: string[] = [];
176
+ let agentOverride: string | undefined;
177
+ let fork = false;
178
+ let fresh = false;
179
+ let worktree = false;
180
+ let bg = false;
181
+ for (let i = 0; i < words.length; i++) {
182
+ const word = words[i]!;
183
+ if (word === "--fork") {
184
+ fork = true;
185
+ continue;
186
+ }
187
+ if (word === "--fresh") {
188
+ fresh = true;
189
+ continue;
190
+ }
191
+ if (word === "--worktree") {
192
+ worktree = true;
193
+ continue;
194
+ }
195
+ if (word === "--bg" || word === "--async") {
196
+ bg = true;
197
+ continue;
198
+ }
199
+ if (word === "--subagent") {
200
+ agentOverride = words[++i];
201
+ continue;
202
+ }
203
+ const eq = word.match(/^--subagent(?:=|:)(.+)$/);
204
+ if (eq) {
205
+ agentOverride = eq[1];
206
+ continue;
207
+ }
208
+ args.push(word);
209
+ }
210
+ return { args, agentOverride, fork, fresh, worktree, bg };
211
+ }
212
+
213
+ function splitChainDeclaration(input: string): { declaration: string; argsText: string } {
214
+ const delimiter = input.indexOf(" -- ");
215
+ if (delimiter === -1) return { declaration: input.trim(), argsText: "" };
216
+ return { declaration: input.slice(0, delimiter).trim(), argsText: input.slice(delimiter + 4).trim() };
217
+ }
218
+
219
+ function splitPromptChain(input: string): string[] {
220
+ return input.split(" -> ").map((part) => part.trim()).filter(Boolean);
221
+ }
222
+
223
+ function workflowParams(workflow: PromptWorkflow, args: string[], runtime: ReturnType<typeof parseRuntimeOptions>): SubagentParamsLike {
224
+ const task = substituteArgs(workflow.body, args).trim();
225
+ const context = runtime.fork ? "fork" : runtime.fresh ? "fresh" : workflow.context;
226
+ return {
227
+ agent: runtime.agentOverride ?? workflow.agent,
228
+ task,
229
+ clarify: false,
230
+ agentScope: "both",
231
+ ...(context ? { context } : {}),
232
+ ...(workflow.model ? { model: workflow.model } : {}),
233
+ ...(workflow.skill !== undefined ? { skill: workflow.skill } : {}),
234
+ ...(workflow.cwd ? { cwd: workflow.cwd } : {}),
235
+ ...(runtime.worktree || workflow.worktree ? { worktree: true } : {}),
236
+ ...(runtime.bg ? { async: true } : {}),
237
+ };
238
+ }
239
+
240
+ function workflowChainStep(workflow: PromptWorkflow, args: string[], runtime: ReturnType<typeof parseRuntimeOptions>): ChainStep {
241
+ const params = workflowParams(workflow, args, runtime);
242
+ return {
243
+ agent: params.agent ?? "delegate",
244
+ task: params.task,
245
+ ...(params.model ? { model: params.model } : {}),
246
+ ...(params.skill !== undefined ? { skill: params.skill } : {}),
247
+ ...(params.cwd ? { cwd: params.cwd } : {}),
248
+ };
249
+ }
250
+
251
+ function findWorkflow(workflows: PromptWorkflow[], name: string): PromptWorkflow | undefined {
252
+ return workflows.find((workflow) => workflow.name === name);
253
+ }
254
+
255
+ function formatWorkflowList(workflows: PromptWorkflow[]): string {
256
+ if (workflows.length === 0) return "No prompt workflows found in package, user, or project prompts.";
257
+ return [
258
+ "Prompt workflows:",
259
+ ...workflows.map((workflow) => `- ${workflow.name}: ${workflow.description} (${workflow.filePath})`),
260
+ ].join("\n");
261
+ }
262
+
263
+ export function registerPromptWorkflowCommands(input: {
264
+ pi: ExtensionAPI;
265
+ run: PromptWorkflowRunner;
266
+ }): void {
267
+ const { pi, run } = input;
268
+
269
+ pi.registerCommand("prompt-workflow", {
270
+ description: "Run a prompt template through native pi-subagents: /prompt-workflow <name> [args]",
271
+ handler: async (rawArgs, ctx) => {
272
+ const words = shellWords(rawArgs);
273
+ const name = words.shift();
274
+ const workflows = discoverPromptWorkflows(ctx.cwd);
275
+ if (!name || name === "list") {
276
+ pi.sendMessage({ content: formatWorkflowList(workflows), display: true });
277
+ return;
278
+ }
279
+ const workflow = findWorkflow(workflows, name);
280
+ if (!workflow) {
281
+ ctx.ui.notify(`Unknown prompt workflow: ${name}`, "error");
282
+ return;
283
+ }
284
+ const runtime = parseRuntimeOptions(words);
285
+ try {
286
+ if (workflow.chain) {
287
+ const chainNames = splitPromptChain(workflow.chain);
288
+ const chain = chainNames.map((stepName) => {
289
+ const step = findWorkflow(workflows, stepName);
290
+ if (!step) throw new Error(`Unknown prompt workflow in chain '${workflow.name}': ${stepName}`);
291
+ return workflowChainStep(step, runtime.args, runtime);
292
+ });
293
+ await run({ chain, task: runtime.args.join(" "), clarify: false, agentScope: "both", ...(runtime.bg ? { async: true } : {}) }, ctx);
294
+ return;
295
+ }
296
+ await run(workflowParams(workflow, runtime.args, runtime), ctx);
297
+ } catch (error) {
298
+ ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
299
+ }
300
+ },
301
+ });
302
+
303
+ pi.registerCommand("chain-prompts", {
304
+ description: "Run prompt templates as a native subagent chain: /chain-prompts analyze -> fix -- args",
305
+ handler: async (rawArgs, ctx) => {
306
+ const { declaration, argsText } = splitChainDeclaration(rawArgs);
307
+ const workflows = discoverPromptWorkflows(ctx.cwd);
308
+ if (!declaration || declaration === "list") {
309
+ pi.sendMessage({ content: formatWorkflowList(workflows), display: true });
310
+ return;
311
+ }
312
+ const runtime = parseRuntimeOptions(shellWords(argsText));
313
+ const names = splitPromptChain(declaration);
314
+ if (names.length === 0) {
315
+ ctx.ui.notify("Usage: /chain-prompts prompt-a -> prompt-b -- args", "error");
316
+ return;
317
+ }
318
+ try {
319
+ const chain = names.map((name) => {
320
+ const workflow = findWorkflow(workflows, name);
321
+ if (!workflow) throw new Error(`Unknown prompt workflow: ${name}`);
322
+ return workflowChainStep(workflow, runtime.args, runtime);
323
+ });
324
+ await run({ chain, task: runtime.args.join(" "), clarify: false, agentScope: "both", ...(runtime.bg ? { async: true } : {}) }, ctx);
325
+ } catch (error) {
326
+ ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
327
+ }
328
+ },
329
+ });
330
+ }