@selesai/code 0.8.4 → 0.8.6

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 (147) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +7 -1
  3. package/dist/core/slash-commands.js +1 -0
  4. package/dist/defaults/models.json +46 -55
  5. package/dist/defaults/settings.json +7 -8
  6. package/dist/extensions/agent-browser.test.ts +178 -2
  7. package/dist/extensions/agent-browser.ts +2 -0
  8. package/dist/extensions/copy-turn.test.ts +229 -0
  9. package/dist/extensions/copy-turn.ts +11 -0
  10. package/dist/extensions/grep-app/index.test.ts +356 -1
  11. package/dist/extensions/grep-app/index.ts +6 -1
  12. package/dist/extensions/handoff-new.test.ts +351 -161
  13. package/dist/extensions/handoff-new.ts +3 -0
  14. package/dist/extensions/inline-skills.test.ts +93 -0
  15. package/dist/extensions/inline-skills.ts +3 -0
  16. package/dist/extensions/pi-subagents/CHANGELOG.md +26 -3
  17. package/dist/extensions/pi-subagents/README.md +2 -0
  18. package/dist/extensions/pi-subagents/agents/builder.md +5 -2
  19. package/dist/extensions/pi-subagents/agents/commentator.md +1 -1
  20. package/dist/extensions/pi-subagents/docs/configuration.md +54 -2
  21. package/dist/extensions/pi-subagents/docs/observability.md +1 -1
  22. package/dist/extensions/pi-subagents/docs/tool-reference.md +1 -1
  23. package/dist/extensions/pi-subagents/docs/workflows.md +1 -1
  24. package/dist/extensions/pi-subagents/package-lock.json +2 -2
  25. package/dist/extensions/pi-subagents/package.json +1 -1
  26. package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +1 -1
  27. package/dist/extensions/pi-subagents/src/agents/agents.ts +9 -3
  28. package/dist/extensions/pi-subagents/src/extension/config.ts +12 -0
  29. package/dist/extensions/pi-subagents/src/extension/doctor.ts +40 -0
  30. package/dist/extensions/pi-subagents/src/extension/index.ts +104 -14
  31. package/dist/extensions/pi-subagents/src/extension/public-execution.ts +5 -0
  32. package/dist/extensions/pi-subagents/src/extension/rpc.ts +3 -9
  33. package/dist/extensions/pi-subagents/src/extension/schemas.ts +2 -2
  34. package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +4 -1
  35. package/dist/extensions/pi-subagents/src/missions/lifecycle.ts +4 -7
  36. package/dist/extensions/pi-subagents/src/missions/store.ts +4 -4
  37. package/dist/extensions/pi-subagents/src/missions/workflow-state.ts +6 -2
  38. package/dist/extensions/pi-subagents/src/runs/background/active-async-capacity.ts +374 -0
  39. package/dist/extensions/pi-subagents/src/runs/background/active-run-index.ts +9 -5
  40. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +117 -29
  41. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +12 -6
  42. package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +7 -1
  43. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +14 -5
  44. package/dist/extensions/pi-subagents/src/runs/background/chain-append.ts +33 -15
  45. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +5 -2
  46. package/dist/extensions/pi-subagents/src/runs/background/owned-process-tree.ts +104 -0
  47. package/dist/extensions/pi-subagents/src/runs/background/process-terminal.ts +17 -3
  48. package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +19 -1
  49. package/dist/extensions/pi-subagents/src/runs/background/retained-children.ts +2 -2
  50. package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +5 -2
  51. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +12 -6
  52. package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +3 -3
  53. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +84 -36
  54. package/dist/extensions/pi-subagents/src/runs/background/wait-subscriptions.ts +2 -1
  55. package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +37 -2
  56. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +135 -22
  57. package/dist/extensions/pi-subagents/src/runs/foreground/foreground-control.ts +12 -0
  58. package/dist/extensions/pi-subagents/src/runs/foreground/foreground-history.ts +7 -4
  59. package/dist/extensions/pi-subagents/src/runs/foreground/prompt-audit.ts +171 -0
  60. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +653 -196
  61. package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +13 -4
  62. package/dist/extensions/pi-subagents/src/runs/shared/llm-intent-arbiter.ts +286 -0
  63. package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +23 -0
  64. package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +2 -0
  65. package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +44 -1
  66. package/dist/extensions/pi-subagents/src/runs/shared/run-fanout-budget.ts +280 -0
  67. package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +4 -2
  68. package/dist/extensions/pi-subagents/src/runs/shared/task-intent.ts +19 -3
  69. package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +17 -5
  70. package/dist/extensions/pi-subagents/src/shared/session-lineage.ts +71 -0
  71. package/dist/extensions/pi-subagents/src/shared/types.ts +108 -0
  72. package/dist/extensions/pi-subagents/src/shared/utils.ts +3 -1
  73. package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +7 -5
  74. package/dist/extensions/pi-subagents/src/tui/fleet.ts +226 -13
  75. package/dist/extensions/pi-subagents/src/workflows/scripted-workflow.ts +22 -5
  76. package/dist/extensions/pi-subagents/src/workflows/workflow-auto-relaunch.ts +28 -0
  77. package/dist/extensions/pi-subagents/test/integration/acceptance-file-report.test.ts +87 -0
  78. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +96 -6
  79. package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +28 -5
  80. package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +8 -5
  81. package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +83 -2
  82. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +211 -15
  83. package/dist/extensions/pi-subagents/test/integration/parallel-execution.test.ts +41 -3
  84. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +314 -10
  85. package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +31 -1
  86. package/dist/extensions/pi-subagents/test/unit/active-async-capacity.test.ts +317 -0
  87. package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +21 -2
  88. package/dist/extensions/pi-subagents/test/unit/async-recovery-descriptor.test.ts +16 -1
  89. package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +6 -1
  90. package/dist/extensions/pi-subagents/test/unit/chain-append.test.ts +75 -4
  91. package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +18 -0
  92. package/dist/extensions/pi-subagents/test/unit/doctor.test.ts +29 -1
  93. package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +31 -5
  94. package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +139 -1
  95. package/dist/extensions/pi-subagents/test/unit/foreground-control.test.ts +10 -0
  96. package/dist/extensions/pi-subagents/test/unit/handoff-adoption.test.ts +103 -0
  97. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +22 -0
  98. package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +2 -1
  99. package/dist/extensions/pi-subagents/test/unit/llm-intent-arbiter.test.ts +171 -0
  100. package/dist/extensions/pi-subagents/test/unit/mission-lifecycle.test.ts +30 -4
  101. package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +14 -0
  102. package/dist/extensions/pi-subagents/test/unit/owned-process-tree.test.ts +69 -0
  103. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +83 -0
  104. package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +2 -2
  105. package/dist/extensions/pi-subagents/test/unit/process-terminal.test.ts +55 -2
  106. package/dist/extensions/pi-subagents/test/unit/public-execution.test.ts +12 -1
  107. package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +7 -7
  108. package/dist/extensions/pi-subagents/test/unit/run-fanout-budget.test.ts +121 -0
  109. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +9 -0
  110. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +4 -3
  111. package/dist/extensions/pi-subagents/test/unit/session-lineage.test.ts +73 -0
  112. package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +28 -0
  113. package/dist/extensions/pi-subagents/test/unit/steering-action.test.ts +10 -1
  114. package/dist/extensions/pi-subagents/test/unit/task-intent.test.ts +39 -0
  115. package/dist/extensions/pi-subagents/test/unit/timeout-defaults.test.ts +65 -0
  116. package/dist/extensions/pi-subagents/test/unit/workflow-auto-relaunch.test.ts +45 -0
  117. package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +61 -0
  118. package/dist/extensions/ponytail/index.js +11 -0
  119. package/dist/extensions/ponytail/ponytail-config.cjs +2 -0
  120. package/dist/extensions/ponytail/ponytail-instructions.cjs +6 -0
  121. package/dist/extensions/ponytail/test/extension.test.js +274 -140
  122. package/dist/extensions/ponytail/test/helpers.test.js +280 -92
  123. package/dist/extensions/question/index.ts +18 -0
  124. package/dist/extensions/question/question-list.ts +22 -0
  125. package/dist/extensions/question/tests/batch.test.ts +103 -65
  126. package/dist/extensions/question/tests/helpers.test.ts +68 -35
  127. package/dist/extensions/question/tests/question-list.test.ts +506 -204
  128. package/dist/extensions/question/tests/row-layout.test.ts +180 -114
  129. package/dist/extensions/question/tests/schemas.test.ts +42 -19
  130. package/dist/extensions/question/tests/shortcuts.test.ts +106 -84
  131. package/dist/extensions/question/tests/wizard.test.ts +771 -50
  132. package/dist/extensions/rtk.test.ts +180 -1
  133. package/dist/extensions/tokenin-onboarding.ts +3 -0
  134. package/dist/extensions/undo.test.ts +720 -0
  135. package/dist/extensions/undo.ts +6 -0
  136. package/dist/extensions/web-agent-onboarding.test.ts +415 -0
  137. package/dist/extensions/workflow/extension.ts +3 -3
  138. package/dist/extensions/workflow/modes.ts +41 -19
  139. package/dist/modes/interactive/interactive-mode.d.ts +1 -0
  140. package/dist/modes/interactive/interactive-mode.js +48 -1
  141. package/dist/skills/pi-subagents/references/execution-controls.md +1 -1
  142. package/docs/plans/workflow-autoloop-reference.md +1 -2
  143. package/docs/plans/workflow-handoff-carryover.md +108 -0
  144. package/docs/quickstart.md +27 -43
  145. package/docs/settings.md +4 -0
  146. package/docs/usage.md +1 -0
  147. package/package.json +5 -1
@@ -55,6 +55,7 @@ describe("below-editor subagent FleetView", () => {
55
55
 
56
56
  it("renders main plus active children below the editor and bounds every line", () => {
57
57
  const state = stateForTest();
58
+ state.activeAsyncCapacity = { used: 2, limit: 4 };
58
59
  const now = Date.now();
59
60
  for (let index = 0; index < 7; index++) {
60
61
  state.foregroundControls.set(`run-${index}`, {
@@ -98,7 +99,7 @@ describe("below-editor subagent FleetView", () => {
98
99
  const component = widgetFactory!(tui, theme);
99
100
  const compactLines = component.render(80);
100
101
  assert.equal(compactLines.length, 1);
101
- assert.ok(compactLines[0]!.includes("7 active agents"));
102
+ assert.ok(compactLines[0]!.includes("7 active agents · Async runs 2/4"));
102
103
  assert.ok(compactLines[0]!.includes("↓ 13.7k tokens"));
103
104
  assert.ok(compactLines[0]!.includes("↓/← to inspect"));
104
105
  assert.ok(visibleWidth(compactLines[0]!) <= 80);
@@ -122,6 +123,31 @@ describe("below-editor subagent FleetView", () => {
122
123
  }
123
124
  });
124
125
 
126
+ it("keeps occupied capacity visible without active child rows", () => {
127
+ const state = stateForTest();
128
+ state.activeAsyncCapacity = { used: 1, limit: 2 };
129
+ let widgetFactory: ((tui: unknown, theme: typeof theme) => { render(width: number): string[] }) | undefined;
130
+ const ctx = {
131
+ hasUI: true,
132
+ ui: {
133
+ setWidget(_key: string, content: typeof widgetFactory | undefined) { if (content) widgetFactory = content; },
134
+ onTerminalInput() { return () => {}; },
135
+ getEditorText() { return ""; },
136
+ requestRender() {},
137
+ notify() {},
138
+ theme,
139
+ },
140
+ } as unknown as ExtensionContext;
141
+ const fleet = new SubagentFleetStatus(state, () => {}, { refreshMs: 60_000 });
142
+ try {
143
+ fleet.setContext(ctx);
144
+ assert.ok(widgetFactory);
145
+ assert.deepEqual(widgetFactory!({ requestRender() {} }, theme).render(80), [" Async runs 1/2 · ↓ 0 tokens · ↓/← to inspect"]);
146
+ } finally {
147
+ fleet.dispose();
148
+ }
149
+ });
150
+
125
151
  it("keeps one queued agent visible in the compact summary", () => {
126
152
  const state = stateForTest();
127
153
  state.asyncJobs.set("run-worker", {
@@ -596,7 +622,7 @@ describe("below-editor subagent FleetView", () => {
596
622
  assert.deepEqual(statusKeys, inspectorKeys);
597
623
  });
598
624
 
599
- it("uses tracked async task descriptions and per-child token totals", () => {
625
+ it("hides tracked async task descriptions and shows per-child token totals", () => {
600
626
  const state = stateForTest();
601
627
  state.asyncJobs.set("async-run", {
602
628
  asyncId: "async-run",
@@ -630,9 +656,9 @@ describe("below-editor subagent FleetView", () => {
630
656
  const component = widgetFactory!(tui, theme);
631
657
  assert.deepEqual(fleet.handleKey("\x1b[B"), { consume: true });
632
658
  const lines = component.render(180);
633
- assert.ok(lines.some((line) => line.includes("reviewer (gpt-5 · thinking medium)") && line.includes("Review only authentication")));
634
- assert.ok(lines.some((line) => line.includes("worker") && line.includes("Implement only billing")));
635
- assert.ok(lines.every((line) => !line.includes("Review the authentication changes")), "per-child descriptions should replace the run-level fallback when present");
659
+ assert.ok(lines.some((line) => line.includes("reviewer (gpt-5 · thinking medium)")));
660
+ assert.ok(lines.some((line) => line.includes("worker")));
661
+ assert.ok(lines.every((line) => !line.includes("Review only authentication") && !line.includes("Implement only billing") && !line.includes("Review the authentication changes")));
636
662
  assert.ok(lines.some((line) => line.includes("↓ 4.2k tokens")));
637
663
  } finally {
638
664
  fleet.dispose();
@@ -7,8 +7,11 @@ import { visibleWidth, type MarkdownTheme } from "@earendil-works/pi-tui";
7
7
  import { collectFleetSnapshot, openSubagentFleet, SubagentFleetComponent } from "../../src/tui/fleet.ts";
8
8
  import { persistForegroundRunHistory, restoreForegroundRunHistory } from "../../src/runs/foreground/foreground-history.ts";
9
9
  import { FLEET_STATUS_WIDGET_KEY } from "../../src/tui/fleet-status.ts";
10
+ import { registerLivePromptAudit, rewritePromptWithGuidance } from "../../src/runs/foreground/prompt-audit.ts";
10
11
  import { getArtifactPaths, getArtifactsDir, getProjectArtifactsDir } from "../../src/shared/artifacts.ts";
11
12
  import type { SubagentState } from "../../src/shared/types.ts";
13
+ import { fauxAssistantMessage } from "@earendil-works/pi-ai/providers/faux";
14
+ import { createAssistantMessageEventStream } from "@earendil-works/pi-ai";
12
15
 
13
16
  function stateForTest(): SubagentState {
14
17
  return {
@@ -92,6 +95,35 @@ const markdownTheme: MarkdownTheme = {
92
95
  };
93
96
 
94
97
  describe("native subagent fleet", () => {
98
+ it("rewrites an authored prompt from guidance without persistence", async () => {
99
+ const calls: unknown[] = [];
100
+ const streamFn = (_model: never, context: unknown) => {
101
+ calls.push(context);
102
+ const stream = createAssistantMessageEventStream();
103
+ queueMicrotask(() => stream.push({ type: "done", reason: "stop", message: fauxAssistantMessage("Rewritten task", { stopReason: "stop" }) }));
104
+ return stream;
105
+ };
106
+ const model = { provider: "faux", id: "rewrite", api: "faux", input: ["text"], reasoning: false, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 1000, maxTokens: 100 };
107
+ const ctx = {
108
+ model,
109
+ signal: undefined,
110
+ modelRegistry: {
111
+ async getApiKeyAndHeaders() { return { ok: true as const, apiKey: "test" }; },
112
+ getRegisteredProviderConfig() { return { api: "faux", streamSimple: streamFn }; },
113
+ },
114
+ } as never;
115
+ const rewritten = await rewritePromptWithGuidance({
116
+ ctx,
117
+ authoredTask: "Original task",
118
+ runtimeAdditions: "Runtime context",
119
+ finalEffectivePrompt: "Runtime context\n\nOriginal task",
120
+ guidance: "Make it narrower",
121
+ });
122
+ assert.equal(rewritten, "Rewritten task");
123
+ assert.match(JSON.stringify(calls), /Original task/);
124
+ assert.match(JSON.stringify(calls), /Make it narrower/);
125
+ });
126
+
95
127
  it("collects current-session foreground and flattened async children", () => {
96
128
  const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-fleet-collect-"));
97
129
  try {
@@ -136,6 +168,112 @@ describe("native subagent fleet", () => {
136
168
  }
137
169
  });
138
170
 
171
+ it("shows live prompt summaries and opens Prompt Audit with the authored prompt visible", async () => {
172
+ const sentinel = "PROMPT_AUDIT_SENTINEL_1021";
173
+ const secondSentinel = "PROMPT_AUDIT_SECOND_CHILD";
174
+ const state = stateForTest();
175
+ const control = {
176
+ runId: "prompt-run",
177
+ sessionId: "session-current",
178
+ mode: "parallel" as const,
179
+ startedAt: 10,
180
+ updatedAt: 20,
181
+ activeChildren: new Map([
182
+ [0, { index: 0, agent: "worker", startedAt: 10, updatedAt: 20 }],
183
+ [1, { index: 1, agent: "reviewer", startedAt: 11, updatedAt: 21 }],
184
+ ]),
185
+ };
186
+ state.foregroundControls.set(control.runId, control);
187
+ registerLivePromptAudit(control, 0, sentinel, `[Read from: context.md]\n\n${sentinel}\n\n---\nRuntime acceptance`, { rerun: { params: { agent: "worker", task: sentinel } } });
188
+ registerLivePromptAudit(control, 1, secondSentinel, secondSentinel);
189
+ assert.doesNotMatch(JSON.stringify(control), new RegExp(sentinel), "live prompts must not enter serializable Fleet state");
190
+ const clipboardWrites: string[] = [];
191
+ const redoCalls: Array<{ runId: string; index: number; guidance: string }> = [];
192
+ const component = new SubagentFleetComponent(
193
+ { terminal: { rows: 28, columns: 100 }, requestRender() {} } as never,
194
+ theme as never,
195
+ state,
196
+ () => {},
197
+ {
198
+ refreshMs: 60_000,
199
+ markdownTheme,
200
+ copyText: (text) => { clipboardWrites.push(text); },
201
+ actions: {
202
+ async steer() { return { text: "unused" }; },
203
+ stop() { return { text: "unused" }; },
204
+ async redoPrompt(input) {
205
+ redoCalls.push(input);
206
+ return { text: "Prompt redo started redo-run." };
207
+ },
208
+ },
209
+ },
210
+ );
211
+ try {
212
+ const initialRender = component.render(100).join("\n");
213
+ assert.match(initialRender, new RegExp(`Task: ${sentinel}`));
214
+ assert.match(initialRender, /Prompt audit: 2 live · 3 views · p opens/);
215
+ component.handleInput("p");
216
+ assert.doesNotMatch(component.render(100).join("\n"), /Prompt text hidden|Enter reveal|r hide/);
217
+ assert.match(component.render(100).join("\n"), new RegExp(sentinel));
218
+ component.handleInput("2");
219
+ assert.match(component.render(100).join("\n"), /Read from: context\.md/);
220
+ component.handleInput("3");
221
+ assert.match(component.render(100).join("\n"), /Runtime acceptance/);
222
+ component.handleInput("c");
223
+ await new Promise((resolve) => setImmediate(resolve));
224
+ assert.deepEqual(clipboardWrites, [`[Read from: context.md]\n\n${sentinel}\n\n---\nRuntime acceptance`]);
225
+ assert.match(component.render(100).join("\n"), /Copied visible prompt view/);
226
+ component.handleInput("g");
227
+ for (const char of "make narrower") component.handleInput(char);
228
+ component.handleInput("\r");
229
+ await new Promise((resolve) => setImmediate(resolve));
230
+ assert.deepEqual(redoCalls, [{ runId: "prompt-run", index: 0, guidance: "make narrower", control }]);
231
+ assert.match(component.render(100).join("\n"), /Prompt redo started redo-run/);
232
+ component.handleInput("j");
233
+ assert.match(component.render(100).join("\n"), /Selected: 2\/2/);
234
+ assert.match(component.render(100).join("\n"), /Agent: reviewer/);
235
+ assert.match(component.render(100).join("\n"), new RegExp(secondSentinel));
236
+ component.handleInput("k");
237
+ component.handleInput("1");
238
+ assert.match(component.render(100).join("\n"), /Selected: 1\/2/);
239
+ assert.match(component.render(100).join("\n"), /Agent: worker/);
240
+ assert.match(component.render(100).join("\n"), new RegExp(sentinel));
241
+ component.handleInput("\x1b");
242
+ assert.match(component.render(100).join("\n"), /Prompt audit: 2 live · 3 views · p opens/);
243
+ } finally {
244
+ component.dispose();
245
+ }
246
+ });
247
+
248
+ it("rejects Prompt Audit for a child owned by another session", () => {
249
+ const state = stateForTest();
250
+ const control = {
251
+ runId: "other-run",
252
+ sessionId: "session-other",
253
+ mode: "single" as const,
254
+ startedAt: 10,
255
+ updatedAt: 20,
256
+ activeChildren: new Map([[0, { index: 0, agent: "worker", startedAt: 10, updatedAt: 20 }]]),
257
+ };
258
+ state.foregroundControls.set(control.runId, control);
259
+ registerLivePromptAudit(control, 0, "SECRET", "SECRET");
260
+ const component = new SubagentFleetComponent(
261
+ { terminal: { rows: 28, columns: 100 }, requestRender() {} } as never,
262
+ theme as never,
263
+ state,
264
+ () => {},
265
+ { refreshMs: 60_000, markdownTheme },
266
+ );
267
+ try {
268
+ component.handleInput("p");
269
+ const rendered = component.render(100).join("\n");
270
+ assert.match(rendered, /Prompt Audit is available only/);
271
+ assert.doesNotMatch(rendered, /SECRET/);
272
+ } finally {
273
+ component.dispose();
274
+ }
275
+ });
276
+
139
277
  it("keeps tracked active workflows ahead of older failed terminal history", () => {
140
278
  const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-fleet-history-"));
141
279
  try {
@@ -778,7 +916,7 @@ describe("native subagent fleet", () => {
778
916
  assert.ok(lines.some((line) => line.includes("reviewer")));
779
917
  assert.ok(lines.some((line) => line.includes("foreground · live")));
780
918
  assert.ok(lines.some((line) => line.includes("live-model · thinking high")));
781
- assert.ok(lines.some((line) => line.includes("Task") && line.includes("Implement the active task")));
919
+ assert.ok(lines.every((line) => !line.includes("Implement the active task") && !line.includes("Review the active task")));
782
920
  assert.ok(lines.some((line) => line.includes("Conversation") && line.includes("assistant response")));
783
921
  assert.ok(lines.some((line) => line.includes("Worker live result")));
784
922
  } finally {
@@ -9,6 +9,7 @@ import {
9
9
  settleForegroundSchedulingOwner,
10
10
  updateForegroundChild,
11
11
  } from "../../src/runs/foreground/foreground-control.ts";
12
+ import { getLivePromptAudit } from "../../src/runs/foreground/prompt-audit.ts";
12
13
 
13
14
  function progress(index: number, agent: string, tokens: number): AgentProgress {
14
15
  return {
@@ -43,6 +44,8 @@ describe("foreground child control", () => {
43
44
  beginForegroundChild(control, {
44
45
  index: 0,
45
46
  agent: "reviewer",
47
+ authoredTask: "review",
48
+ effectivePrompt: "review",
46
49
  description: "Review correctness",
47
50
  interrupt: () => { firstInterrupts++; return true; },
48
51
  detach: () => { firstDetaches++; return true; },
@@ -50,11 +53,17 @@ describe("foreground child control", () => {
50
53
  beginForegroundChild(control, {
51
54
  index: 1,
52
55
  agent: "reviewer",
56
+ authoredTask: "review next",
57
+ effectivePrompt: "review next",
53
58
  description: "Review quality",
59
+ rerun: { params: { agent: "reviewer", task: "review next" } },
54
60
  interrupt: () => { secondInterrupts++; return true; },
55
61
  });
56
62
 
57
63
  assert.equal(control.activeChildren?.size, 2);
64
+ assert.equal(getLivePromptAudit(control, 1)?.authoredTask, "review next");
65
+ assert.deepEqual(getLivePromptAudit(control, 1)?.rerun?.params, { agent: "reviewer", task: "review next" });
66
+ assert.doesNotMatch(JSON.stringify(control), /review next/);
58
67
  assert.equal(control.currentIndex, 1);
59
68
  updateForegroundChild(control, 0, progress(0, "reviewer", 120));
60
69
  assert.equal(control.currentIndex, 0);
@@ -72,6 +81,7 @@ describe("foreground child control", () => {
72
81
 
73
82
  updateForegroundChild(control, 1, progress(1, "reviewer", 240));
74
83
  finishForegroundChild(control, 1);
84
+ assert.equal(getLivePromptAudit(control, 1), undefined);
75
85
  assert.equal(control.currentIndex, 0);
76
86
  assert.equal(control.tokens, 120);
77
87
  assert.deepEqual([...control.activeChildren!.keys()], [0]);
@@ -0,0 +1,103 @@
1
+ import assert from "node:assert/strict";
2
+ import { execFileSync } from "node:child_process";
3
+ import { fileURLToPath } from "node:url";
4
+ import path from "node:path";
5
+ import { describe, it } from "node:test";
6
+
7
+ const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
8
+
9
+ describe("handoff workflow carry-over", () => {
10
+ it("adopts the previous session's active async workflows and tells the agent", () => {
11
+ const script = String.raw`
12
+ import os from "node:os";
13
+ import path from "node:path";
14
+ import fs from "node:fs";
15
+ import { DIRS } from "./src/shared/types.ts";
16
+ import registerSubagentExtension from "./index.ts";
17
+ const handlers = new Map();
18
+ const events = { listeners: new Map(), on(name, handler) { this.listeners.set(name, handler); return () => this.listeners.delete(name); }, emit(name, payload) { this.listeners.get(name)?.(payload); } };
19
+ const sent = [];
20
+ const widgets = [];
21
+ const pi = new Proxy({
22
+ events,
23
+ on(name, handler) { handlers.set(name, handler); },
24
+ registerTool() {}, registerCommand() {}, registerShortcut() {}, registerMessageRenderer() {},
25
+ sendMessage(message, options) { sent.push({ message, options }); }, getSessionName() { return undefined; },
26
+ }, { get(target, prop) { return prop in target ? target[prop] : () => undefined; } });
27
+
28
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "selesai-handoff-"));
29
+ const oldSession = path.join(tmp, "old-session.jsonl");
30
+ const newSession = path.join(tmp, "new-session.jsonl");
31
+ fs.writeFileSync(oldSession, JSON.stringify({ type: "session", version: 1, id: "old-session", timestamp: new Date().toISOString(), cwd: process.cwd() }) + "\n");
32
+ fs.writeFileSync(newSession, JSON.stringify({ type: "session", version: 1, id: "new-session", timestamp: new Date().toISOString(), cwd: process.cwd(), parentSession: oldSession }) + "\n");
33
+
34
+ // One active async workflow owned by the previous session.
35
+ const runId = "adopted-workflow-run";
36
+ const asyncDir = path.join(DIRS.async, runId);
37
+ fs.mkdirSync(asyncDir, { recursive: true });
38
+ fs.writeFileSync(path.join(asyncDir, "status.json"), JSON.stringify({
39
+ runId,
40
+ state: "running",
41
+ sessionId: oldSession,
42
+ mode: "workflow",
43
+ workflowKey: "task",
44
+ agent: "builder",
45
+ startedAt: Date.now(),
46
+ steps: [{ agent: "builder", status: "running", phase: "build-1", description: "Implement the carry-over plan" }],
47
+ }));
48
+ fs.mkdirSync(path.join(DIRS.async, ".active-runs"), { recursive: true });
49
+ fs.writeFileSync(path.join(DIRS.async, ".active-runs", runId), "");
50
+
51
+ // A result file for an old-session run written during the switch window.
52
+ const resultRunId = "adopted-completed-run";
53
+ const resultPath = path.join(DIRS.results, resultRunId + ".json");
54
+ fs.mkdirSync(DIRS.results, { recursive: true });
55
+ fs.writeFileSync(resultPath, JSON.stringify({ runId: resultRunId, sessionId: oldSession, agent: "explorer", summary: "done", success: true }));
56
+
57
+ const ctx = { cwd: process.cwd(), hasUI: true, ui: { setWidget(key, value) { widgets.push([key, value]); }, requestRender() {}, onTerminalInput() { return () => {}; }, getEditorText() { return ""; }, notify() {}, theme: { fg(_name, text) { return text; }, bg(_name, text) { return text; }, bold(text) { return text; } } }, sessionManager: { getSessionId() { return "new-session"; }, getSessionFile() { return newSession; }, getHeader() { return { parentSession: oldSession }; }, getEntries() { return []; } }, modelRegistry: { getAvailable() { return []; } } };
58
+
59
+ registerSubagentExtension(pi);
60
+ handlers.get("session_start")({ reason: "new", previousSessionFile: oldSession }, ctx);
61
+ sent.length = 0;
62
+
63
+ // No handoff notice until a turn settles.
64
+ if (sent.length !== 0) throw new Error("handoff notice sent before the first settled turn");
65
+
66
+ handlers.get("agent_settled")();
67
+ const handoff = sent.filter((entry) => entry.message?.customType === "subagent-handoff-resume");
68
+ if (handoff.length !== 1) throw new Error(JSON.stringify(sent.map((entry) => entry.message)));
69
+ if (handoff[0].options?.triggerTurn !== true) throw new Error("handoff notice did not trigger a turn");
70
+ if (typeof handoff[0].message?.content !== "string" || !handoff[0].message.content.includes(runId) || !handoff[0].message.content.includes("carried over")) {
71
+ throw new Error("handoff notice lacks the adopted workflow: " + JSON.stringify(handoff[0].message));
72
+ }
73
+
74
+ // Only once per adoption.
75
+ handlers.get("agent_settled")();
76
+ if (sent.filter((entry) => entry.message?.customType === "subagent-handoff-resume").length !== 1) throw new Error("handoff notice repeated");
77
+
78
+ // Compaction resume message now lists the live workflows.
79
+ handlers.get("session_before_compact")({ reason: "threshold", signal: new AbortController().signal });
80
+ handlers.get("session_compact")({ reason: "threshold" });
81
+ const compaction = sent.filter((entry) => entry.message?.customType === "subagent-compaction-resume");
82
+ if (compaction.length !== 1) throw new Error("compaction resume missing: " + JSON.stringify(sent.map((entry) => entry.message?.customType)));
83
+ if (typeof compaction[0].message?.content !== "string" || !compaction[0].message.content.includes(runId)) {
84
+ throw new Error("compaction resume lacks the workflow manifest: " + JSON.stringify(compaction[0].message));
85
+ }
86
+
87
+ // Adopted old-session result gets delivered and cleaned up (batched
88
+ // notification debounces, so poll with a generous bound).
89
+ let delivered = false;
90
+ for (let attempt = 0; attempt < 40 && !delivered; attempt++) {
91
+ await new Promise((resolve) => setTimeout(resolve, 250));
92
+ delivered = !fs.existsSync(resultPath);
93
+ }
94
+ if (!delivered) throw new Error("adopted result file was not delivered");
95
+
96
+ await handlers.get("session_shutdown")();
97
+ `;
98
+ const env = { ...process.env };
99
+ delete env.SELESAI_SUBAGENT_CHILD;
100
+ execFileSync(process.execPath, ["--experimental-strip-types", "--import", "./test/support/register-loader.mjs", "--input-type=module", "--eval", script], { cwd: projectRoot, env, stdio: "pipe" });
101
+ assert.ok(true);
102
+ });
103
+ });
@@ -103,6 +103,28 @@ describe("subagent extension child mode", () => {
103
103
  execFileSync(process.execPath, ["--experimental-strip-types", "--import", "./test/support/register-loader.mjs", "--input-type=module", "--eval", script], { cwd: projectRoot, env: parentToolEnv(), stdio: "pipe" });
104
104
  });
105
105
 
106
+ it("declares packaged prompts through resources_discover", () => {
107
+ const script = String.raw`
108
+ import registerSubagentExtension from "./index.ts";
109
+ import * as fs from "node:fs";
110
+ const handlers = new Map();
111
+ const fakePi = new Proxy({
112
+ events: { on() { return () => {}; }, emit() {} },
113
+ registerTool() {}, registerCommand() {}, registerShortcut() {}, registerMessageRenderer() {}, sendMessage() {}, getSessionName() {},
114
+ on(event, handler) { handlers.set(event, handler); },
115
+ }, { get(target, prop) { return prop in target ? target[prop] : () => undefined; } });
116
+ registerSubagentExtension(fakePi);
117
+ const handler = handlers.get("resources_discover");
118
+ if (!handler) throw new Error("resources_discover handler not registered");
119
+ const result = handler({ type: "resources_discover", cwd: process.cwd(), reason: "startup" });
120
+ if (!result || !Array.isArray(result.promptPaths) || result.promptPaths.length !== 1) throw new Error("expected one promptPath, got " + JSON.stringify(result));
121
+ const promptDir = result.promptPaths[0];
122
+ if (!fs.existsSync(promptDir + "/review-loop.md")) throw new Error("review-loop.md not found under " + promptDir);
123
+ if (!fs.existsSync(promptDir + "/parallel-review.md")) throw new Error("parallel-review.md not found under " + promptDir);
124
+ `;
125
+ execFileSync(process.execPath, ["--experimental-strip-types", "--import", "./test/support/register-loader.mjs", "--input-type=module", "--eval", script], { cwd: projectRoot, env: parentToolEnv(), stdio: "pipe" });
126
+ });
127
+
106
128
  it("shows omitted workflow async as background even when asyncByDefault is false", () => {
107
129
  const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-workflow-manifest-config-"));
108
130
  try {
@@ -52,7 +52,8 @@ describe("resolveIntercomSessionTarget", () => {
52
52
  });
53
53
 
54
54
  it("uses the current pi-intercom runtime id for unnamed fallback when present", () => {
55
- assert.equal(resolveIntercomSessionTarget(undefined, "session-12345678", "session-stableabcdef"), "subagent-chat-stableab");
55
+ assert.equal(resolveIntercomSessionTarget(undefined, "session-12345678", "session-stableabcdef"), "subagent-chat-stableabcdef");
56
+ assert.equal(resolveIntercomSessionTarget(undefined, "session-12345678", "session-abcdefghijklmnopqrst"), "subagent-chat-abcdefghijklmnopqr");
56
57
  });
57
58
  });
58
59
 
@@ -0,0 +1,171 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
+ import {
4
+ arbitrateCompletionGuardRescue,
5
+ createTaskMutationArbiter,
6
+ isCompletionGuardFailure,
7
+ maybeRescueCompletionGuardFailure,
8
+ type TaskMutationArbiter,
9
+ } from "../../src/runs/shared/llm-intent-arbiter.ts";
10
+
11
+ const GUARD_ERROR =
12
+ "Subagent completed without making edits for an implementation task.\nIt appears to have returned planning or scratchpad output instead of applying changes.";
13
+
14
+ function fakeCtx(overrides: { models?: Array<{ provider?: string; id?: string; api?: string }> } = {}) {
15
+ const models = overrides.models ?? [{ provider: "test", id: "model-1", api: "test-api" }];
16
+ return {
17
+ model: models[0],
18
+ modelRegistry: {
19
+ getAvailable: () => models,
20
+ getRegisteredProviderConfig: () => undefined,
21
+ },
22
+ } as never;
23
+ }
24
+
25
+ function stubArbiter(verdict: "read-only" | "implementation" | "unavailable" | "throw"): TaskMutationArbiter {
26
+ return async () => {
27
+ if (verdict === "throw") throw new Error("boom");
28
+ return verdict;
29
+ };
30
+ }
31
+
32
+ describe("arbitrateCompletionGuardRescue", () => {
33
+ it("rescues only a confident read-only verdict", async () => {
34
+ const rescued = await arbitrateCompletionGuardRescue({
35
+ guardTriggered: true,
36
+ task: "t",
37
+ arbiter: stubArbiter("read-only"),
38
+ });
39
+ assert.deepEqual(rescued, { triggered: false, rescued: true });
40
+ for (const verdict of ["implementation", "unavailable"] as const) {
41
+ const kept = await arbitrateCompletionGuardRescue({
42
+ guardTriggered: true,
43
+ task: "t",
44
+ arbiter: stubArbiter(verdict),
45
+ });
46
+ assert.deepEqual(kept, { triggered: true, rescued: false }, verdict);
47
+ }
48
+ });
49
+
50
+ it("keeps the guard verdict without an arbiter or on arbiter failure", async () => {
51
+ assert.deepEqual(
52
+ await arbitrateCompletionGuardRescue({ guardTriggered: true, task: "t" }),
53
+ { triggered: true, rescued: false },
54
+ );
55
+ assert.deepEqual(
56
+ await arbitrateCompletionGuardRescue({ guardTriggered: true, task: "t", arbiter: stubArbiter("throw") }),
57
+ { triggered: true, rescued: false },
58
+ );
59
+ assert.deepEqual(
60
+ await arbitrateCompletionGuardRescue({ guardTriggered: false, task: "t", arbiter: stubArbiter("read-only") }),
61
+ { triggered: false, rescued: false },
62
+ );
63
+ });
64
+ });
65
+
66
+ describe("isCompletionGuardFailure", () => {
67
+ it("recognizes the guard error prefix", () => {
68
+ assert.equal(isCompletionGuardFailure({ error: GUARD_ERROR }), true);
69
+ assert.equal(isCompletionGuardFailure({ error: "Some other failure." }), false);
70
+ assert.equal(isCompletionGuardFailure({}), false);
71
+ });
72
+ });
73
+
74
+ describe("maybeRescueCompletionGuardFailure", () => {
75
+ it("keeps the failure without an arbiter", async () => {
76
+ const result = { exitCode: 1, error: GUARD_ERROR };
77
+ const rescued = await maybeRescueCompletionGuardFailure(result, "task", undefined);
78
+ assert.equal(rescued, false);
79
+ assert.equal(result.exitCode, 1);
80
+ assert.equal(result.error, GUARD_ERROR);
81
+ });
82
+
83
+ it("rescues only a confident read-only verdict", async () => {
84
+ for (const verdict of ["implementation", "unavailable"] as const) {
85
+ const result = { exitCode: 1, error: GUARD_ERROR };
86
+ const rescued = await maybeRescueCompletionGuardFailure(result, "task", stubArbiter(verdict));
87
+ assert.equal(rescued, false, verdict);
88
+ assert.equal(result.exitCode, 1);
89
+ assert.equal(result.error, GUARD_ERROR);
90
+ }
91
+ const result = { exitCode: 1, error: GUARD_ERROR };
92
+ const rescued = await maybeRescueCompletionGuardFailure(result, "task", stubArbiter("read-only"));
93
+ assert.equal(rescued, true);
94
+ assert.equal(result.exitCode, 0);
95
+ assert.equal(result.error, undefined);
96
+ });
97
+
98
+ it("fails closed when the arbiter throws", async () => {
99
+ const result = { exitCode: 1, error: GUARD_ERROR };
100
+ const rescued = await maybeRescueCompletionGuardFailure(result, "task", stubArbiter("throw"));
101
+ assert.equal(rescued, false);
102
+ assert.equal(result.exitCode, 1);
103
+ assert.equal(result.error, GUARD_ERROR);
104
+ });
105
+
106
+ it("does not touch unrelated failures", async () => {
107
+ const result = { exitCode: 1, error: "Something else broke." };
108
+ const rescued = await maybeRescueCompletionGuardFailure(result, "task", stubArbiter("read-only"));
109
+ assert.equal(rescued, false);
110
+ assert.equal(result.exitCode, 1);
111
+ });
112
+ });
113
+
114
+ describe("createTaskMutationArbiter", () => {
115
+ it("is disabled by the env opt-out", () => {
116
+ const previous = process.env.SELESAI_SUBAGENTS_LLM_INTENT_ARBITER;
117
+ process.env.SELESAI_SUBAGENTS_LLM_INTENT_ARBITER = "0";
118
+ try {
119
+ assert.equal(createTaskMutationArbiter(fakeCtx()), undefined);
120
+ } finally {
121
+ if (previous === undefined) delete process.env.SELESAI_SUBAGENTS_LLM_INTENT_ARBITER;
122
+ else process.env.SELESAI_SUBAGENTS_LLM_INTENT_ARBITER = previous;
123
+ }
124
+ });
125
+
126
+ it("returns undefined when no model can be resolved", () => {
127
+ assert.equal(createTaskMutationArbiter(fakeCtx({ models: [] })), undefined);
128
+ });
129
+
130
+ it("returns a working arbiter bound to the host model", async () => {
131
+ const arbiter = createTaskMutationArbiter(fakeCtx(), {
132
+ streamFn: async () => {
133
+ throw new Error("not reached in this seam test");
134
+ },
135
+ });
136
+ assert.notEqual(arbiter, undefined);
137
+ // Without a scripted stream the underlying Agent call cannot succeed;
138
+ // the arbiter must degrade to "unavailable" rather than throw.
139
+ const verdict = await arbiter!("task");
140
+ assert.equal(verdict, "unavailable");
141
+ });
142
+
143
+ it("memoizes verdicts per task", async () => {
144
+ let streamCalls = 0;
145
+ const arbiter = createTaskMutationArbiter(fakeCtx(), {
146
+ streamFn: async () => {
147
+ streamCalls += 1;
148
+ throw new Error("fail");
149
+ },
150
+ })!;
151
+ const v1 = await arbiter("t");
152
+ const v2 = await arbiter("t");
153
+ assert.equal(v1, "unavailable");
154
+ assert.equal(v2, "unavailable");
155
+ assert.equal(streamCalls, 1);
156
+ });
157
+
158
+ it("uses the full task as the cache key", async () => {
159
+ let streamCalls = 0;
160
+ const arbiter = createTaskMutationArbiter(fakeCtx(), {
161
+ streamFn: async () => {
162
+ streamCalls += 1;
163
+ throw new Error("fail");
164
+ },
165
+ })!;
166
+ const prefix = "x".repeat(180);
167
+ await arbiter(`${prefix}A`);
168
+ await arbiter(`${prefix}B`);
169
+ assert.equal(streamCalls, 2);
170
+ });
171
+ });
@@ -11,6 +11,7 @@ import {
11
11
  syncMissionFromAsyncCompletion,
12
12
  } from "../../src/missions/lifecycle.ts";
13
13
  import { readMission } from "../../src/missions/store.ts";
14
+ import { PROMPT_REDACTED } from "../../src/shared/utils.ts";
14
15
 
15
16
  function projectFixture() {
16
17
  const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-mission-lifecycle-"));
@@ -52,7 +53,8 @@ describe("mission launch lifecycle", () => {
52
53
  });
53
54
  assert.ok(binding);
54
55
  const mission = readMission(binding.location, binding.missionId);
55
- assert.equal(mission.objective, "Map the auth flow");
56
+ assert.equal(mission.objective, PROMPT_REDACTED);
57
+ assert.equal(mission.title, PROMPT_REDACTED);
56
58
  assert.equal(mission.status, "active");
57
59
  const result = attachMissionToLaunchResult({
58
60
  binding,
@@ -80,7 +82,7 @@ describe("mission launch lifecycle", () => {
80
82
  config: test.missionConfig,
81
83
  });
82
84
  assert.ok(parallelOnly);
83
- assert.equal(readMission(parallelOnly.location, parallelOnly.missionId).objective, "Review parallel work");
85
+ assert.equal(readMission(parallelOnly.location, parallelOnly.missionId).objective, PROMPT_REDACTED);
84
86
 
85
87
  assert.throws(() => prepareMissionLaunch({
86
88
  params: { missionId: "" },
@@ -107,7 +109,7 @@ describe("mission launch lifecycle", () => {
107
109
  config: { ...test.missionConfig, enabled: false },
108
110
  });
109
111
  assert.ok(explicit);
110
- assert.equal(readMission(explicit.location, explicit.missionId).objective, "Tiny one-off");
112
+ assert.equal(readMission(explicit.location, explicit.missionId).objective, "Explicit mission");
111
113
 
112
114
  const summaryAlias = prepareMissionLaunch({
113
115
  params: { mission: { summary: "Review active backlog", labels: ["review"] }, task: "Review the current diff" },
@@ -117,7 +119,31 @@ describe("mission launch lifecycle", () => {
117
119
  assert.ok(summaryAlias);
118
120
  assert.deepEqual(readMission(summaryAlias.location, summaryAlias.missionId).labels, ["review"]);
119
121
  assert.equal(readMission(summaryAlias.location, summaryAlias.missionId).title, "Review active backlog");
120
- assert.equal(readMission(summaryAlias.location, summaryAlias.missionId).objective, "Review the current diff");
122
+ assert.equal(readMission(summaryAlias.location, summaryAlias.missionId).objective, "Review active backlog");
123
+ } finally {
124
+ fs.rmSync(test.root, { recursive: true, force: true });
125
+ }
126
+ });
127
+
128
+ it("explains when an explicit mission belongs to another worktree", () => {
129
+ const test = projectFixture();
130
+ const worktreeRoot = path.join(test.root, "worktree");
131
+ fs.mkdirSync(worktreeRoot, { recursive: true });
132
+ try {
133
+ const binding = prepareMissionLaunch({
134
+ params: { mission: { title: "Root mission" }, task: "Prepare root work" },
135
+ projectRoot: test.projectRoot,
136
+ config: test.missionConfig,
137
+ });
138
+ assert.ok(binding);
139
+ const missionDir = path.join(worktreeRoot, ".pi-subagents", "missions");
140
+ assert.throws(() => prepareMissionLaunch({
141
+ params: { missionId: binding.missionId, task: "Prepare worktree work" },
142
+ projectRoot: worktreeRoot,
143
+ config: test.missionConfig,
144
+ }), {
145
+ message: `Mission '${binding.missionId}' was not found in mission directory '${missionDir}' for project root '${worktreeRoot}'. If it was created in another worktree, run the request from that worktree.`,
146
+ });
121
147
  } finally {
122
148
  fs.rmSync(test.root, { recursive: true, force: true });
123
149
  }