@emiliosp/pi-maestro 0.14.0 → 0.16.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 (38) hide show
  1. package/ADR.md +155 -0
  2. package/CHANGELOG.md +19 -0
  3. package/README.md +33 -19
  4. package/ROADMAP.md +176 -40
  5. package/agents/simplifier.md +2 -1
  6. package/agents/verifier.md +2 -1
  7. package/docs/configuration.md +49 -26
  8. package/docs/glossary.md +1 -1
  9. package/docs/subagent-integration.md +8 -6
  10. package/docs/workflow.md +32 -23
  11. package/extensions/maestro.ts +11 -13
  12. package/package.json +3 -3
  13. package/src/artifacts/builder-handoff/schema.ts +1 -2
  14. package/src/config/assertConfiguration.ts +0 -6
  15. package/src/config/defaults.ts +3 -5
  16. package/src/config/schema.ts +2 -14
  17. package/src/maestro/checks/assertEnvironment.ts +24 -9
  18. package/src/maestro/instructions/getMaestroInstructions.ts +19 -11
  19. package/src/tools/child/record-builder-handoff.ts +2 -6
  20. package/src/tools/child/record-simplifier-handoff.ts +2 -1
  21. package/src/tools/child/record-verifier-handoff.ts +2 -6
  22. package/src/tools/child/utils/resolveWorkflowContext.ts +2 -2
  23. package/src/tools/main/mark-spec-ready.ts +8 -3
  24. package/src/tools/main/resolve-escalations.ts +2 -1
  25. package/src/tools/main/resolve-findings.ts +9 -10
  26. package/src/tools/main/run-builder.ts +47 -15
  27. package/src/tools/main/run-simplifier.ts +3 -3
  28. package/src/tools/main/run-verifier.ts +19 -8
  29. package/src/tools/utils/pi-subagent-delegation.ts +3 -0
  30. package/src/workflow/builder/completeBuilderPass.ts +4 -0
  31. package/src/workflow/builder/prepareBuilderRun.ts +7 -1
  32. package/src/workflow/builder/resolveEscalations.ts +4 -0
  33. package/src/workflow/simplifier/completeSimplifierPass.ts +4 -0
  34. package/src/workflow/spec/markSpecReady.ts +4 -0
  35. package/src/workflow/transitions.ts +28 -7
  36. package/src/workflow/verifier/completeVerifierPass.ts +4 -0
  37. package/src/workflow/verifier/prepareVerifierRun.ts +7 -1
  38. package/src/workflow/verifier/resolveFindings.ts +4 -0
@@ -44,12 +44,13 @@ export const registerRecordSimplifierHandoffTool = (pi: ExtensionAPI): void => {
44
44
  async execute(_toolCallId, params, _signal, _onUpdate, context) {
45
45
  assertSimplifierSubmission(params);
46
46
 
47
- const { paths } = await resolveWorkflowContext({
47
+ const { paths, config } = await resolveWorkflowContext({
48
48
  cwd: context.cwd,
49
49
  specId: params.specId,
50
50
  });
51
51
 
52
52
  const completed = await completeSimplifierPass({
53
+ config,
53
54
  paths,
54
55
  specId: params.specId,
55
56
  handoff: { ...params, version: SIMPLIFIER_HANDOFF_VERSION },
@@ -53,7 +53,7 @@ export const registerRecordVerifierHandoffTool = (pi: ExtensionAPI): void => {
53
53
  async execute(_toolCallId, params, _signal, _onUpdate, context) {
54
54
  const { specId, ...submission } = params;
55
55
 
56
- const { paths } = await resolveWorkflowContext({
56
+ const { paths, config } = await resolveWorkflowContext({
57
57
  cwd: context.cwd,
58
58
  specId,
59
59
  });
@@ -69,11 +69,7 @@ export const registerRecordVerifierHandoffTool = (pi: ExtensionAPI): void => {
69
69
  notes: submission.notes,
70
70
  };
71
71
 
72
- const completed = await completeVerifierPass({
73
- paths,
74
- specId,
75
- handoff,
76
- });
72
+ const completed = await completeVerifierPass({ config, paths, specId, handoff });
77
73
 
78
74
  return {
79
75
  content: [
@@ -16,7 +16,7 @@ export const resolveWorkflowContext = async ({ cwd, specId }: ResolveWorkflowCon
16
16
  throw new Error(`Invalid spec ID: "${specId}".`);
17
17
  }
18
18
 
19
- const { paths, projectRoot } = await resolveToolRunContext(cwd);
19
+ const { paths, projectRoot, config } = await resolveToolRunContext(cwd);
20
20
 
21
- return { paths, specId, projectRoot };
21
+ return { paths, specId, projectRoot, config };
22
22
  };
@@ -10,12 +10,13 @@ import { SPEC_ID_PATTERN } from '#ids/isValidSpecId.ts';
10
10
  import maestroSessionState from '#maestro/session/MaestroSessionState.ts';
11
11
  import { resolveToolRunContext } from '#tools/utils/resolveToolRunContext.ts';
12
12
  import { markSpecReady } from '#workflow/spec/markSpecReady.ts';
13
+ import { WORKFLOW_PHASES } from '#workflow/state/schema.ts';
13
14
 
14
15
  export const MARK_SPEC_READY_TOOL = {
15
16
  NAME: 'maestro_mark_spec_ready',
16
17
  LABEL: 'Mark Spec Ready',
17
18
  DESCRIPTION:
18
- 'Approve proposal.md and spec.md together with design.md if present, only from drafting-spec after the owner replies GREEN FLAG. Freeze all present documents and move to ready-for-builder. Only the owner reply GREEN FLAG authorizes approval. After this transition succeeds, call maestro_run_builder in the foreground.',
19
+ 'Approve proposal.md and spec.md together with design.md if present, only from drafting-spec after the owner replies GREEN FLAG. Freeze all present documents and move to the configured next phase. Only the owner reply GREEN FLAG authorizes approval. Run the builder only if the returned phase is ready-for-builder; otherwise summarize the approved spec and skipped agents.',
19
20
  } as const;
20
21
 
21
22
  const MarkSpecReadyToolParameters = Type.Object(
@@ -32,9 +33,10 @@ export const registerMarkSpecReadyTool = (pi: ExtensionAPI): void => {
32
33
  description: MARK_SPEC_READY_TOOL.DESCRIPTION,
33
34
  parameters: MarkSpecReadyToolParameters,
34
35
  async execute(_toolCallId, { specId }, _signal, _onUpdate, context) {
35
- const { paths } = await resolveToolRunContext(context.cwd);
36
+ const { paths, config } = await resolveToolRunContext(context.cwd);
36
37
 
37
38
  const state = await markSpecReady({
39
+ config,
38
40
  paths,
39
41
  specId,
40
42
  activeWorkflowSpecId: maestroSessionState.getActiveSpecId(),
@@ -44,7 +46,10 @@ export const registerMarkSpecReadyTool = (pi: ExtensionAPI): void => {
44
46
  content: [
45
47
  {
46
48
  type: 'text',
47
- text: `Spec ${state.specId} is approved and ready-for-builder. Call maestro_run_builder now. No separate owner start request is needed.`,
49
+ text:
50
+ state.phase === WORKFLOW_PHASES.CANDIDATE_READY
51
+ ? `Spec ${state.specId} is approved. The workflow is candidate-ready. Builder, verifier, and simplifier skipped by configuration. No implementation or verification was performed.`
52
+ : `Spec ${state.specId} is approved and ready-for-builder. Call maestro_run_builder now. No separate owner start request is needed.`,
48
53
  },
49
54
  ],
50
55
  details: state,
@@ -47,9 +47,10 @@ export const registerResolveEscalationsTool = (pi: ExtensionAPI): void => {
47
47
  parameters: ResolveEscalationsToolParameters,
48
48
  async execute(_toolCallId, params, _signal, _onUpdate, context) {
49
49
  const { specId, specChangeRequired, decisions } = params;
50
- const { paths } = await resolveToolRunContext(context.cwd);
50
+ const { paths, config } = await resolveToolRunContext(context.cwd);
51
51
 
52
52
  const resolved = await resolveEscalations({
53
+ config,
53
54
  paths,
54
55
  specId,
55
56
  specChangeRequired,
@@ -3,7 +3,6 @@
3
3
  * Used: When the owner invokes the resolve-findings tool.
4
4
  */
5
5
 
6
- import { StringEnum } from '@earendil-works/pi-ai';
7
6
  import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
8
7
  import { Type } from 'typebox';
9
8
 
@@ -27,7 +26,7 @@ const FindingDecisionFields = {
27
26
  const RejectFindingDecisionSchema = Type.Object(
28
27
  {
29
28
  ...FindingDecisionFields,
30
- decision: StringEnum([FINDING_DECISIONS.REJECT] as const),
29
+ decision: Type.Enum([FINDING_DECISIONS.REJECT] as const, { type: 'string' }),
31
30
  reason: Type.String({ minLength: 1 }),
32
31
  },
33
32
  {
@@ -38,7 +37,7 @@ const RejectFindingDecisionSchema = Type.Object(
38
37
  const FixCodeFindingDecisionSchema = Type.Object(
39
38
  {
40
39
  ...FindingDecisionFields,
41
- decision: StringEnum([FINDING_DECISIONS.FIX_CODE] as const),
40
+ decision: Type.Enum([FINDING_DECISIONS.FIX_CODE] as const, { type: 'string' }),
42
41
  },
43
42
  {
44
43
  additionalProperties: false,
@@ -48,7 +47,7 @@ const FixCodeFindingDecisionSchema = Type.Object(
48
47
  const ReviseSpecFindingDecisionSchema = Type.Object(
49
48
  {
50
49
  ...FindingDecisionFields,
51
- decision: StringEnum([FINDING_DECISIONS.REVISE_SPEC] as const),
50
+ decision: Type.Enum([FINDING_DECISIONS.REVISE_SPEC] as const, { type: 'string' }),
52
51
  reason: Type.String({ minLength: 1, pattern: '\\S' }),
53
52
  specChange: Type.String({ minLength: 1, pattern: '\\S' }),
54
53
  },
@@ -96,6 +95,10 @@ const formatResolution = (details: FindingResolutionDetails): string => {
96
95
  return `${summary} The workflow is ready-for-builder. Run the builder separately.`;
97
96
  }
98
97
 
98
+ if (details.phase === WORKFLOW_PHASES.CANDIDATE_READY) {
99
+ return `${summary} The workflow is candidate-ready. Simplifier skipped by configuration.`;
100
+ }
101
+
99
102
  return `${summary} The workflow is simplifier-running. Call maestro_run_simplifier next with this specId, without another approval or verifier pass.`;
100
103
  };
101
104
 
@@ -106,13 +109,9 @@ export const registerResolveFindingsTool = (pi: ExtensionAPI): void => {
106
109
  description: RESOLVE_FINDINGS_TOOL.DESCRIPTION,
107
110
  parameters: ResolveFindingsToolParameters,
108
111
  async execute(_toolCallId, { specId, decisions }, _signal, _onUpdate, context) {
109
- const { paths } = await resolveToolRunContext(context.cwd);
112
+ const { paths, config } = await resolveToolRunContext(context.cwd);
110
113
 
111
- const resolved = await resolveFindings({
112
- paths,
113
- specId,
114
- decisions,
115
- });
114
+ const resolved = await resolveFindings({ config, paths, specId, decisions });
116
115
 
117
116
  const details: FindingResolutionDetails = {
118
117
  specId: resolved.state.specId,
@@ -14,19 +14,21 @@ import {
14
14
  BUILDER_HANDOFF_STATUSES,
15
15
  type BuilderHandoff,
16
16
  } from '#artifacts/builder-handoff/schema.ts';
17
- import { AGENTS } from '#config/schema.ts';
17
+ import { AGENTS, type MaestroConfig } from '#config/schema.ts';
18
18
  import { SPEC_ID_PATTERN } from '#ids/isValidSpecId.ts';
19
19
  import { refreshMaestroStatus } from '#maestro/status/refreshMaestroStatus.ts';
20
20
  import type { MaestroPaths } from '#MaestroPaths.ts';
21
21
  import {
22
22
  assertDelegationResponse,
23
+ SUBAGENT_TIMEOUT_MS,
23
24
  waitForDelegationResponse,
24
25
  } from '#tools/utils/pi-subagent-delegation.ts';
25
26
  import { resolveToolRunContext } from '#tools/utils/resolveToolRunContext.ts';
26
27
  import { prepareBuilderRun } from '#workflow/builder/prepareBuilderRun.ts';
27
28
  import { WORKFLOW_ROLES } from '#workflow/roles.ts';
28
29
  import { readWorkflowState } from '#workflow/state/readWorkflowState.ts';
29
- import { WORKFLOW_PHASES } from '#workflow/state/schema.ts';
30
+ import { WORKFLOW_PHASES, WORKFLOW_EVENTS, type WorkflowState } from '#workflow/state/schema.ts';
31
+ import { transitionWorkflow } from '#workflow/transitions.ts';
30
32
 
31
33
  export const RUN_BUILDER_TOOL = {
32
34
  NAME: 'maestro_run_builder',
@@ -42,13 +44,14 @@ const RunBuilderToolParameters = Type.Object(
42
44
  { additionalProperties: false },
43
45
  );
44
46
 
45
- const MILLISECONDS_PER_MINUTE = 60_000;
46
-
47
47
  type BuilderRunResult =
48
48
  | {
49
49
  outcome: typeof BUILDER_HANDOFF_STATUSES.DONE;
50
50
  specId: string;
51
- phase: typeof WORKFLOW_PHASES.READY_FOR_VERIFIER;
51
+ phase:
52
+ | typeof WORKFLOW_PHASES.READY_FOR_VERIFIER
53
+ | typeof WORKFLOW_PHASES.SIMPLIFIER_RUNNING
54
+ | typeof WORKFLOW_PHASES.CANDIDATE_READY;
52
55
  handoff: BuilderHandoff;
53
56
  }
54
57
  | {
@@ -59,11 +62,15 @@ type BuilderRunResult =
59
62
  };
60
63
 
61
64
  type ReadBuilderResultInput = {
65
+ config: MaestroConfig;
66
+ builderRunningState: WorkflowState;
62
67
  paths: MaestroPaths;
63
68
  specId: string;
64
69
  };
65
70
 
66
71
  const readBuilderResult = async ({
72
+ config,
73
+ builderRunningState,
67
74
  paths,
68
75
  specId,
69
76
  }: ReadBuilderResultInput): Promise<BuilderRunResult> => {
@@ -74,20 +81,32 @@ const readBuilderResult = async ({
74
81
  throw new Error(`Workflow spec ID mismatch: expected "${specId}", found "${state.specId}".`);
75
82
  }
76
83
 
84
+ const donePhase = transitionWorkflow({
85
+ state: builderRunningState,
86
+ event: WORKFLOW_EVENTS.BUILDER_DONE,
87
+ config,
88
+ }).phase;
89
+
77
90
  if (
78
91
  state.phase !== WORKFLOW_PHASES.READY_FOR_VERIFIER &&
92
+ state.phase !== WORKFLOW_PHASES.SIMPLIFIER_RUNNING &&
93
+ state.phase !== WORKFLOW_PHASES.CANDIDATE_READY &&
79
94
  state.phase !== WORKFLOW_PHASES.ESCALATION_DECISIONS
80
95
  ) {
81
96
  throw new Error(`Builder returned with unexpected workflow phase "${state.phase}".`);
82
97
  }
83
98
 
99
+ if (state.phase !== WORKFLOW_PHASES.ESCALATION_DECISIONS && state.phase !== donePhase) {
100
+ throw new Error(`Builder returned with unexpected workflow phase "${state.phase}".`);
101
+ }
102
+
84
103
  const handoff = await readBuilderHandoff({
85
104
  path: await paths.getActiveBuilderHandoffPath(specId),
86
105
  specId,
87
106
  });
88
107
 
89
108
  const expectedStatus =
90
- state.phase === WORKFLOW_PHASES.READY_FOR_VERIFIER
109
+ state.phase !== WORKFLOW_PHASES.ESCALATION_DECISIONS
91
110
  ? BUILDER_HANDOFF_STATUSES.DONE
92
111
  : BUILDER_HANDOFF_STATUSES.ESCALATION;
93
112
 
@@ -95,7 +114,7 @@ const readBuilderResult = async ({
95
114
  throw new Error(`Builder handoff status does not match workflow phase "${state.phase}".`);
96
115
  }
97
116
 
98
- if (state.phase === WORKFLOW_PHASES.READY_FOR_VERIFIER) {
117
+ if (state.phase !== WORKFLOW_PHASES.ESCALATION_DECISIONS) {
99
118
  return {
100
119
  outcome: BUILDER_HANDOFF_STATUSES.DONE,
101
120
  specId,
@@ -122,11 +141,19 @@ const readBuilderResult = async ({
122
141
  };
123
142
 
124
143
  const formatBuilderResult = (result: BuilderRunResult): string => {
125
- if (result.outcome === BUILDER_HANDOFF_STATUSES.DONE) {
126
- return `Builder completed spec ${result.specId}. The workflow is ready-for-verifier.`;
144
+ if (result.outcome !== BUILDER_HANDOFF_STATUSES.DONE) {
145
+ return `Builder submitted escalation questions for spec ${result.specId}. The workflow is waiting for an owner decision.`;
127
146
  }
128
147
 
129
- return `Builder submitted escalation questions for spec ${result.specId}. The workflow is waiting for an owner decision.`;
148
+ if (result.phase === WORKFLOW_PHASES.CANDIDATE_READY) {
149
+ return `Builder completed spec ${result.specId}. The workflow is candidate-ready. Verifier and simplifier skipped by configuration. No independent verification was performed.`;
150
+ }
151
+
152
+ if (result.phase === WORKFLOW_PHASES.SIMPLIFIER_RUNNING) {
153
+ return `Builder completed spec ${result.specId}. The workflow is simplifier-running. Verifier skipped by configuration. Call maestro_run_simplifier next.`;
154
+ }
155
+
156
+ return `Builder completed spec ${result.specId}. The workflow is ${result.phase}.`;
130
157
  };
131
158
 
132
159
  export const registerRunBuilderTool = (pi: ExtensionAPI): void => {
@@ -138,7 +165,12 @@ export const registerRunBuilderTool = (pi: ExtensionAPI): void => {
138
165
  async execute(toolCallId, { specId }, _signal, _onUpdate, context) {
139
166
  const { paths, config } = await resolveToolRunContext(context.cwd);
140
167
 
141
- const run = await prepareBuilderRun({ paths, specId });
168
+ const { projectRoot, state: builderRunningState } = await prepareBuilderRun({
169
+ config,
170
+ paths,
171
+ specId,
172
+ });
173
+
142
174
  await refreshMaestroStatus(context);
143
175
 
144
176
  const request: SubagentDelegationRequest = {
@@ -148,12 +180,12 @@ export const registerRunBuilderTool = (pi: ExtensionAPI): void => {
148
180
  // pi-subagents loads this name from agents/builder.md through package.json.
149
181
  // See docs/subagent-integration.md.
150
182
  agent: AGENTS.BUILDER,
151
- task: `Implement specId "${specId}" in the project directory "${run.projectRoot}".`,
183
+ task: `Implement specId "${specId}" in the project directory "${projectRoot}".`,
152
184
  context: 'fresh',
153
- cwd: run.projectRoot,
185
+ cwd: projectRoot,
154
186
  model: config.builder.model,
155
187
  thinking: config.builder.thinking,
156
- timeoutMs: config.builder.timeoutMinutes * MILLISECONDS_PER_MINUTE,
188
+ timeoutMs: SUBAGENT_TIMEOUT_MS,
157
189
  result: { kind: 'text' },
158
190
  };
159
191
 
@@ -163,7 +195,7 @@ export const registerRunBuilderTool = (pi: ExtensionAPI): void => {
163
195
  });
164
196
 
165
197
  assertDelegationResponse(response);
166
- const result = await readBuilderResult({ paths, specId });
198
+ const result = await readBuilderResult({ paths, specId, config, builderRunningState });
167
199
 
168
200
  return {
169
201
  content: [{ type: 'text', text: formatBuilderResult(result) }],
@@ -16,6 +16,7 @@ import { refreshMaestroStatus } from '#maestro/status/refreshMaestroStatus.ts';
16
16
  import type { MaestroPaths } from '#MaestroPaths.ts';
17
17
  import {
18
18
  assertDelegationResponse,
19
+ SUBAGENT_TIMEOUT_MS,
19
20
  waitForDelegationResponse,
20
21
  } from '#tools/utils/pi-subagent-delegation.ts';
21
22
  import { resolveToolRunContext } from '#tools/utils/resolveToolRunContext.ts';
@@ -37,8 +38,6 @@ const RunSimplifierToolParameters = Type.Object(
37
38
  { additionalProperties: false },
38
39
  );
39
40
 
40
- const MILLISECONDS_PER_MINUTE = 60_000;
41
-
42
41
  const GIT_SKIP_WARNING = 'Simplifier skipped: Git repository unavailable.';
43
42
 
44
43
  type GitReadinessInput = {
@@ -118,6 +117,7 @@ export const registerRunSimplifierTool = (pi: ExtensionAPI): void => {
118
117
 
119
118
  if (!(await isGitReady({ pi, projectRoot }))) {
120
119
  const nextState = transitionWorkflow({
120
+ config,
121
121
  state,
122
122
  event: WORKFLOW_EVENTS.SIMPLIFIER_SKIPPED,
123
123
  });
@@ -146,7 +146,7 @@ export const registerRunSimplifierTool = (pi: ExtensionAPI): void => {
146
146
  cwd: projectRoot,
147
147
  model: config.simplifier.model,
148
148
  thinking: config.simplifier.thinking,
149
- timeoutMs: config.simplifier.timeoutMinutes * MILLISECONDS_PER_MINUTE,
149
+ timeoutMs: SUBAGENT_TIMEOUT_MS,
150
150
  result: { kind: 'text' },
151
151
  };
152
152
 
@@ -11,18 +11,20 @@ import { Type } from 'typebox';
11
11
 
12
12
  import { readVerifierHandoff } from '#artifacts/verifier-handoff/readVerifierHandoff.ts';
13
13
  import type { VerifierHandoff } from '#artifacts/verifier-handoff/schema.ts';
14
- import { AGENTS } from '#config/schema.ts';
14
+ import { AGENTS, 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
18
  import {
19
19
  assertDelegationResponse,
20
+ SUBAGENT_TIMEOUT_MS,
20
21
  waitForDelegationResponse,
21
22
  } from '#tools/utils/pi-subagent-delegation.ts';
22
23
  import { resolveToolRunContext } from '#tools/utils/resolveToolRunContext.ts';
23
24
  import { WORKFLOW_ROLES } from '#workflow/roles.ts';
24
25
  import { readWorkflowState } from '#workflow/state/readWorkflowState.ts';
25
- import { WORKFLOW_PHASES } from '#workflow/state/schema.ts';
26
+ import { WORKFLOW_PHASES, WORKFLOW_EVENTS, type WorkflowState } from '#workflow/state/schema.ts';
27
+ import { transitionWorkflow } from '#workflow/transitions.ts';
26
28
  import { prepareVerifierRun } from '#workflow/verifier/prepareVerifierRun.ts';
27
29
 
28
30
  export const RUN_VERIFIER_TOOL = {
@@ -39,9 +41,8 @@ const RunVerifierToolParameters = Type.Object(
39
41
  { additionalProperties: false },
40
42
  );
41
43
 
42
- const MILLISECONDS_PER_MINUTE = 60_000;
43
-
44
44
  type VerifierTerminalPhase =
45
+ | typeof WORKFLOW_PHASES.CANDIDATE_READY
45
46
  | typeof WORKFLOW_PHASES.SIMPLIFIER_RUNNING
46
47
  | typeof WORKFLOW_PHASES.FINDING_DECISIONS;
47
48
 
@@ -53,11 +54,15 @@ type VerifierRunResult = {
53
54
  };
54
55
 
55
56
  type ReadVerifierResultInput = {
57
+ config: MaestroConfig;
58
+ runningState: WorkflowState;
56
59
  paths: MaestroPaths;
57
60
  specId: string;
58
61
  };
59
62
 
60
63
  const readVerifierResult = async ({
64
+ config,
65
+ runningState,
61
66
  paths,
62
67
  specId,
63
68
  }: ReadVerifierResultInput): Promise<VerifierRunResult> => {
@@ -74,6 +79,7 @@ const readVerifierResult = async ({
74
79
 
75
80
  if (
76
81
  state.phase !== WORKFLOW_PHASES.SIMPLIFIER_RUNNING &&
82
+ state.phase !== WORKFLOW_PHASES.CANDIDATE_READY &&
77
83
  state.phase !== WORKFLOW_PHASES.FINDING_DECISIONS
78
84
  ) {
79
85
  throw new Error(`Verifier returned with unexpected workflow phase "${state.phase}".`);
@@ -87,7 +93,8 @@ const readVerifierResult = async ({
87
93
  const expectedPhase =
88
94
  handoff.findings.length > 0
89
95
  ? WORKFLOW_PHASES.FINDING_DECISIONS
90
- : WORKFLOW_PHASES.SIMPLIFIER_RUNNING;
96
+ : transitionWorkflow({ state: runningState, event: WORKFLOW_EVENTS.VERIFIER_DONE, config })
97
+ .phase;
91
98
 
92
99
  if (state.phase !== expectedPhase) {
93
100
  throw new Error(`Verifier handoff findings do not match workflow phase "${state.phase}".`);
@@ -107,6 +114,10 @@ const readVerifierResult = async ({
107
114
  };
108
115
 
109
116
  const formatVerifierResult = (result: VerifierRunResult): string => {
117
+ if (result.outcome === WORKFLOW_PHASES.CANDIDATE_READY) {
118
+ return `Verifier completed spec ${result.specId}. The workflow is candidate-ready. Simplifier skipped by configuration.`;
119
+ }
120
+
110
121
  if (result.outcome === WORKFLOW_PHASES.SIMPLIFIER_RUNNING) {
111
122
  return `Verifier completed spec ${result.specId}. The workflow is simplifier-running. Call maestro_run_simplifier next with this specId, without another approval or verifier pass.`;
112
123
  }
@@ -123,7 +134,7 @@ export const registerRunVerifierTool = (pi: ExtensionAPI): void => {
123
134
  async execute(toolCallId, { specId }, _signal, _onUpdate, context) {
124
135
  const { paths, config } = await resolveToolRunContext(context.cwd);
125
136
 
126
- const run = await prepareVerifierRun({ paths, specId });
137
+ const run = await prepareVerifierRun({ config, paths, specId });
127
138
  await refreshMaestroStatus(context);
128
139
 
129
140
  const request: SubagentDelegationRequest = {
@@ -137,7 +148,7 @@ export const registerRunVerifierTool = (pi: ExtensionAPI): void => {
137
148
  cwd: run.projectRoot,
138
149
  model: config.verifier.model,
139
150
  thinking: config.verifier.thinking,
140
- timeoutMs: config.verifier.timeoutMinutes * MILLISECONDS_PER_MINUTE,
151
+ timeoutMs: SUBAGENT_TIMEOUT_MS,
141
152
  result: { kind: 'text' },
142
153
  };
143
154
 
@@ -147,7 +158,7 @@ export const registerRunVerifierTool = (pi: ExtensionAPI): void => {
147
158
  });
148
159
 
149
160
  assertDelegationResponse(response);
150
- const result = await readVerifierResult({ paths, specId });
161
+ const result = await readVerifierResult({ paths, specId, config, runningState: run.state });
151
162
 
152
163
  return {
153
164
  content: [{ type: 'text', text: formatVerifierResult(result) }],
@@ -13,6 +13,9 @@ import {
13
13
 
14
14
  // pi-subagents exports SubagentDelegationStatus as a type, not runtime values.
15
15
  // TypeScript erases the type, so the adapter needs these constants for comparisons.
16
+ // Fixed deadline passed to pi-subagents in place of the removed per-agent timeout configuration.
17
+ export const SUBAGENT_TIMEOUT_MS = 9_999 * 60_000;
18
+
16
19
  export const DELEGATION_STATUSES = {
17
20
  COMPLETED: 'completed',
18
21
  FAILED: 'failed',
@@ -15,6 +15,7 @@ import {
15
15
  BuilderHandoffSubmissionSchema,
16
16
  } from '#artifacts/builder-handoff/schema.ts';
17
17
  import { writeBuilderHandoff } from '#artifacts/builder-handoff/writeBuilderHandoff.ts';
18
+ import type { MaestroConfig } from '#config/schema.ts';
18
19
  import type { MaestroPaths } from '#MaestroPaths.ts';
19
20
  import { getWorkflowEventByBuilderHandoff } from '#workflow/builder/utils/getWorkflowEventByBuilderHandoff.ts';
20
21
  import { readWorkflowState } from '#workflow/state/readWorkflowState.ts';
@@ -39,12 +40,14 @@ function assertBuilderHandoffSubmission(
39
40
  }
40
41
 
41
42
  type CompleteBuilderPassInput = {
43
+ config: MaestroConfig;
42
44
  paths: MaestroPaths;
43
45
  specId: string;
44
46
  handoff: BuilderHandoffSubmissionInput;
45
47
  };
46
48
 
47
49
  export const completeBuilderPass = async ({
50
+ config,
48
51
  paths,
49
52
  specId,
50
53
  handoff: draftHandoff,
@@ -84,6 +87,7 @@ export const completeBuilderPass = async ({
84
87
  });
85
88
 
86
89
  const nextState = transitionWorkflow({
90
+ config,
87
91
  state: currentState,
88
92
  event: getWorkflowEventByBuilderHandoff(handoff),
89
93
  });
@@ -3,15 +3,17 @@
3
3
  * Used: Before Maestro runs a builder pass.
4
4
  */
5
5
 
6
+ import type { MaestroConfig } from '#config/schema.ts';
6
7
  import maestroSessionState from '#maestro/session/MaestroSessionState.ts';
7
8
  import type { MaestroPaths } from '#MaestroPaths.ts';
8
9
  import { pathExists } from '#utils/path-exists.ts';
9
10
  import { readWorkflowState } from '#workflow/state/readWorkflowState.ts';
10
- import { WORKFLOW_EVENTS, WORKFLOW_PHASES } from '#workflow/state/schema.ts';
11
+ import { WORKFLOW_EVENTS, WORKFLOW_PHASES, type WorkflowState } from '#workflow/state/schema.ts';
11
12
  import { writeWorkflowState } from '#workflow/state/writeWorkflowState.ts';
12
13
  import { transitionWorkflow } from '#workflow/transitions.ts';
13
14
 
14
15
  export type BuilderRun = {
16
+ state: WorkflowState;
15
17
  specId: string;
16
18
  projectRoot: string;
17
19
  };
@@ -38,11 +40,13 @@ async function assertBuilderRunBase({ paths, specId }: AssertBuilderRunBaseInput
38
40
  }
39
41
 
40
42
  type PrepareBuilderRunInput = {
43
+ config: MaestroConfig;
41
44
  paths: MaestroPaths;
42
45
  specId: string;
43
46
  };
44
47
 
45
48
  export const prepareBuilderRun = async ({
49
+ config,
46
50
  paths,
47
51
  specId,
48
52
  }: PrepareBuilderRunInput): Promise<BuilderRun> => {
@@ -60,6 +64,7 @@ export const prepareBuilderRun = async ({
60
64
  const currentState = await readWorkflowState(workflowPath);
61
65
 
62
66
  const nextState = transitionWorkflow({
67
+ config,
63
68
  state: currentState,
64
69
  event: WORKFLOW_EVENTS.RUN_BUILDER,
65
70
  });
@@ -70,6 +75,7 @@ export const prepareBuilderRun = async ({
70
75
  });
71
76
 
72
77
  return {
78
+ state: nextState,
73
79
  specId,
74
80
  projectRoot: paths.getProjectRoot(),
75
81
  };
@@ -10,6 +10,7 @@ import {
10
10
  type EscalationResolution,
11
11
  } from '#artifacts/builder-handoff/schema.ts';
12
12
  import { writeBuilderHandoff } from '#artifacts/builder-handoff/writeBuilderHandoff.ts';
13
+ import type { MaestroConfig } from '#config/schema.ts';
13
14
  import type { MaestroPaths } from '#MaestroPaths.ts';
14
15
  import { getWorkflowEventBySpecChangeRequired } from '#workflow/builder/utils/getWorkflowEventBySpecChangeRequired.ts';
15
16
  import { readWorkflowState } from '#workflow/state/readWorkflowState.ts';
@@ -57,6 +58,7 @@ const applyEscalationDecisions = ({ escalations, decisions }: ApplyEscalationDec
57
58
  };
58
59
 
59
60
  type ResolveEscalationsInput = {
61
+ config: MaestroConfig;
60
62
  paths: MaestroPaths;
61
63
  specId: string;
62
64
  specChangeRequired: boolean;
@@ -64,6 +66,7 @@ type ResolveEscalationsInput = {
64
66
  };
65
67
 
66
68
  export const resolveEscalations = async ({
69
+ config,
67
70
  paths,
68
71
  specId,
69
72
  specChangeRequired,
@@ -95,6 +98,7 @@ export const resolveEscalations = async ({
95
98
  const nextHandoff = { ...handoff, escalations };
96
99
 
97
100
  const nextState = transitionWorkflow({
101
+ config,
98
102
  state,
99
103
  event: getWorkflowEventBySpecChangeRequired(specChangeRequired),
100
104
  });
@@ -7,6 +7,7 @@ import { mkdir } from 'node:fs/promises';
7
7
  import { dirname } from 'node:path';
8
8
 
9
9
  import { assertSimplifierHandoff } from '#artifacts/simplifier-handoff/assertSimplifierHandoff.ts';
10
+ import type { MaestroConfig } from '#config/schema.ts';
10
11
  import type { MaestroPaths } from '#MaestroPaths.ts';
11
12
  import { writeJson } from '#utils/write-json.ts';
12
13
  import { readWorkflowState } from '#workflow/state/readWorkflowState.ts';
@@ -15,12 +16,14 @@ import { writeWorkflowState } from '#workflow/state/writeWorkflowState.ts';
15
16
  import { transitionWorkflow } from '#workflow/transitions.ts';
16
17
 
17
18
  type CompleteSimplifierPassInput = {
19
+ config: MaestroConfig;
18
20
  paths: MaestroPaths;
19
21
  specId: string;
20
22
  handoff: unknown;
21
23
  };
22
24
 
23
25
  export const completeSimplifierPass = async ({
26
+ config,
24
27
  paths,
25
28
  specId,
26
29
  handoff,
@@ -46,6 +49,7 @@ export const completeSimplifierPass = async ({
46
49
  await writeJson({ path: handoffPath, data: input.handoff });
47
50
 
48
51
  const nextState = transitionWorkflow({
52
+ config,
49
53
  state,
50
54
  event: WORKFLOW_EVENTS.SIMPLIFIER_DONE,
51
55
  });
@@ -5,6 +5,7 @@
5
5
 
6
6
  import { readFile } from 'node:fs/promises';
7
7
 
8
+ import type { MaestroConfig } from '#config/schema.ts';
8
9
  import type { MaestroPaths } from '#MaestroPaths.ts';
9
10
  import { pathExists } from '#utils/path-exists.ts';
10
11
  import { readWorkflowState } from '#workflow/state/readWorkflowState.ts';
@@ -22,12 +23,14 @@ function assertAcceptanceCriterionOrdering(markdown: string): void {
22
23
  }
23
24
 
24
25
  type MarkSpecReadyInput = {
26
+ config: MaestroConfig;
25
27
  paths: MaestroPaths;
26
28
  specId: string;
27
29
  activeWorkflowSpecId: string | null;
28
30
  };
29
31
 
30
32
  export const markSpecReady = async ({
33
+ config,
31
34
  paths,
32
35
  specId,
33
36
  activeWorkflowSpecId,
@@ -62,6 +65,7 @@ export const markSpecReady = async ({
62
65
  }
63
66
 
64
67
  const state = transitionWorkflow({
68
+ config,
65
69
  state: current,
66
70
  event: WORKFLOW_EVENTS.MARK_SPEC_READY,
67
71
  });