@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.
- package/README.md +17 -5
- package/agents/builder.md +6 -7
- package/agents/verifier.md +3 -3
- package/changelog.md +85 -0
- package/docs/glossary.md +69 -0
- package/docs/subagent-integration.md +1 -1
- package/docs/workflow.md +55 -23
- package/extensions/maestro-subagent.ts +0 -2
- package/extensions/maestro.ts +5 -5
- package/mission.md +7 -0
- package/package.json +7 -4
- package/roadmap.md +85 -0
- package/src/MaestroPaths.ts +0 -32
- package/src/artifacts/builder-handoff/assertBuilderHandoff.ts +29 -0
- package/src/artifacts/builder-handoff/schema.ts +87 -23
- package/src/maestro/instructions/getMaestroInstructions.ts +7 -7
- package/src/specs/create.ts +0 -4
- package/src/tools/child/record-builder-handoff.ts +14 -16
- package/src/tools/main/resolve-escalations.ts +72 -0
- package/src/tools/main/run-builder.ts +14 -10
- package/src/workflow/builder/completeBuilderPass.ts +20 -32
- package/src/workflow/escalation/resolveEscalations.ts +100 -0
- package/tech-stack.md +14 -0
- package/src/artifacts/escalation/assertEscalation.ts +0 -66
- package/src/artifacts/escalation/createEscalation.ts +0 -52
- package/src/artifacts/escalation/getNextEscalationId.ts +0 -23
- package/src/artifacts/escalation/readEscalation.ts +0 -40
- package/src/artifacts/escalation/readEscalationHistory.ts +0 -44
- package/src/artifacts/escalation/resolveEscalation.ts +0 -43
- package/src/artifacts/escalation/schema.ts +0 -73
- package/src/tools/child/open-escalation.ts +0 -71
- package/src/tools/main/resolve-escalation.ts +0 -65
- package/src/workflow/escalation/openBuilderEscalation.ts +0 -82
- package/src/workflow/escalation/resolveBuilderEscalation.ts +0 -118
package/src/MaestroPaths.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
121
|
-
If the contract stays unchanged, call
|
|
122
|
-
The tool saves
|
|
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.
|
package/src/specs/create.ts
CHANGED
|
@@ -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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
35
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
163
|
-
|
|
160
|
+
const handoff = await readBuilderHandoff({
|
|
161
|
+
path: await paths.getActiveBuilderHandoffPath(specId),
|
|
164
162
|
specId,
|
|
165
163
|
});
|
|
166
164
|
|
|
167
|
-
|
|
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 (
|
|
170
|
-
throw new Error(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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:
|
|
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
|
-
:
|
|
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 });
|