@emiliosp/pi-maestro 0.6.3 → 0.7.1

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 (34) hide show
  1. package/README.md +17 -5
  2. package/agents/builder.md +6 -7
  3. package/agents/verifier.md +3 -3
  4. package/changelog.md +85 -0
  5. package/docs/glossary.md +69 -0
  6. package/docs/subagent-integration.md +1 -1
  7. package/docs/workflow.md +55 -23
  8. package/extensions/maestro-subagent.ts +0 -2
  9. package/extensions/maestro.ts +5 -5
  10. package/mission.md +7 -0
  11. package/package.json +7 -4
  12. package/roadmap.md +85 -0
  13. package/src/MaestroPaths.ts +0 -32
  14. package/src/artifacts/builder-handoff/assertBuilderHandoff.ts +29 -0
  15. package/src/artifacts/builder-handoff/schema.ts +87 -23
  16. package/src/maestro/instructions/getMaestroInstructions.ts +7 -7
  17. package/src/specs/create.ts +0 -4
  18. package/src/tools/child/record-builder-handoff.ts +14 -16
  19. package/src/tools/main/resolve-escalations.ts +72 -0
  20. package/src/tools/main/run-builder.ts +14 -10
  21. package/src/workflow/builder/completeBuilderPass.ts +20 -32
  22. package/src/workflow/escalation/resolveEscalations.ts +100 -0
  23. package/tech-stack.md +14 -0
  24. package/src/artifacts/escalation/assertEscalation.ts +0 -66
  25. package/src/artifacts/escalation/createEscalation.ts +0 -52
  26. package/src/artifacts/escalation/getNextEscalationId.ts +0 -23
  27. package/src/artifacts/escalation/readEscalation.ts +0 -40
  28. package/src/artifacts/escalation/readEscalationHistory.ts +0 -44
  29. package/src/artifacts/escalation/resolveEscalation.ts +0 -43
  30. package/src/artifacts/escalation/schema.ts +0 -73
  31. package/src/tools/child/open-escalation.ts +0 -71
  32. package/src/tools/main/resolve-escalation.ts +0 -65
  33. package/src/workflow/escalation/openBuilderEscalation.ts +0 -82
  34. package/src/workflow/escalation/resolveBuilderEscalation.ts +0 -118
@@ -15,7 +15,6 @@ const PATHS = {
15
15
  HANDOFFS_PATH: 'handoffs',
16
16
  BUILDER_HANDOFFS_PATH: 'handoffs/builder',
17
17
  VERIFIER_HANDOFFS_PATH: 'handoffs/verifier',
18
- ESCALATIONS_PATH: 'handoffs/escalations',
19
18
  PROTOTYPES_PATH: 'prototypes',
20
19
  } as const;
21
20
 
@@ -55,11 +54,6 @@ export type MaestroPathsInput = {
55
54
  config: MaestroConfig;
56
55
  };
57
56
 
58
- type EscalationPathInput = {
59
- specId: string;
60
- escalationNumber: number;
61
- };
62
-
63
57
  type HandoffPathInput = {
64
58
  specId: string;
65
59
  handoffPassNumber: number;
@@ -213,32 +207,6 @@ export class MaestroPaths {
213
207
  });
214
208
  }
215
209
 
216
- public getEscalationsPath(specId: string): string {
217
- assertSpecId(specId);
218
-
219
- return resolve(
220
- this.projectRoot,
221
- this.specDirectoryFromRoot,
222
- specId,
223
- PATHS.ESCALATIONS_PATH,
224
- );
225
- }
226
-
227
- public getEscalationPath({
228
- specId,
229
- escalationNumber,
230
- }: EscalationPathInput): string {
231
- assertArtifactNumber(escalationNumber);
232
- assertSpecId(specId);
233
-
234
- return resolve(
235
- this.projectRoot,
236
- this.specDirectoryFromRoot,
237
- specId,
238
- `${PATHS.ESCALATIONS_PATH}/E${escalationNumber}.json`,
239
- );
240
- }
241
-
242
210
  public getPrototypesPath(specId: string): string {
243
211
  assertSpecId(specId);
244
212
 
@@ -80,6 +80,35 @@ export function assertBuilderHandoff(
80
80
  );
81
81
  }
82
82
 
83
+ if (handoff.status === BUILDER_HANDOFF_STATUSES.ESCALATION) {
84
+ for (const [index, escalation] of handoff.escalations.entries()) {
85
+ if (escalation.id !== `E${index + 1}`) {
86
+ throw new Error('Escalation IDs must be sequential in array order.');
87
+ }
88
+
89
+ const optionIds = new Set(escalation.options.map(({ id }) => id));
90
+
91
+ if (optionIds.size !== escalation.options.length) {
92
+ throw new Error('Escalation option IDs must be unique.');
93
+ }
94
+
95
+ if (
96
+ escalation.recommendation !== null &&
97
+ !optionIds.has(escalation.recommendation.optionId)
98
+ ) {
99
+ throw new Error('Escalation recommendation references unknown option.');
100
+ }
101
+
102
+ if (
103
+ escalation.resolution !== null &&
104
+ escalation.resolution.selectedOptionId !== null &&
105
+ !optionIds.has(escalation.resolution.selectedOptionId)
106
+ ) {
107
+ throw new Error('Escalation resolution references unknown option.');
108
+ }
109
+ }
110
+ }
111
+
83
112
  if (!isValidSpecId(specId)) {
84
113
  throw new Error(`Invalid expected builder handoff spec ID: "${specId}".`);
85
114
  }
@@ -37,6 +37,10 @@ export const BuilderAcceptanceCriterionSchema = Type.Object(
37
37
  { additionalProperties: false },
38
38
  );
39
39
 
40
+ export type BuilderAcceptanceCriterion = Static<
41
+ typeof BuilderAcceptanceCriterionSchema
42
+ >;
43
+
40
44
  export const BuilderHandoffFailureSchema = Type.Object(
41
45
  { reason: Type.String({ minLength: 1 }) },
42
46
  { additionalProperties: false },
@@ -55,10 +59,77 @@ const BuilderHandoffFields = {
55
59
  ...BuilderHandoffContentFields,
56
60
  };
57
61
 
62
+ export const ESCALATION_ID_PATTERN = /^E([1-9]\d*)$/;
63
+
64
+ export const EscalationOptionSchema = Type.Object(
65
+ {
66
+ id: Type.String({ minLength: 1 }),
67
+ description: Type.String({ minLength: 1 }),
68
+ consequences: Type.String({ minLength: 1 }),
69
+ nextStep: Type.String({ minLength: 1 }),
70
+ },
71
+ { additionalProperties: false },
72
+ );
73
+
74
+ export const EscalationRecommendationSchema = Type.Object(
75
+ {
76
+ optionId: Type.String({ minLength: 1 }),
77
+ reason: Type.String({ minLength: 1 }),
78
+ },
79
+ { additionalProperties: false },
80
+ );
81
+
82
+ export const EscalationResolutionSchema = Type.Object(
83
+ {
84
+ selectedOptionId: Type.Union([Type.String({ minLength: 1 }), Type.Null()]),
85
+ decision: Type.String({ minLength: 1, pattern: '\\S' }),
86
+ reason: Type.String({ minLength: 1, pattern: '\\S' }),
87
+ },
88
+ { additionalProperties: false },
89
+ );
90
+
91
+ const EscalationContentFields = {
92
+ question: Type.String({ minLength: 1 }),
93
+ context: Type.String({ minLength: 1 }),
94
+ options: Type.Array(EscalationOptionSchema, { minItems: 1 }),
95
+ notes: Type.Array(Type.String()),
96
+ };
97
+
98
+ export const EscalationSchema = Type.Object(
99
+ {
100
+ id: Type.String({ pattern: ESCALATION_ID_PATTERN.source }),
101
+ ...EscalationContentFields,
102
+ recommendation: Type.Union([EscalationRecommendationSchema, Type.Null()]),
103
+ resolution: Type.Union([EscalationResolutionSchema, Type.Null()]),
104
+ },
105
+ { additionalProperties: false },
106
+ );
107
+
108
+ const EscalationSubmissionSchema = Type.Object(
109
+ { ...EscalationSchema.properties, resolution: Type.Null() },
110
+ { additionalProperties: false },
111
+ );
112
+
113
+ export type EscalationOption = Static<typeof EscalationOptionSchema>;
114
+
115
+ export type EscalationResolution = Static<typeof EscalationResolutionSchema>;
116
+
117
+ export type Escalation = Static<typeof EscalationSchema>;
118
+
58
119
  export const BuilderDoneHandoffSchema = Type.Object(
59
120
  {
60
121
  ...BuilderHandoffFields,
61
122
  status: Type.Literal(BUILDER_HANDOFF_STATUSES.DONE),
123
+ escalations: Type.Array(EscalationSchema, { maxItems: 0 }),
124
+ },
125
+ { additionalProperties: false },
126
+ );
127
+
128
+ export const BuilderEscalationHandoffSchema = Type.Object(
129
+ {
130
+ ...BuilderHandoffFields,
131
+ status: Type.Literal(BUILDER_HANDOFF_STATUSES.ESCALATION),
132
+ escalations: Type.Array(EscalationSchema, { minItems: 1 }),
62
133
  },
63
134
  { additionalProperties: false },
64
135
  );
@@ -74,18 +145,31 @@ export const BuilderFailedHandoffSchema = Type.Object(
74
145
 
75
146
  export const BuilderHandoffSchema = Type.Union([
76
147
  BuilderDoneHandoffSchema,
148
+ BuilderEscalationHandoffSchema,
77
149
  BuilderFailedHandoffSchema,
78
150
  ]);
79
151
 
80
- const BuilderDoneHandoffSubmissionSchema = Type.Object(
152
+ export type BuilderHandoff = Static<typeof BuilderHandoffSchema>;
153
+
154
+ export const BuilderDoneHandoffSubmissionSchema = Type.Object(
81
155
  {
82
156
  status: Type.Literal(BUILDER_HANDOFF_STATUSES.DONE),
83
157
  ...BuilderHandoffContentFields,
158
+ escalations: Type.Array(EscalationSchema, { maxItems: 0 }),
159
+ },
160
+ { additionalProperties: false },
161
+ );
162
+
163
+ export const BuilderEscalationHandoffSubmissionSchema = Type.Object(
164
+ {
165
+ status: Type.Literal(BUILDER_HANDOFF_STATUSES.ESCALATION),
166
+ ...BuilderHandoffContentFields,
167
+ escalations: Type.Array(EscalationSubmissionSchema, { minItems: 1 }),
84
168
  },
85
169
  { additionalProperties: false },
86
170
  );
87
171
 
88
- const BuilderFailedHandoffSubmissionSchema = Type.Object(
172
+ export const BuilderFailedHandoffSubmissionSchema = Type.Object(
89
173
  {
90
174
  status: Type.Literal(BUILDER_HANDOFF_STATUSES.FAILED),
91
175
  ...BuilderHandoffContentFields,
@@ -96,30 +180,10 @@ const BuilderFailedHandoffSubmissionSchema = Type.Object(
96
180
 
97
181
  export const BuilderHandoffSubmissionSchema = Type.Union([
98
182
  BuilderDoneHandoffSubmissionSchema,
183
+ BuilderEscalationHandoffSubmissionSchema,
99
184
  BuilderFailedHandoffSubmissionSchema,
100
185
  ]);
101
186
 
102
- export type BuilderAcceptanceCriterion = Static<
103
- typeof BuilderAcceptanceCriterionSchema
104
- >;
105
-
106
- export type BuilderHandoff = Static<typeof BuilderHandoffSchema>;
107
-
108
187
  export type BuilderHandoffSubmissionInput = Static<
109
188
  typeof BuilderHandoffSubmissionSchema
110
189
  >;
111
-
112
- export type BuilderHandoffSubmission =
113
- | {
114
- status: typeof BUILDER_HANDOFF_STATUSES.DONE;
115
- summary: string;
116
- acceptanceCriteria: BuilderAcceptanceCriterion[];
117
- notes: string[];
118
- }
119
- | {
120
- status: typeof BUILDER_HANDOFF_STATUSES.FAILED;
121
- summary: string;
122
- acceptanceCriteria: BuilderAcceptanceCriterion[];
123
- failure: { reason: string };
124
- notes: string[];
125
- };
@@ -16,7 +16,7 @@ The project root is the canonical Pi working directory, resolved with realpath.
16
16
  Load .pi/maestro.json only from this root. Resolve spec and product paths against this root. Children run in this directory.
17
17
  Do not add worktree checks, activation gates, warnings, or configuration changes: compatible pi-subagents configuration is the owner's responsibility.
18
18
  Use generic tools for project inspection. Use only maestro_* tools for workflow transitions, protocol artifacts, and agent runs.
19
- Do not edit workflow.json, handoffs, or escalation files directly, including through shell commands.
19
+ Do not edit workflow.json or handoffs directly, including through shell commands.
20
20
  The spec, prototype, and experiment permissions below are the only exceptions for file changes.
21
21
 
22
22
  Use the specId and paths returned by maestro_create_spec. Read workflow.json before selecting the next action. Its validated phase is the source of truth.
@@ -82,7 +82,7 @@ The tool saves builder-running before launch. The builder records its result bef
82
82
  Read the returned artifact and follow its outcome:
83
83
 
84
84
  1. done: The phase is ready-for-verifier. Call maestro_run_verifier with the active specId.
85
- 2. escalation: The phase is escalation-decision. Present the question, evidence, options, consequences, and next steps to the owner.
85
+ 2. escalation: The phase is escalation-decision. Present all questions from the active builder handoff, with evidence, options, consequences, and next steps.
86
86
  3. failed: The phase is builder-failed. Report the failure and stop. There is no builder retry or spec revision from this phase.
87
87
 
88
88
  A significant discovery needs an escalation when the owner must choose between meaningful alternatives, even without a technical blocker.
@@ -98,14 +98,14 @@ Read the returned verifier handoff. If there are findings, follow findings-decis
98
98
 
99
99
  ### Explain findings and escalations
100
100
 
101
- Before requesting a decision, read the active spec, the finding or escalation artifact, and the relevant code.
101
+ Before requesting a decision, read the active spec, the handoff that contains the finding or escalation, and the relevant code.
102
102
  Trace the affected behavior across components. Do not just repeat the builder's or verifier's summary.
103
103
  For findings, inspect the live project files checked by the verifier.
104
104
  This inspection does not authorize product repairs, new verification runs, or experiments. Follow the existing permissions above.
105
105
 
106
106
  For each finding or escalation, give the owner enough detail to decide without opening other files:
107
107
 
108
- 1. Identify findings with both handoff and finding IDs, such as V1/F1. Finding IDs are local to each verifier handoff. Identify escalations by their IDs. Explain the issue or open question and its relation to the approved contract.
108
+ 1. Identify findings with both handoff and finding IDs, such as V1/F1. Finding IDs are local to each verifier handoff. Identify escalation questions with both handoff and local IDs, such as B1/E1 or B2/E1. Escalation IDs restart at E1 in each builder handoff. Explain the issue or open question and its relation to the approved contract.
109
109
  2. For code-related issues, show a short code excerpt with its file path and line numbers. Explain how that code causes or constrains the behavior. A file reference alone is not enough.
110
110
  3. Give a concrete example with starting conditions, input or action, current behavior, and practical impact. Compare with the contract's expected result when defined. Otherwise identify the behavior that needs an owner decision.
111
111
  4. Explain each available choice, its required changes, scope, consequences, and next workflow step. For findings, cover fix-code, rejection, and spec revision when relevant. Explain what remains unchanged or unresolved if no code changes.
@@ -117,9 +117,9 @@ Keep excerpts and explanations focused, but do not replace the details with seve
117
117
 
118
118
  ### Record owner decisions
119
119
 
120
- In escalation-decision, wait for the explicit owner answer. Do not choose an option for the owner.
121
- If the contract stays unchanged, call maestro_resolve_escalation with the current escalation ID and the owner's decision and reason.
122
- The tool saves the resolution and returns ready-for-builder. Call maestro_run_builder separately.
120
+ In escalation-decision, collect an explicit owner answer for every current question. Do not choose options for the owner.
121
+ If the contract stays unchanged, call maestro_resolve_escalations once with specId and the complete decisions array. Each item contains escalationId, selectedOptionId, decision, and reason. Use selectedOptionId: null for an owner decision outside the listed options. The tool saves all resolutions inside the active builder handoff. Do not submit partial batches.
122
+ The tool saves all resolutions and returns ready-for-builder. Call maestro_run_builder separately.
123
123
  If the contract must change, use the spec revision procedure below instead of resolving the escalation against the old contract.
124
124
 
125
125
  In findings-decision, present every current finding. Every finding requires an owner decision, regardless of severity.
@@ -22,7 +22,6 @@ export type CreatedSpec = {
22
22
  specPath: string;
23
23
  specFilePath: string;
24
24
  workflowPath: string;
25
- escalationsPath: string;
26
25
  prototypesPath: string;
27
26
  state: WorkflowState;
28
27
  };
@@ -48,7 +47,6 @@ export const createSpec = async ({
48
47
  const specPath = paths.getSpecPath(specId);
49
48
  const specFilePath = paths.getSpecFilePath(specId);
50
49
  const workflowPath = paths.getWorkflowPath(specId);
51
- const escalationsPath = paths.getEscalationsPath(specId);
52
50
  const prototypesPath = paths.getPrototypesPath(specId);
53
51
 
54
52
  if (await pathExists(specPath)) {
@@ -71,7 +69,6 @@ export const createSpec = async ({
71
69
  await mkdir(paths.getSpecDirectory(), { recursive: true });
72
70
  await mkdir(specPath);
73
71
  created = true;
74
- await mkdir(escalationsPath, { recursive: true });
75
72
  await mkdir(prototypesPath, { recursive: true });
76
73
  await writeFile(specFilePath, spec, { encoding: 'utf8', flag: 'wx' });
77
74
  await writeWorkflowState({
@@ -100,7 +97,6 @@ export const createSpec = async ({
100
97
  specPath,
101
98
  specFilePath,
102
99
  workflowPath,
103
- escalationsPath,
104
100
  prototypesPath,
105
101
  state,
106
102
  };
@@ -1,14 +1,14 @@
1
1
  /**
2
2
  * Objective: Register the builder handoff tool for the current project.
3
- * Used: When the builder reports a done or failed result.
3
+ * Used: When the builder reports a done, escalation, or failed result.
4
4
  */
5
5
 
6
6
  import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
7
7
  import { Type } from 'typebox';
8
8
  import {
9
- BUILDER_HANDOFF_STATUSES,
10
- BuilderAcceptanceCriterionSchema,
11
- BuilderHandoffFailureSchema,
9
+ BuilderDoneHandoffSubmissionSchema,
10
+ BuilderEscalationHandoffSubmissionSchema,
11
+ BuilderFailedHandoffSubmissionSchema,
12
12
  } from '#artifacts/builder-handoff/schema.ts';
13
13
  import { SPEC_ID_PATTERN } from '#ids/isValidSpecId.ts';
14
14
  import { resolveWorkflowContext } from '#tools/child/utils/resolveWorkflowContext.ts';
@@ -18,30 +18,28 @@ export const BUILDER_HANDOFF_TOOL = {
18
18
  NAME: 'maestro_record_builder_handoff',
19
19
  LABEL: 'Record Builder Handoff',
20
20
  DESCRIPTION:
21
- 'Record the builder pass as done or failed. Put significant discoveries that do not require an owner decision in notes. After success, stop. The tool saves a numbered builder handoff and the workflow phase.',
21
+ 'Record the builder pass as done, escalation, or failed. Put significant discoveries that do not require an owner decision in notes. After success, stop. The tool saves a numbered builder handoff and the workflow phase.',
22
22
  } as const;
23
23
 
24
- const BuilderHandoffContentFields = {
25
- summary: Type.String({ minLength: 1 }),
26
- acceptanceCriteria: Type.Array(BuilderAcceptanceCriterionSchema),
27
- notes: Type.Array(Type.String()),
28
- };
29
-
30
24
  const BuilderHandoffToolParameters = Type.Union([
31
25
  Type.Object(
32
26
  {
33
27
  specId: Type.String({ pattern: SPEC_ID_PATTERN.source }),
34
- status: Type.Literal(BUILDER_HANDOFF_STATUSES.DONE),
35
- ...BuilderHandoffContentFields,
28
+ ...BuilderDoneHandoffSubmissionSchema.properties,
29
+ },
30
+ { additionalProperties: false },
31
+ ),
32
+ Type.Object(
33
+ {
34
+ specId: Type.String({ pattern: SPEC_ID_PATTERN.source }),
35
+ ...BuilderEscalationHandoffSubmissionSchema.properties,
36
36
  },
37
37
  { additionalProperties: false },
38
38
  ),
39
39
  Type.Object(
40
40
  {
41
41
  specId: Type.String({ pattern: SPEC_ID_PATTERN.source }),
42
- status: Type.Literal(BUILDER_HANDOFF_STATUSES.FAILED),
43
- ...BuilderHandoffContentFields,
44
- failure: BuilderHandoffFailureSchema,
42
+ ...BuilderFailedHandoffSubmissionSchema.properties,
45
43
  },
46
44
  { additionalProperties: false },
47
45
  ),
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Objective: Register owner resolution of every current builder escalation.
3
+ * Used: When the owner keeps the approved contract and resolves all current questions.
4
+ */
5
+
6
+ import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
7
+ import { Type } from 'typebox';
8
+ import {
9
+ ESCALATION_ID_PATTERN,
10
+ EscalationResolutionSchema,
11
+ } from '#artifacts/builder-handoff/schema.ts';
12
+ import { SPEC_ID_PATTERN } from '#ids/isValidSpecId.ts';
13
+ import { resolveToolRunContext } from '#tools/utils/resolveToolRunContext.ts';
14
+ import { resolveEscalations } from '#workflow/escalation/resolveEscalations.ts';
15
+
16
+ export const RESOLVE_ESCALATIONS_TOOL = {
17
+ NAME: 'maestro_resolve_escalations',
18
+ LABEL: 'Resolve Escalations',
19
+ DESCRIPTION:
20
+ 'Record the explicit owner decision for every current builder escalation when the approved contract remains valid. Save the resolution and workflow transition, then return ready-for-builder without running the builder. If the contract must change, edit spec.md and use maestro_mark_spec_ready instead.',
21
+ } as const;
22
+
23
+ const ResolveEscalationsToolParameters = Type.Object(
24
+ {
25
+ specId: Type.String({ pattern: SPEC_ID_PATTERN.source }),
26
+ decisions: Type.Array(
27
+ Type.Object(
28
+ {
29
+ escalationId: Type.String({ pattern: ESCALATION_ID_PATTERN.source }),
30
+ ...EscalationResolutionSchema.properties,
31
+ },
32
+ { additionalProperties: false },
33
+ ),
34
+ { minItems: 1 },
35
+ ),
36
+ },
37
+ { additionalProperties: false },
38
+ );
39
+
40
+ export const registerResolveEscalationsTool = (pi: ExtensionAPI): void => {
41
+ pi.registerTool({
42
+ name: RESOLVE_ESCALATIONS_TOOL.NAME,
43
+ label: RESOLVE_ESCALATIONS_TOOL.LABEL,
44
+ description: RESOLVE_ESCALATIONS_TOOL.DESCRIPTION,
45
+ parameters: ResolveEscalationsToolParameters,
46
+ async execute(_toolCallId, params, _signal, _onUpdate, context) {
47
+ const { specId, decisions } = params;
48
+ const { paths } = await resolveToolRunContext(context.cwd);
49
+
50
+ const resolved = await resolveEscalations({
51
+ paths,
52
+ specId,
53
+ decisions,
54
+ });
55
+
56
+ return {
57
+ content: [
58
+ {
59
+ type: 'text',
60
+ text: `All current escalations resolved. Spec ${resolved.state.specId} is ready-for-builder. Run the builder separately.`,
61
+ },
62
+ ],
63
+ details: {
64
+ specId: resolved.state.specId,
65
+ handoff: resolved.handoff,
66
+ phase: resolved.state.phase,
67
+ projectRoot: resolved.projectRoot,
68
+ },
69
+ };
70
+ },
71
+ });
72
+ };
@@ -12,8 +12,6 @@ import {
12
12
  BUILDER_HANDOFF_STATUSES,
13
13
  type BuilderHandoff,
14
14
  } from '#artifacts/builder-handoff/schema.ts';
15
- import { readEscalationHistory } from '#artifacts/escalation/readEscalationHistory.ts';
16
- import type { Escalation } from '#artifacts/escalation/schema.ts';
17
15
  import { AGENTS } from '#config/schema.ts';
18
16
  import { SPEC_ID_PATTERN } from '#ids/isValidSpecId.ts';
19
17
  import type { MaestroPaths } from '#MaestroPaths.ts';
@@ -61,7 +59,7 @@ type BuilderRunResult =
61
59
  outcome: typeof BUILDER_HANDOFF_STATUSES.ESCALATION;
62
60
  specId: string;
63
61
  phase: typeof WORKFLOW_PHASES.ESCALATION_DECISION;
64
- escalation: Escalation;
62
+ handoff: BuilderHandoff;
65
63
  };
66
64
 
67
65
  type ReadBuilderResultInput = {
@@ -159,22 +157,28 @@ const buildRunResult = async ({
159
157
  }
160
158
 
161
159
  if (state.phase === WORKFLOW_PHASES.ESCALATION_DECISION) {
162
- const history = await readEscalationHistory({
163
- directory: paths.getEscalationsPath(specId),
160
+ const handoff = await readBuilderHandoff({
161
+ path: await paths.getActiveBuilderHandoffPath(specId),
164
162
  specId,
165
163
  });
166
164
 
167
- const escalation = history.at(-1);
165
+ if (handoff.status !== BUILDER_HANDOFF_STATUSES.ESCALATION) {
166
+ throw new Error(
167
+ `Builder handoff status does not match workflow phase "${state.phase}".`,
168
+ );
169
+ }
168
170
 
169
- if (escalation === undefined || escalation.resolution !== null) {
170
- throw new Error('The builder escalation is missing or already resolved.');
171
+ if (handoff.escalations.some(({ resolution }) => resolution !== null)) {
172
+ throw new Error(
173
+ 'Current builder escalations already contain an owner resolution.',
174
+ );
171
175
  }
172
176
 
173
177
  return {
174
178
  outcome: BUILDER_HANDOFF_STATUSES.ESCALATION,
175
179
  specId,
176
180
  phase: state.phase,
177
- escalation,
181
+ handoff,
178
182
  };
179
183
  }
180
184
 
@@ -192,7 +196,7 @@ const formatBuilderResult = (result: BuilderRunResult): string => {
192
196
  return `Builder failed for spec ${result.specId}. The workflow is builder-failed and cannot be retried.`;
193
197
  }
194
198
 
195
- return `Builder opened escalation ${result.escalation.id} for spec ${result.specId}. The workflow is waiting for an owner decision.`;
199
+ return `Builder submitted escalation questions for spec ${result.specId}. The workflow is waiting for an owner decision.`;
196
200
  };
197
201
 
198
202
  export const registerRunBuilderTool = (pi: ExtensionAPI): void => {
@@ -1,15 +1,17 @@
1
1
  /**
2
2
  * Objective: Complete a builder pass with a validated terminal handoff.
3
- * Used: When the builder reports done or failed through the child tool.
3
+ * Used: When the builder reports done, escalation, or failed through the child tool.
4
4
  */
5
5
 
6
6
  import { mkdir } from 'node:fs/promises';
7
+ import { Value } from 'typebox/value';
7
8
  import { assertBuilderHandoff } from '#artifacts/builder-handoff/assertBuilderHandoff.ts';
8
9
  import {
9
10
  BUILDER_HANDOFF_STATUSES,
10
11
  BUILDER_HANDOFF_VERSION,
11
12
  type BuilderHandoff,
12
13
  type BuilderHandoffSubmissionInput,
14
+ BuilderHandoffSubmissionSchema,
13
15
  } from '#artifacts/builder-handoff/schema.ts';
14
16
  import { writeBuilderHandoff } from '#artifacts/builder-handoff/writeBuilderHandoff.ts';
15
17
  import type { MaestroPaths } from '#MaestroPaths.ts';
@@ -29,36 +31,14 @@ export type CompletedBuilderPass = {
29
31
  handoffPath: string;
30
32
  };
31
33
 
32
- type BuildBuilderHandoffInput = {
33
- draftHandoff: BuilderHandoffSubmissionInput;
34
- state: WorkflowState;
35
- };
36
-
37
- const buildBuilderHandoff = ({
38
- draftHandoff,
39
- state,
40
- }: BuildBuilderHandoffInput) => {
41
- if (draftHandoff.status === BUILDER_HANDOFF_STATUSES.FAILED) {
42
- return {
43
- version: BUILDER_HANDOFF_VERSION,
44
- specId: state.specId,
45
- status: draftHandoff.status,
46
- summary: draftHandoff.summary,
47
- acceptanceCriteria: draftHandoff.acceptanceCriteria,
48
- failure: draftHandoff.failure,
49
- notes: draftHandoff.notes,
50
- };
51
- }
34
+ function assertBuilderHandoffSubmission(
35
+ input: unknown,
36
+ ): asserts input is BuilderHandoffSubmissionInput {
37
+ const [error] = Value.Errors(BuilderHandoffSubmissionSchema, input);
52
38
 
53
- return {
54
- version: BUILDER_HANDOFF_VERSION,
55
- specId: state.specId,
56
- status: draftHandoff.status,
57
- summary: draftHandoff.summary,
58
- acceptanceCriteria: draftHandoff.acceptanceCriteria,
59
- notes: draftHandoff.notes,
60
- };
61
- };
39
+ if (error !== undefined)
40
+ throw new Error(`Invalid builder submission: ${error.message}.`);
41
+ }
62
42
 
63
43
  type CompleteBuilderPassInput = {
64
44
  paths: MaestroPaths;
@@ -86,8 +66,14 @@ export const completeBuilderPass = async ({
86
66
  );
87
67
  }
88
68
 
69
+ assertBuilderHandoffSubmission(draftHandoff);
70
+
89
71
  const handoffInput = {
90
- handoff: buildBuilderHandoff({ draftHandoff, state: currentState }),
72
+ handoff: {
73
+ ...draftHandoff,
74
+ version: BUILDER_HANDOFF_VERSION,
75
+ specId: currentState.specId,
76
+ },
91
77
  specId: currentState.specId,
92
78
  };
93
79
 
@@ -99,7 +85,9 @@ export const completeBuilderPass = async ({
99
85
  event:
100
86
  draftHandoff.status === BUILDER_HANDOFF_STATUSES.DONE
101
87
  ? WORKFLOW_EVENTS.BUILDER_DONE
102
- : WORKFLOW_EVENTS.BUILDER_FAILED,
88
+ : draftHandoff.status === BUILDER_HANDOFF_STATUSES.ESCALATION
89
+ ? WORKFLOW_EVENTS.OPEN_ESCALATION
90
+ : WORKFLOW_EVENTS.BUILDER_FAILED,
103
91
  });
104
92
 
105
93
  await mkdir(paths.getBuilderHandoffsPath(specId), { recursive: true });