@emiliosp/pi-maestro 0.18.0 → 0.18.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/ADR.md CHANGED
@@ -280,7 +280,7 @@ Approval can finish at `candidate-ready` without agent execution. Agent and mode
280
280
 
281
281
  ### Status
282
282
 
283
- Design decisions confirmed by the `owner`. Not implemented.
283
+ Design decisions confirmed by the `owner`. Implemented.
284
284
 
285
285
  ### Context
286
286
 
package/CHANGELOG.md CHANGED
@@ -3,6 +3,16 @@
3
3
  This file summarizes meaningful changes to the codebase, including behavior, data formats, architecture, and development tools. Git tags identify version boundaries;
4
4
  GitHub Releases are not required.
5
5
 
6
+ ## 0.18.2
7
+
8
+ 1. The `run` progress for `builder`, `verifier`, and `simplifier` now shows only elapsed time and the latest assistant text. The role name and the active tool no longer
9
+ appear.
10
+
11
+ ## 0.18.1
12
+
13
+ 1. Running `builder`, `verifier`, and `simplifier` tools now show elapsed time, active tools, and streaming normal assistant text. Elapsed time keeps updating during
14
+ silent work. Pi controls preview and expansion, while intermediate activity stays out of the main model's context.
15
+
6
16
  ## 0.18.0
7
17
 
8
18
  1. `Builder`, `verifier`, and `simplifier` now run in fresh native Pi SDK sessions. Activation no longer requires a separate agent extension. The `owner` can interrupt
package/ROADMAP.md CHANGED
@@ -38,7 +38,7 @@ Completed entries preserve recorded outcomes and evidence, which may describe be
38
38
  | MAESTRO-FREE-3 | [Run the builder through the Pi SDK](#maestro-free-3-run-the-builder-through-the-pi-sdk) | Completed |
39
39
  | MAESTRO-FREE-4 | [Run the verifier through the Pi SDK](#maestro-free-4-run-the-verifier-through-the-pi-sdk) | Completed |
40
40
  | MAESTRO-FREE-5 | [Run the simplifier through the Pi SDK](#maestro-free-5-run-the-simplifier-through-the-pi-sdk) | Completed |
41
- | MAESTRO-FREE-6 | [Show SDK agent activity through Pi](#maestro-free-6-show-sdk-agent-activity-through-pi) | Backlog |
41
+ | MAESTRO-FREE-6 | [Show SDK agent activity through Pi](#maestro-free-6-show-sdk-agent-activity-through-pi) | Completed |
42
42
  | MAESTRO-FREE-7 | [Remove the pi-subagents dependency](#maestro-free-7-remove-the-pi-subagents-dependency) | Completed |
43
43
 
44
44
  ## MAESTRO-6: Display specs in a browser
@@ -176,6 +176,10 @@ Connect the `simplifier` to the SDK executor delivered by [MAESTRO-FREE-PRE-3](#
176
176
 
177
177
  ## MAESTRO-FREE-6: Show SDK agent activity through Pi
178
178
 
179
+ Implemented under the [approved specification](.specs/20261106-143021-maestro-free-6-show-sdk-agent-activity-through-pi/spec.md), with `builder` checks passing. `Owner`
180
+ acceptance is pending. Review follow-up moved the shared role executor to `src/delegation/executeRoleTask.ts` and separated event tracking in `trackAgentActivity.ts` from
181
+ UI updates in `createAgentProgress.ts`. The full check passes all 293 tests.
182
+
179
183
  ### Objective
180
184
 
181
185
  Show activity for SDK-based agent `runs` through Pi's standard `onUpdate` presentation, as decided in
@@ -192,7 +196,7 @@ Show activity for SDK-based agent `runs` through Pi's standard `onUpdate` presen
192
196
  ## MAESTRO-FREE-7: Remove the pi-subagents dependency
193
197
 
194
198
  Implementation follows the [approved migration specification](.specs/20261106-092601-feat-migrate-builder-verifier-and-simplifier-to-the-pi-sdk/spec.md). Code cleanup is
195
- included in this migration. Package declarations, lockfile entries, bundling, and `pi.subagents` metadata remain for the `owner` to remove manually.
199
+ included in this migration. Package declarations, lockfile entries, bundling, and `pi.subagents` metadata are removed.
196
200
 
197
201
  ### Objective
198
202
 
@@ -221,8 +225,7 @@ Linear marks this item Done, but its description still says final `owner` accept
221
225
  [specification](.specs/20261030-145712-automatically-exit-maestro-mode-at-workflow-completion/spec.md), and
222
226
  [original issue](https://linear.app/emilio-spatola/issue/MAESTRO-26/feat-exit-maestro-mode-automatically-at-workflow-completion).
223
227
 
224
- Completed entries include historical records imported from Linear and work explicitly accepted by the `owner`. Historical verification evidence alone does not establish
225
- final `owner` acceptance.
228
+ Completed entries include historical records imported from Linear.
226
229
 
227
230
  ## MAESTRO-FREE-1: Require Pi 1.5.0
228
231
 
@@ -261,7 +264,7 @@ The `pi-subagents` integration then in use still received a fixed deadline of 9,
261
264
  ### Implementation and evidence
262
265
 
263
266
  The standalone executor is implemented under `src/delegation/` against the [approved specification](.specs/20261105-212231-implement-internal-pi-sdk-delegation/spec.md).
264
- Execution orchestration, session lifecycle, and pure reply interpretation are separated into `executeTask`, `SubSessionManager`, and `readTaskResult`. Manual verification
267
+ Review follow-up consolidated execution orchestration and session lifecycle in `executeTask`; pure reply interpretation remains in `readTaskResult`. Manual verification
265
268
  on Pi 1.5.0 exercised fresh context, native and additional tools, MCP through tool search and codemode, sequential waiting, failure, cancellation, cleanup, and native
266
269
  session persistence. See [manual evidence](.specs/20261105-212231-implement-internal-pi-sdk-delegation/handoffs/verifier/V1.json). The three workflow roles still use
267
270
  `pi-subagents`. The `owner` confirmed completion. The caller's message is named `prompt`, execution is inlined in `executeTask`, and cancellation comments explain Node.js
@@ -427,6 +430,8 @@ and [merged PR #34](https://github.com/emilioSp/pi-maestro/pull/34).
427
430
 
428
431
  ## MAESTRO-13: Check subagent extensions at activation
429
432
 
433
+ Historical outcome: activation no longer requires the parent `pi-subagents` availability check after the SDK migration. See [changelog](CHANGELOG.md).
434
+
430
435
  Require a successful parent `pi-subagents` availability reply before activation.
431
436
 
432
437
  Implemented in `0.9.3`. Activation requires a matching successful version 1 `ping` reply from the parent `pi-subagents` extension within 2000 ms. A failed or invalid
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@emiliosp/pi-maestro",
3
- "version": "0.18.0",
3
+ "version": "0.18.2",
4
4
  "description": "A spec-driven multiagent development workflow for Pi.",
5
5
  "keywords": [
6
6
  "orchestration",
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Objective: Publish elapsed run time and the latest normal assistant text through Pi's progress UI.
3
+ * Used: During a delegated run, on assistant text events and once per second during silence.
4
+ */
5
+
6
+ import type { AgentToolUpdateCallback } from '@earendil-works/pi-coding-agent';
7
+
8
+ type FormatProgressInput = {
9
+ elapsedSeconds: number;
10
+ assistantText: string;
11
+ };
12
+
13
+ const formatProgress = ({ elapsedSeconds, assistantText }: FormatProgressInput): string => {
14
+ const elapsed = `${Math.floor(elapsedSeconds / 60)}m ${elapsedSeconds % 60}s`;
15
+ const recentText = assistantText ? `\n\n${assistantText}` : '';
16
+
17
+ return `Elapsed: ${elapsed}${recentText}`;
18
+ };
19
+
20
+ type CreateAgentProgressInput = {
21
+ onUpdate?: AgentToolUpdateCallback;
22
+ };
23
+
24
+ export const createAgentProgress = ({ onUpdate }: CreateAgentProgressInput) => {
25
+ const startedAt = performance.now();
26
+ let timer: ReturnType<typeof setInterval> | undefined;
27
+ let stopped = false;
28
+ let latestText = '';
29
+
30
+ const show = (assistantText: string = latestText): void => {
31
+ if (stopped) return;
32
+
33
+ latestText = assistantText;
34
+
35
+ onUpdate?.({
36
+ content: [
37
+ {
38
+ type: 'text',
39
+ text: formatProgress({
40
+ elapsedSeconds: Math.floor((performance.now() - startedAt) / 1000),
41
+ assistantText,
42
+ }),
43
+ },
44
+ ],
45
+ details: undefined,
46
+ });
47
+ };
48
+
49
+ const startShow = (): void => {
50
+ timer = setInterval(show, 1000);
51
+ show();
52
+ };
53
+
54
+ const stopShow = (): void => {
55
+ stopped = true;
56
+ clearInterval(timer);
57
+ };
58
+
59
+ return { startShow, show, stopShow };
60
+ };
@@ -1,12 +1,17 @@
1
1
  /**
2
- * Objective: Supply bundled role guidance and its handoff tool to the shared SDK executor.
2
+ * Objective: Run a bundled SDK role with its handoff tool and UI-only live activity.
3
3
  * Used: By the three foreground run tools after preparing their workflow phase.
4
4
  */
5
5
 
6
6
  import { readFile } from 'node:fs/promises';
7
7
 
8
- import type { ExtensionContext } from '@earendil-works/pi-coding-agent';
8
+ import type {
9
+ AgentSessionEvent,
10
+ AgentToolUpdateCallback,
11
+ ExtensionContext,
12
+ } from '@earendil-works/pi-coding-agent';
9
13
 
14
+ import { createAgentProgress } from '#src/delegation/createAgentProgress.ts';
10
15
  import { executeTask, type ExecuteTaskInput } from '#src/delegation/executeTask.ts';
11
16
  import { TaskExecutionError } from '#src/delegation/TaskExecutionError.ts';
12
17
  import { recordBuilderHandoffTool } from '#tools/child/record-builder-handoff.ts';
@@ -20,29 +25,59 @@ const HANDOFF_TOOL_BY_ROLE = {
20
25
  [WORKFLOW_ROLES.SIMPLIFIER]: recordSimplifierHandoffTool,
21
26
  } as const;
22
27
 
28
+ const readAssistantText = (event: AgentSessionEvent): string => {
29
+ if (event.type !== 'message_update' && event.type !== 'message_end') return '';
30
+
31
+ if (event.message.role !== 'assistant') return '';
32
+
33
+ return event.message.content
34
+ .filter((block) => block.type === 'text')
35
+ .map((block) => block.text)
36
+ .join('\n');
37
+ };
38
+
23
39
  type ExecuteRoleTaskInput = Pick<
24
40
  ExecuteTaskInput,
25
41
  'prompt' | 'cwd' | 'model' | 'thinkingLevel' | 'signal'
26
42
  > & {
27
43
  role: WorkflowRole;
28
44
  ui: ExtensionContext['ui'];
45
+ onUpdate?: AgentToolUpdateCallback;
29
46
  };
30
47
 
31
48
  export const executeRoleTask = async ({
32
49
  role,
33
50
  ui,
34
- ...input
51
+ onUpdate,
52
+ prompt,
53
+ cwd,
54
+ model,
55
+ thinkingLevel,
56
+ signal,
35
57
  }: ExecuteRoleTaskInput): Promise<void> => {
36
- const roleGuidance = await readFile(
37
- new URL(`../../../agents/${role}.md`, import.meta.url),
38
- 'utf8',
39
- );
58
+ const roleGuidance = await readFile(new URL(`../../agents/${role}.md`, import.meta.url), 'utf8');
59
+
60
+ const agentProgress = createAgentProgress({ onUpdate });
40
61
 
41
62
  try {
63
+ agentProgress.startShow();
64
+ signal?.addEventListener('abort', agentProgress.stopShow, { once: true });
65
+
66
+ if (signal?.aborted) agentProgress.stopShow();
67
+
42
68
  const result = await executeTask({
43
- ...input,
69
+ prompt,
70
+ cwd,
71
+ model,
72
+ thinkingLevel,
73
+ signal,
44
74
  roleGuidance,
45
75
  additionalTools: [HANDOFF_TOOL_BY_ROLE[role]],
76
+ onEvent: (event) => {
77
+ const assistantText = readAssistantText(event);
78
+
79
+ if (assistantText) agentProgress.show(assistantText);
80
+ },
46
81
  });
47
82
 
48
83
  // Publish before protocol validation so a missing handoff still exposes the transcript.
@@ -53,5 +88,8 @@ export const executeRoleTask = async ({
53
88
  }
54
89
 
55
90
  throw error;
91
+ } finally {
92
+ agentProgress.stopShow();
93
+ signal?.removeEventListener('abort', agentProgress.stopShow);
56
94
  }
57
95
  };
@@ -1,17 +1,34 @@
1
1
  /**
2
- * Objective: Execute one independent task in a fresh, persisted native Pi conversation.
2
+ * Objective: Execute one independent task and own its complete native Pi session lifecycle.
3
3
  * Used: By internal callers that await child executions sequentially, outside workflow routing.
4
4
  */
5
5
 
6
- import { readTaskResult } from '#src/delegation/readTaskResult.ts';
6
+ import { existsSync } from 'node:fs';
7
+ import { realpath } from 'node:fs/promises';
8
+ import { join } from 'node:path';
9
+
7
10
  import {
8
- SubSessionManager,
9
- type SubSessionManagerInput,
10
- } from '#src/delegation/SubSessionManager.ts';
11
+ type AgentSession,
12
+ type AgentSessionEvent,
13
+ createAgentSession,
14
+ type CreateAgentSessionOptions,
15
+ SessionManager,
16
+ type ToolDefinition,
17
+ } from '@earendil-works/pi-coding-agent';
18
+
19
+ import { prepareSubagentEnvironment } from '#src/delegation/prepareSubagentEnvironment.ts';
20
+ import { readTaskResult } from '#src/delegation/readTaskResult.ts';
11
21
  import { TaskExecutionError } from '#src/delegation/TaskExecutionError.ts';
12
22
 
13
- export type ExecuteTaskInput = SubSessionManagerInput & {
23
+ export type ExecuteTaskInput = {
14
24
  prompt: string;
25
+ roleGuidance: string;
26
+ cwd: string;
27
+ model: string;
28
+ thinkingLevel: NonNullable<CreateAgentSessionOptions['thinkingLevel']>;
29
+ additionalTools: ToolDefinition[];
30
+ signal?: AbortSignal;
31
+ onEvent?: (event: AgentSessionEvent) => void;
15
32
  };
16
33
 
17
34
  export type TaskExecutionResult = {
@@ -19,38 +36,96 @@ export type TaskExecutionResult = {
19
36
  sessionFile?: string;
20
37
  };
21
38
 
39
+ const getSavedFile = (sessionManager?: SessionManager): string | undefined => {
40
+ const path = sessionManager?.getSessionFile();
41
+
42
+ return path && existsSync(path) ? path : undefined;
43
+ };
44
+
45
+ type CloseSessionInput = {
46
+ session?: AgentSession;
47
+ abortPromise?: Promise<void>;
48
+ };
49
+
50
+ const closeSession = async ({ session, abortPromise }: CloseSessionInput): Promise<void> => {
51
+ if (!session) return;
52
+
53
+ try {
54
+ await session.abort();
55
+ await abortPromise;
56
+ } finally {
57
+ try {
58
+ // AgentSession.dispose() does not emit shutdown. Extensions such as MCP need it
59
+ // to release their connections before the session is disposed.
60
+ await session.extensionRunner.emit({ type: 'session_shutdown', reason: 'quit' });
61
+ } finally {
62
+ session.dispose();
63
+ }
64
+ }
65
+ };
66
+
22
67
  export const executeTask = async ({
23
68
  prompt,
24
69
  roleGuidance,
25
- cwd,
26
- model,
70
+ cwd: workingDirectory,
71
+ model: modelId,
27
72
  thinkingLevel,
28
73
  additionalTools,
29
74
  signal,
75
+ onEvent,
30
76
  }: ExecuteTaskInput): Promise<TaskExecutionResult> => {
31
- const subSessionManager = new SubSessionManager({
32
- roleGuidance,
33
- cwd,
34
- model,
35
- thinkingLevel,
36
- additionalTools,
37
- signal,
38
- });
77
+ let session: AgentSession | undefined;
78
+ let sessionManager: SessionManager | undefined;
79
+ let abortPromise: Promise<void> | undefined;
80
+ let unsubscribe: (() => void) | undefined;
81
+
82
+ const abort = (): void => {
83
+ abortPromise = session?.abort();
84
+ // Cleanup awaits this promise. Handle rejection immediately as well.
85
+ void abortPromise?.catch(() => {});
86
+ };
39
87
 
40
88
  try {
41
89
  try {
42
- const session = await subSessionManager.open();
90
+ const cwd = await realpath(workingDirectory);
91
+ const services = await prepareSubagentEnvironment({ cwd, roleGuidance });
92
+
93
+ const model = (await services.modelRuntime.getAvailable()).find(
94
+ (entry) => `${entry.provider}/${entry.id}` === modelId,
95
+ );
96
+
97
+ if (!model) throw new Error(`Model is not available: "${modelId}".`);
98
+
99
+ sessionManager = SessionManager.create(
100
+ cwd,
101
+ join(SessionManager.create(cwd).getSessionDir(), 'subagents'),
102
+ );
103
+
104
+ ({ session } = await createAgentSession({
105
+ ...services,
106
+ sessionManager,
107
+ model,
108
+ thinkingLevel,
109
+ customTools: additionalTools,
110
+ }));
111
+
112
+ if (onEvent) unsubscribe = session.subscribe(onEvent);
113
+ signal?.addEventListener('abort', abort, { once: true });
114
+
115
+ await session.bindExtensions({ mode: 'print' });
43
116
  await session.prompt(prompt, { expandPromptTemplates: false });
44
117
 
45
118
  const text = readTaskResult(
46
119
  session.messages.findLast((message) => message.role === 'assistant'),
47
120
  );
48
121
 
49
- return { text, sessionFile: subSessionManager.getSavedFile() };
122
+ return { text, sessionFile: getSavedFile(sessionManager) };
50
123
  } finally {
51
- await subSessionManager.close();
124
+ unsubscribe?.();
125
+ signal?.removeEventListener('abort', abort);
126
+ await closeSession({ session, abortPromise });
52
127
  }
53
128
  } catch (cause) {
54
- throw new TaskExecutionError({ cause, sessionFile: subSessionManager.getSavedFile() });
129
+ throw new TaskExecutionError({ cause, sessionFile: getSavedFile(sessionManager) });
55
130
  }
56
131
  };
@@ -51,12 +51,15 @@ const isMainExtension = (path: string): boolean => {
51
51
  }
52
52
  };
53
53
 
54
- type CreateChildServicesInput = {
54
+ type PrepareSubagentEnvironmentInput = {
55
55
  cwd: string;
56
56
  roleGuidance: string;
57
57
  };
58
58
 
59
- export const createChildServices = async ({ cwd, roleGuidance }: CreateChildServicesInput) => {
59
+ export const prepareSubagentEnvironment = async ({
60
+ cwd,
61
+ roleGuidance,
62
+ }: PrepareSubagentEnvironmentInput) => {
60
63
  const agentDir = getAgentDir();
61
64
  const settingsManager = SettingsManager.create(cwd, agentDir);
62
65
 
@@ -92,6 +95,8 @@ export const createChildServices = async ({ cwd, roleGuidance }: CreateChildServ
92
95
  agentDir,
93
96
  settingsManager,
94
97
  resourceLoaderOptions: {
98
+ // Disable automatic extension discovery; load only the filtered paths below
99
+ // and the built-in factories, so discovery cannot add Maestro back.
95
100
  noExtensions: true,
96
101
  additionalExtensionPaths,
97
102
  extensionFactories,
@@ -35,10 +35,8 @@ const normalizeSlug = (value: string): string => {
35
35
 
36
36
  type CreateSpecIdInput = {
37
37
  title: string;
38
- instant?: Temporal.Instant;
38
+ instant: Temporal.Instant;
39
39
  };
40
40
 
41
- export const createSpecId = ({
42
- title,
43
- instant = Temporal.Now.instant(),
44
- }: CreateSpecIdInput): string => `${formatUtcTimestamp(instant)}-${normalizeSlug(title)}`;
41
+ export const createSpecId = ({ title, instant }: CreateSpecIdInput): string =>
42
+ `${formatUtcTimestamp(instant)}-${normalizeSlug(title)}`;
@@ -44,7 +44,7 @@ export const createSpec = async ({
44
44
  throw new Error(`Workflow ${activeWorkflowSpecId} is already active.`);
45
45
  }
46
46
 
47
- const specId = createSpecId({ title, instant });
47
+ const specId = createSpecId({ title, instant: instant ?? Temporal.Now.instant() });
48
48
  const specPath = paths.getSpecPath(specId);
49
49
  const proposalFilePath = paths.getProposalFilePath(specId);
50
50
  const specFilePath = paths.getSpecFilePath(specId);
@@ -15,7 +15,7 @@ import type { MaestroConfig } from '#config/schema.ts';
15
15
  import { SPEC_ID_PATTERN } from '#ids/isValidSpecId.ts';
16
16
  import { refreshMaestroStatus } from '#maestro/status/refreshMaestroStatus.ts';
17
17
  import type { MaestroPaths } from '#MaestroPaths.ts';
18
- import { executeRoleTask } from '#tools/utils/executeRoleTask.ts';
18
+ import { executeRoleTask } from '#src/delegation/executeRoleTask.ts';
19
19
  import { resolveToolRunContext } from '#tools/utils/resolveToolRunContext.ts';
20
20
  import { prepareBuilderRun } from '#workflow/builder/prepareBuilderRun.ts';
21
21
  import { WORKFLOW_ROLES } from '#workflow/roles.ts';
@@ -155,7 +155,7 @@ export const registerRunBuilderTool = (pi: ExtensionAPI): void => {
155
155
  label: RUN_BUILDER_TOOL.LABEL,
156
156
  description: RUN_BUILDER_TOOL.DESCRIPTION,
157
157
  parameters: RunBuilderToolParameters,
158
- async execute(_toolCallId, { specId }, signal, _onUpdate, context) {
158
+ async execute(_toolCallId, { specId }, signal, onUpdate, context) {
159
159
  const { paths, config } = await resolveToolRunContext(context.cwd);
160
160
 
161
161
  const { projectRoot, state: builderRunningState } = await prepareBuilderRun({
@@ -174,6 +174,7 @@ export const registerRunBuilderTool = (pi: ExtensionAPI): void => {
174
174
  thinkingLevel: config.builder.thinking,
175
175
  signal,
176
176
  ui: context.ui,
177
+ onUpdate,
177
178
  });
178
179
 
179
180
  const result = await readBuilderResult({ paths, specId, config, builderRunningState });
@@ -10,7 +10,7 @@ import { readSimplifierHandoff } from '#artifacts/simplifier-handoff/readSimplif
10
10
  import { SPEC_ID_PATTERN } from '#ids/isValidSpecId.ts';
11
11
  import { refreshMaestroStatus } from '#maestro/status/refreshMaestroStatus.ts';
12
12
  import type { MaestroPaths } from '#MaestroPaths.ts';
13
- import { executeRoleTask } from '#tools/utils/executeRoleTask.ts';
13
+ import { executeRoleTask } from '#src/delegation/executeRoleTask.ts';
14
14
  import { resolveToolRunContext } from '#tools/utils/resolveToolRunContext.ts';
15
15
  import { WORKFLOW_ROLES } from '#workflow/roles.ts';
16
16
  import { readWorkflowState } from '#workflow/state/readWorkflowState.ts';
@@ -87,7 +87,7 @@ export const registerRunSimplifierTool = (pi: ExtensionAPI): void => {
87
87
  label: RUN_SIMPLIFIER_TOOL.LABEL,
88
88
  description: RUN_SIMPLIFIER_TOOL.DESCRIPTION,
89
89
  parameters: RunSimplifierToolParameters,
90
- async execute(_toolCallId, { specId }, signal, _onUpdate, context) {
90
+ async execute(_toolCallId, { specId }, signal, onUpdate, context) {
91
91
  const { paths, config, projectRoot } = await resolveToolRunContext(context.cwd);
92
92
 
93
93
  const workflowPath = paths.getWorkflowPath(specId);
@@ -136,6 +136,7 @@ export const registerRunSimplifierTool = (pi: ExtensionAPI): void => {
136
136
  thinkingLevel: config.simplifier.thinking,
137
137
  signal,
138
138
  ui: context.ui,
139
+ onUpdate,
139
140
  });
140
141
 
141
142
  const result = await readSimplifierResult({ paths, specId });
@@ -12,7 +12,7 @@ import type { MaestroConfig } from '#config/schema.ts';
12
12
  import { SPEC_ID_PATTERN } from '#ids/isValidSpecId.ts';
13
13
  import { refreshMaestroStatus } from '#maestro/status/refreshMaestroStatus.ts';
14
14
  import type { MaestroPaths } from '#MaestroPaths.ts';
15
- import { executeRoleTask } from '#tools/utils/executeRoleTask.ts';
15
+ import { executeRoleTask } from '#src/delegation/executeRoleTask.ts';
16
16
  import { resolveToolRunContext } from '#tools/utils/resolveToolRunContext.ts';
17
17
  import { WORKFLOW_ROLES } from '#workflow/roles.ts';
18
18
  import { readWorkflowState } from '#workflow/state/readWorkflowState.ts';
@@ -124,7 +124,7 @@ export const registerRunVerifierTool = (pi: ExtensionAPI): void => {
124
124
  label: RUN_VERIFIER_TOOL.LABEL,
125
125
  description: RUN_VERIFIER_TOOL.DESCRIPTION,
126
126
  parameters: RunVerifierToolParameters,
127
- async execute(_toolCallId, { specId }, signal, _onUpdate, context) {
127
+ async execute(_toolCallId, { specId }, signal, onUpdate, context) {
128
128
  const { paths, config } = await resolveToolRunContext(context.cwd);
129
129
 
130
130
  const run = await prepareVerifierRun({ config, paths, specId });
@@ -138,6 +138,7 @@ export const registerRunVerifierTool = (pi: ExtensionAPI): void => {
138
138
  thinkingLevel: config.verifier.thinking,
139
139
  signal,
140
140
  ui: context.ui,
141
+ onUpdate,
141
142
  });
142
143
 
143
144
  const result = await readVerifierResult({ paths, specId, config, runningState: run.state });
@@ -1,102 +0,0 @@
1
- /**
2
- * Objective: Own the native session, cancellation, and cleanup for one child execution.
3
- * Used: By the task executor while opening and closing a fresh SDK conversation.
4
- */
5
-
6
- import { existsSync } from 'node:fs';
7
- import { realpath } from 'node:fs/promises';
8
- import { join } from 'node:path';
9
-
10
- import {
11
- type AgentSession,
12
- createAgentSession,
13
- type CreateAgentSessionOptions,
14
- SessionManager,
15
- type ToolDefinition,
16
- } from '@earendil-works/pi-coding-agent';
17
-
18
- import { createChildServices } from '#src/delegation/createChildServices.ts';
19
-
20
- export type SubSessionManagerInput = {
21
- roleGuidance: string;
22
- cwd: string;
23
- model: string;
24
- thinkingLevel: NonNullable<CreateAgentSessionOptions['thinkingLevel']>;
25
- additionalTools: ToolDefinition[];
26
- signal?: AbortSignal;
27
- };
28
-
29
- export class SubSessionManager {
30
- private session?: AgentSession;
31
- private sessionManager?: SessionManager;
32
- private abortPromise?: Promise<void>;
33
-
34
- private readonly input: SubSessionManagerInput;
35
-
36
- constructor(input: SubSessionManagerInput) {
37
- this.input = input;
38
- }
39
-
40
- public getSavedFile = (): string | undefined => {
41
- const path = this.sessionManager?.getSessionFile();
42
-
43
- return path && existsSync(path) ? path : undefined;
44
- };
45
-
46
- private abort = (): void => {
47
- this.abortPromise = this.session?.abort();
48
- // The cleanup path awaits this promise. Handle rejection immediately as well.
49
- void this.abortPromise?.catch(() => {});
50
- };
51
-
52
- public open = async (): Promise<AgentSession> => {
53
- const cwd = await realpath(this.input.cwd);
54
- const services = await createChildServices({ cwd, roleGuidance: this.input.roleGuidance });
55
-
56
- const model = (await services.modelRuntime.getAvailable()).find(
57
- (entry) => `${entry.provider}/${entry.id}` === this.input.model,
58
- );
59
-
60
- if (!model) throw new Error(`Model is not available: "${this.input.model}".`);
61
-
62
- this.sessionManager = SessionManager.create(
63
- cwd,
64
- join(SessionManager.create(cwd).getSessionDir(), 'subagents'),
65
- );
66
-
67
- const { session } = await createAgentSession({
68
- ...services,
69
- sessionManager: this.sessionManager,
70
- model,
71
- thinkingLevel: this.input.thinkingLevel,
72
- customTools: this.input.additionalTools, // here child tool registration happens through SDK
73
- });
74
-
75
- this.session = session;
76
-
77
- this.input.signal?.addEventListener('abort', this.abort, { once: true });
78
-
79
- await this.session.bindExtensions({ mode: 'print' });
80
-
81
- return this.session;
82
- };
83
-
84
- public close = async (): Promise<void> => {
85
- this.input.signal?.removeEventListener('abort', this.abort);
86
-
87
- if (!this.session) return;
88
-
89
- try {
90
- await this.session.abort();
91
- await this.abortPromise;
92
- } finally {
93
- try {
94
- // AgentSession.dispose() does not emit shutdown. This is the native runtime's
95
- // shutdown sequence, including extensions such as MCP that own connections.
96
- await this.session.extensionRunner.emit({ type: 'session_shutdown', reason: 'quit' });
97
- } finally {
98
- this.session.dispose();
99
- }
100
- }
101
- };
102
- }