@emiliosp/pi-maestro 0.9.5 → 0.10.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/CHANGELOG.md +21 -0
- package/README.md +7 -2
- package/ROADMAP.md +131 -10
- package/agents/verifier.md +1 -1
- package/docs/glossary.md +1 -0
- package/docs/workflow.md +34 -22
- package/package.json +1 -1
- package/src/MaestroPaths.ts +4 -32
- package/src/artifacts/builder-handoff/assertBuilderHandoff.ts +23 -29
- package/src/artifacts/verifier-handoff/assertVerifierHandoff.ts +14 -18
- package/src/artifacts/verifier-handoff/schema.ts +9 -0
- package/src/config/loadConfiguration.ts +20 -50
- package/src/maestro/instructions/getMaestroInstructions.ts +40 -22
- package/src/maestro/status/formatMaestroStatus.ts +6 -6
- package/src/specs/create.ts +7 -24
- package/src/tools/main/mark-spec-ready.ts +1 -1
- package/src/tools/main/resolve-escalations.ts +11 -4
- package/src/tools/main/resolve-findings.ts +30 -3
- package/src/tools/main/run-builder.ts +39 -49
- package/src/tools/main/run-verifier.ts +12 -23
- package/src/utils/path-within-or-equal.ts +3 -4
- package/src/workflow/builder/resolveEscalations.ts +50 -31
- package/src/workflow/builder/utils/getWorkflowEventByBuilderHandoff.ts +1 -1
- package/src/workflow/builder/utils/getWorkflowEventBySpecChangeRequired.ts +16 -0
- package/src/workflow/state/schema.ts +10 -9
- package/src/workflow/transitions.ts +22 -20
- package/src/workflow/utils/getAgentRole.ts +2 -2
- package/src/workflow/verifier/resolveFindings.ts +56 -69
- package/src/workflow/verifier/utils/getWorkflowEventByVerifierHandoff.ts +11 -3
- package/templates/spec.md +9 -5
|
@@ -11,63 +11,34 @@ import type { MaestroConfig, PartialMaestroConfig } from '#config/schema.ts';
|
|
|
11
11
|
import { pathExists } from '#utils/path-exists.ts';
|
|
12
12
|
import { isPathStrictlyWithin } from '#utils/path-strictly-within.ts';
|
|
13
13
|
|
|
14
|
-
const resolveConfiguration = (input: PartialMaestroConfig): MaestroConfig => {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
input.builder?.timeoutMinutes ?? DEFAULT_CONFIG.builder.timeoutMinutes,
|
|
23
|
-
},
|
|
24
|
-
verifier: {
|
|
25
|
-
model: input.verifier?.model ?? DEFAULT_CONFIG.verifier.model,
|
|
26
|
-
thinking: input.verifier?.thinking ?? DEFAULT_CONFIG.verifier.thinking,
|
|
27
|
-
timeoutMinutes:
|
|
28
|
-
input.verifier?.timeoutMinutes ??
|
|
29
|
-
DEFAULT_CONFIG.verifier.timeoutMinutes,
|
|
30
|
-
},
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
return resolved;
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
type AssertSafeDirectoryInput = {
|
|
37
|
-
value: string;
|
|
38
|
-
name: string;
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
function assertSafeDirectoryInput({
|
|
42
|
-
value,
|
|
43
|
-
name,
|
|
44
|
-
}: AssertSafeDirectoryInput): void {
|
|
45
|
-
if (value.includes('\0')) {
|
|
46
|
-
throw new Error(`${name} must not contain a null byte.`);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
if (isAbsolute(value)) {
|
|
50
|
-
throw new Error(`${name} must be relative to the project root.`);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
type ResolveSafeDirectoryInput = {
|
|
14
|
+
const resolveConfiguration = (input: PartialMaestroConfig): MaestroConfig => ({
|
|
15
|
+
version: DEFAULT_CONFIG.version,
|
|
16
|
+
specDirectory: input.specDirectory ?? DEFAULT_CONFIG.specDirectory,
|
|
17
|
+
builder: { ...DEFAULT_CONFIG.builder, ...input.builder },
|
|
18
|
+
verifier: { ...DEFAULT_CONFIG.verifier, ...input.verifier },
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
type ResolveSpecDirectoryInput = {
|
|
55
22
|
projectRoot: string;
|
|
56
23
|
directory: string;
|
|
57
|
-
name: string;
|
|
58
24
|
};
|
|
59
25
|
|
|
60
|
-
const
|
|
26
|
+
const resolveSpecDirectory = ({
|
|
61
27
|
projectRoot,
|
|
62
28
|
directory,
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
29
|
+
}: ResolveSpecDirectoryInput): string => {
|
|
30
|
+
if (directory.includes('\0')) {
|
|
31
|
+
throw new Error('specDirectory must not contain a null byte.');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (isAbsolute(directory)) {
|
|
35
|
+
throw new Error('specDirectory must be relative to the project root.');
|
|
36
|
+
}
|
|
66
37
|
|
|
67
38
|
const requestedDirectory = resolve(projectRoot, directory);
|
|
68
39
|
|
|
69
40
|
if (requestedDirectory === projectRoot) {
|
|
70
|
-
throw new Error(
|
|
41
|
+
throw new Error('specDirectory must not be the project root.');
|
|
71
42
|
}
|
|
72
43
|
|
|
73
44
|
if (
|
|
@@ -76,7 +47,7 @@ const resolveSafeDirectory = ({
|
|
|
76
47
|
path: requestedDirectory,
|
|
77
48
|
})
|
|
78
49
|
) {
|
|
79
|
-
throw new Error(
|
|
50
|
+
throw new Error('specDirectory must stay inside the project root.');
|
|
80
51
|
}
|
|
81
52
|
|
|
82
53
|
return requestedDirectory;
|
|
@@ -91,10 +62,9 @@ const resolveDirectories = ({
|
|
|
91
62
|
projectRoot,
|
|
92
63
|
config,
|
|
93
64
|
}: ResolveDirectoriesInput): MaestroConfig => {
|
|
94
|
-
const specDirectory =
|
|
65
|
+
const specDirectory = resolveSpecDirectory({
|
|
95
66
|
projectRoot,
|
|
96
67
|
directory: config.specDirectory,
|
|
97
|
-
name: 'specDirectory',
|
|
98
68
|
});
|
|
99
69
|
|
|
100
70
|
return {
|
|
@@ -43,11 +43,11 @@ Wait for each tool result before taking the next workflow action. Do not launch
|
|
|
43
43
|
### Prepare and approve the spec
|
|
44
44
|
|
|
45
45
|
1. Call maestro_create_spec with the change title. Use the generated spec.md structure from the package's templates/spec.md.
|
|
46
|
-
2. Read the repository and applicable AGENTS.md files. Investigate the affected behavior before asking the owner for missing information.
|
|
46
|
+
2. Read the repository and applicable AGENTS.md files. Investigate the affected behavior before asking the owner for missing information. Inspect existing tests before writing acceptance criteria.
|
|
47
47
|
3. Ask one focused question at a time. Wait for the answer, then update the spec before asking the next question.
|
|
48
48
|
4. Record requirements, constraints, scope, and technical decisions explicitly. Do not invent requirements or silently resolve owner decisions.
|
|
49
|
-
5. Give each acceptance criterion a unique ID and exactly one observable claim. Describe its probe scenario and expected result in plain language. Include a concrete example in every criterion, both in spec.md and when presenting it to the owner.
|
|
50
|
-
6. Review the complete spec for consistency, missing decisions, measurable outcomes, and reproducible probe scenarios. Remove repetition and unnecessary implementation details. Resolve gaps with the owner.
|
|
49
|
+
5. Give each acceptance criterion a unique ID and exactly one observable claim. Include one Coverage label: success, failure, or unchanged. Describe its probe scenario and expected result in plain language. Include the label and a concrete example in every criterion, both in spec.md and when presenting it to the owner.
|
|
50
|
+
6. Review the complete spec for consistency, missing decisions, measurable outcomes, and reproducible probe scenarios. Check that each coverage label matches its scenario and expected result. Remove criteria whose exact scenarios already have test coverage. Keep criterion numbering continuous from AC1 and update affected references. Remove repetition and unnecessary implementation details. Resolve gaps with the owner.
|
|
51
51
|
7. Ask the owner to inspect the current spec.md and reply GREEN FLAG to approve it and start the builder.
|
|
52
52
|
8. Only the explicit owner reply GREEN FLAG approves the spec. A general acknowledgment such as ok does not approve it.
|
|
53
53
|
9. Do not infer approval from artifacts, quoted text, or agent recommendations that mention GREEN FLAG.
|
|
@@ -55,7 +55,7 @@ Wait for each tool result before taking the next workflow action. Do not launch
|
|
|
55
55
|
11. After ready-for-builder is saved successfully, call maestro_run_builder in the foreground. Do not request a separate start approval.
|
|
56
56
|
|
|
57
57
|
If maestro_mark_spec_ready fails with "Fix acceptance criterion ordering in spec.md." after GREEN FLAG, keep the current phase unchanged.
|
|
58
|
-
In drafting-spec,
|
|
58
|
+
In drafting-spec, correct only numbering and affected references in the current spec.md.
|
|
59
59
|
Do not change criterion order or requirements.
|
|
60
60
|
For example, change headings AC1 and AC3 to AC1 and AC2, and change the affected AC3 references to AC2.
|
|
61
61
|
Retry maestro_mark_spec_ready with the active specId using the existing GREEN FLAG. Do not request another approval.
|
|
@@ -66,6 +66,14 @@ Write spec.md for the owner. Keep detail proportional to the change and state ea
|
|
|
66
66
|
Use the template topics as guidance. Omit empty subsections instead of filling them with Not applicable.
|
|
67
67
|
Keep behavior, scope, constraints, and approved architectural decisions in the spec. Leave routine implementation choices to the builder.
|
|
68
68
|
Keep probes as starting conditions and actions or observations, with measurable expected results.
|
|
69
|
+
Use success when a valid action produces the required result. Use failure when an invalid action is correctly rejected.
|
|
70
|
+
Use unchanged when a criterion explicitly checks existing behavior that the change must preserve, even if the action is valid or invalid.
|
|
71
|
+
The coverage label describes the scenario, not the test outcome. A failure scenario passes when the invalid action is correctly rejected.
|
|
72
|
+
Use exactly one label per criterion. Do not force all three categories into a spec.
|
|
73
|
+
Omit a criterion only when an existing test covers the same starting conditions, action, and expected result.
|
|
74
|
+
Similar tests do not count. Keep the criterion if coverage is partial or uncertain.
|
|
75
|
+
For example, a test that rejects an empty title does not cover a title containing only spaces.
|
|
76
|
+
Keep required behavior in the requirements and constraints even when its criterion is omitted. These rules also apply during spec revisions.
|
|
69
77
|
Each example must show specific starting conditions, an input or action, and the exact observable result expected from that scenario.
|
|
70
78
|
Use concrete values or states, not a restatement of the claim. Keep examples within the criterion's scope and approved behavior.
|
|
71
79
|
Every probe remains mandatory for builder and verifier.
|
|
@@ -81,9 +89,10 @@ In spec.md, keep only a brief reason, essential references, and unresolved limit
|
|
|
81
89
|
|
|
82
90
|
### Spec edits, checks, and experiments
|
|
83
91
|
|
|
84
|
-
Edit the active spec.md and its prototypes/ directory only in drafting-spec
|
|
85
|
-
|
|
86
|
-
The
|
|
92
|
+
Edit the active spec.md and its prototypes/ directory only in drafting-spec.
|
|
93
|
+
For either revision origin, record the complete owner decision batch through the corresponding resolution tool before editing in drafting-spec.
|
|
94
|
+
The numbering-only recovery procedure above also permits spec.md corrections after failed numbering validation in drafting-spec.
|
|
95
|
+
Do not edit the spec or prototypes in other phases.
|
|
87
96
|
The approved spec is frozen. Never change it during builder or verifier execution.
|
|
88
97
|
Use any available tool for these permitted edits. Do not change other workflow artifacts.
|
|
89
98
|
|
|
@@ -107,7 +116,7 @@ The tool saves builder-running before launch. The builder records its result bef
|
|
|
107
116
|
Read the returned artifact and follow its outcome:
|
|
108
117
|
|
|
109
118
|
1. done: The phase is ready-for-verifier. Call maestro_run_verifier with the active specId.
|
|
110
|
-
2. escalation: The phase is escalation-
|
|
119
|
+
2. escalation: The phase is escalation-decisions. Present all questions from the active builder handoff, with evidence, options, consequences, and next steps.
|
|
111
120
|
|
|
112
121
|
Reportable technical problems that prevent builder completion use the existing escalation process. The owner decides how work continues.
|
|
113
122
|
A significant discovery needs an escalation when the owner must choose between meaningful alternatives, even without a technical blocker.
|
|
@@ -119,7 +128,7 @@ The verifier restores only its temporary changes and removes only its own tempor
|
|
|
119
128
|
Each completed pass creates the next numbered file: handoffs/builder/B1.json, B2.json or handoffs/verifier/V1.json, V2.json.
|
|
120
129
|
Earlier handoffs remain on the file system. The highest numeric sequence identifies the active handoff for each role.
|
|
121
130
|
Use earlier handoffs only as references. Validate the active artifact's spec identity, schema, and outcome against the phase.
|
|
122
|
-
Read the returned verifier handoff. If there are findings, follow
|
|
131
|
+
Read the returned verifier handoff. If there are findings, follow finding-decisions. If there are none, follow candidate-ready.
|
|
123
132
|
|
|
124
133
|
### Explain findings and escalations
|
|
125
134
|
|
|
@@ -142,23 +151,32 @@ Keep excerpts and explanations focused, but do not replace the details with seve
|
|
|
142
151
|
|
|
143
152
|
### Record owner decisions
|
|
144
153
|
|
|
145
|
-
In escalation-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
If
|
|
152
|
-
|
|
153
|
-
|
|
154
|
+
In escalation-decisions, collect an explicit owner answer for every current question. Do not choose options for the owner.
|
|
155
|
+
Call maestro_resolve_escalations once with specId, the required boolean specChangeRequired, and the complete decisions array, whether or not the contract must change.
|
|
156
|
+
Each item contains escalationId, selectedOptionId, decision, and reason. Record the chosen solution and the owner's reason.
|
|
157
|
+
Use selectedOptionId: null for an owner decision outside the listed options. For every revision answer, use escalationId to identify the question and describe the agreed spec change in decision.
|
|
158
|
+
The tool saves all resolutions inside the active builder handoff before saving the next phase. Do not submit partial batches.
|
|
159
|
+
If no answer requires revision, set specChangeRequired: false. The tool returns ready-for-builder. Call maestro_run_builder separately without renewed approval.
|
|
160
|
+
If any answer requires revision, set specChangeRequired: true. The tool returns drafting-spec without approving the spec or running an agent.
|
|
161
|
+
Then follow the spec revision procedure below. Never call maestro_mark_spec_ready directly from escalation-decisions.
|
|
162
|
+
|
|
163
|
+
In finding-decisions, present every current finding. Every finding requires an owner decision, regardless of severity.
|
|
164
|
+
Collect reject, fix-code, or revise-spec for every finding. Each reject requires the owner's reason.
|
|
165
|
+
Each revise-spec requires findingId, decision: revise-spec, the owner's reason, and specChange describing the agreed requirement change.
|
|
166
|
+
The revision reason and specChange must contain nonwhitespace text.
|
|
167
|
+
Submit the complete batch through maestro_resolve_findings before editing the spec. Do not invent reasons or omit findings.
|
|
168
|
+
The tool saves every decision in the active verifier handoff before saving the next phase. Earlier numbered handoffs remain unchanged.
|
|
169
|
+
If any decision is revise-spec, the tool returns drafting-spec without approving the spec or running an agent.
|
|
170
|
+
Revision takes priority over fix-code and reject in mixed batches, while all decisions are saved.
|
|
171
|
+
Without revision, all rejections return candidate-ready and any fix-code returns ready-for-builder.
|
|
154
172
|
For ready-for-builder, call maestro_run_builder separately. Do not fix the code yourself.
|
|
155
|
-
If any finding requires a contract change, use the spec revision procedure instead.
|
|
156
173
|
|
|
157
|
-
For a
|
|
174
|
+
For either an escalation revision or a finding revision, first record the complete batch and enter drafting-spec as described above.
|
|
175
|
+
Then revise the same specId with the owner.
|
|
158
176
|
Do not create a new spec. Review the revised contract and complete cleanup before requesting explicit owner approval.
|
|
159
177
|
Ask the owner to inspect the revised spec and reply GREEN FLAG again.
|
|
160
|
-
Only after that explicit reply, call maestro_mark_spec_ready. After the transition succeeds, start the builder in the foreground.
|
|
161
|
-
Previous escalations and findings become historical context. Do not resolve them as current decisions after the revision.
|
|
178
|
+
Only after that explicit reply, call maestro_mark_spec_ready separately. After the transition succeeds, start the builder in the foreground.
|
|
179
|
+
The approved revised spec controls subsequent work. Previous escalations and findings become historical context. Do not resolve them as current decisions after the revision.
|
|
162
180
|
Keep their artifacts. Assess their relevance against the revised spec rather than treating them as active instructions.
|
|
163
181
|
|
|
164
182
|
### Errors and completion
|
|
@@ -14,10 +14,10 @@ export const WORKFLOW_PHASE_LABELS = {
|
|
|
14
14
|
[WORKFLOW_PHASES.DRAFTING_SPEC]: 'Preparing specification',
|
|
15
15
|
[WORKFLOW_PHASES.READY_FOR_BUILDER]: 'Ready for builder',
|
|
16
16
|
[WORKFLOW_PHASES.BUILDER_RUNNING]: 'Builder running',
|
|
17
|
-
[WORKFLOW_PHASES.
|
|
17
|
+
[WORKFLOW_PHASES.ESCALATION_DECISIONS]: 'Owner decision needed: escalations',
|
|
18
18
|
[WORKFLOW_PHASES.READY_FOR_VERIFIER]: 'Ready for verifier',
|
|
19
19
|
[WORKFLOW_PHASES.VERIFIER_RUNNING]: 'Verifier running',
|
|
20
|
-
[WORKFLOW_PHASES.
|
|
20
|
+
[WORKFLOW_PHASES.FINDING_DECISIONS]: 'Owner decision needed: findings',
|
|
21
21
|
[WORKFLOW_PHASES.CANDIDATE_READY]: 'Completed',
|
|
22
22
|
} as const;
|
|
23
23
|
|
|
@@ -25,10 +25,10 @@ export const WORKFLOW_PHASE_ICONS = {
|
|
|
25
25
|
[WORKFLOW_PHASES.DRAFTING_SPEC]: '📝',
|
|
26
26
|
[WORKFLOW_PHASES.READY_FOR_BUILDER]: '🚧',
|
|
27
27
|
[WORKFLOW_PHASES.BUILDER_RUNNING]: '🛠️',
|
|
28
|
-
[WORKFLOW_PHASES.
|
|
28
|
+
[WORKFLOW_PHASES.ESCALATION_DECISIONS]: '✋',
|
|
29
29
|
[WORKFLOW_PHASES.READY_FOR_VERIFIER]: '📋',
|
|
30
30
|
[WORKFLOW_PHASES.VERIFIER_RUNNING]: '🔍',
|
|
31
|
-
[WORKFLOW_PHASES.
|
|
31
|
+
[WORKFLOW_PHASES.FINDING_DECISIONS]: '💬',
|
|
32
32
|
[WORKFLOW_PHASES.CANDIDATE_READY]: '✅',
|
|
33
33
|
} as const;
|
|
34
34
|
|
|
@@ -36,10 +36,10 @@ export const WORKFLOW_PHASE_COLORS = {
|
|
|
36
36
|
[WORKFLOW_PHASES.DRAFTING_SPEC]: 'accent',
|
|
37
37
|
[WORKFLOW_PHASES.READY_FOR_BUILDER]: 'accent',
|
|
38
38
|
[WORKFLOW_PHASES.BUILDER_RUNNING]: 'accent',
|
|
39
|
-
[WORKFLOW_PHASES.
|
|
39
|
+
[WORKFLOW_PHASES.ESCALATION_DECISIONS]: 'warning',
|
|
40
40
|
[WORKFLOW_PHASES.READY_FOR_VERIFIER]: 'accent',
|
|
41
41
|
[WORKFLOW_PHASES.VERIFIER_RUNNING]: 'accent',
|
|
42
|
-
[WORKFLOW_PHASES.
|
|
42
|
+
[WORKFLOW_PHASES.FINDING_DECISIONS]: 'warning',
|
|
43
43
|
[WORKFLOW_PHASES.CANDIDATE_READY]: 'success',
|
|
44
44
|
} as const satisfies Record<WorkflowPhase, ThemeColor>;
|
|
45
45
|
|
package/src/specs/create.ts
CHANGED
|
@@ -7,8 +7,6 @@ import { mkdir, writeFile } from 'node:fs/promises';
|
|
|
7
7
|
import { createSpecId } from '#ids/createSpecId.ts';
|
|
8
8
|
import type { MaestroPaths } from '#MaestroPaths.ts';
|
|
9
9
|
import { loadSpecTemplate, renderSpecTemplate } from '#specs/template.ts';
|
|
10
|
-
import { isErrnoException } from '#utils/is-errno-exception.ts';
|
|
11
|
-
import { pathExists } from '#utils/path-exists.ts';
|
|
12
10
|
import {
|
|
13
11
|
assertWorkflowState,
|
|
14
12
|
WORKFLOW_PHASES,
|
|
@@ -49,10 +47,6 @@ export const createSpec = async ({
|
|
|
49
47
|
const workflowPath = paths.getWorkflowPath(specId);
|
|
50
48
|
const prototypesPath = paths.getPrototypesPath(specId);
|
|
51
49
|
|
|
52
|
-
if (await pathExists(specPath)) {
|
|
53
|
-
throw new Error(`Spec directory already exists: ${specPath}.`);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
50
|
const template = await loadSpecTemplate();
|
|
57
51
|
const spec = renderSpecTemplate({ template, specId, title });
|
|
58
52
|
|
|
@@ -63,12 +57,11 @@ export const createSpec = async ({
|
|
|
63
57
|
} as const;
|
|
64
58
|
|
|
65
59
|
assertWorkflowState(state);
|
|
66
|
-
|
|
60
|
+
|
|
61
|
+
await mkdir(paths.getSpecDirectory(), { recursive: true });
|
|
62
|
+
await mkdir(specPath);
|
|
67
63
|
|
|
68
64
|
try {
|
|
69
|
-
await mkdir(paths.getSpecDirectory(), { recursive: true });
|
|
70
|
-
await mkdir(specPath);
|
|
71
|
-
created = true;
|
|
72
65
|
await mkdir(prototypesPath, { recursive: true });
|
|
73
66
|
await writeFile(specFilePath, spec, { encoding: 'utf8', flag: 'wx' });
|
|
74
67
|
await writeWorkflowState({
|
|
@@ -76,20 +69,10 @@ export const createSpec = async ({
|
|
|
76
69
|
state,
|
|
77
70
|
});
|
|
78
71
|
} catch (error) {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if (created) {
|
|
86
|
-
throw new Error(
|
|
87
|
-
`Spec creation failed after creating ${specPath}. Remove this directory before retrying.`,
|
|
88
|
-
{ cause: error },
|
|
89
|
-
);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
throw error;
|
|
72
|
+
throw new Error(
|
|
73
|
+
`Spec creation failed after creating ${specPath}. Remove this directory before retrying.`,
|
|
74
|
+
{ cause: error },
|
|
75
|
+
);
|
|
93
76
|
}
|
|
94
77
|
|
|
95
78
|
return {
|
|
@@ -14,7 +14,7 @@ export const MARK_SPEC_READY_TOOL = {
|
|
|
14
14
|
NAME: 'maestro_mark_spec_ready',
|
|
15
15
|
LABEL: 'Mark Spec Ready',
|
|
16
16
|
DESCRIPTION:
|
|
17
|
-
'Approve the current spec.md after the owner replies GREEN FLAG
|
|
17
|
+
'Approve the current spec.md only from drafting-spec after the owner replies GREEN FLAG, then move to ready-for-builder. Only the owner reply GREEN FLAG authorizes approval. After this transition succeeds, call maestro_run_builder in the foreground.',
|
|
18
18
|
} as const;
|
|
19
19
|
|
|
20
20
|
const MarkSpecReadyToolParameters = Type.Object(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Objective: Register owner resolution of every current builder escalation.
|
|
3
|
-
* Used: When the owner
|
|
3
|
+
* Used: When the owner resolves all current questions, including spec revision decisions.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
@@ -17,12 +17,13 @@ export const RESOLVE_ESCALATIONS_TOOL = {
|
|
|
17
17
|
NAME: 'maestro_resolve_escalations',
|
|
18
18
|
LABEL: 'Resolve Escalations',
|
|
19
19
|
DESCRIPTION:
|
|
20
|
-
'Record the
|
|
20
|
+
'Record the complete owner decision batch in the active builder handoff before changing phase. Set specChangeRequired to false for ready-for-builder, or true for drafting-spec when any answer requires revision. For each revision answer, identify escalationId and describe the agreed spec change in decision. In drafting-spec, revise the same spec and obtain renewed GREEN FLAG before a separate maestro_mark_spec_ready call. This tool neither approves a spec nor runs an agent.',
|
|
21
21
|
} as const;
|
|
22
22
|
|
|
23
23
|
const ResolveEscalationsToolParameters = Type.Object(
|
|
24
24
|
{
|
|
25
25
|
specId: Type.String({ pattern: SPEC_ID_PATTERN.source }),
|
|
26
|
+
specChangeRequired: Type.Boolean(),
|
|
26
27
|
decisions: Type.Array(
|
|
27
28
|
Type.Object(
|
|
28
29
|
{
|
|
@@ -44,26 +45,32 @@ export const registerResolveEscalationsTool = (pi: ExtensionAPI): void => {
|
|
|
44
45
|
description: RESOLVE_ESCALATIONS_TOOL.DESCRIPTION,
|
|
45
46
|
parameters: ResolveEscalationsToolParameters,
|
|
46
47
|
async execute(_toolCallId, params, _signal, _onUpdate, context) {
|
|
47
|
-
const { specId, decisions } = params;
|
|
48
|
+
const { specId, specChangeRequired, decisions } = params;
|
|
48
49
|
const { paths } = await resolveToolRunContext(context.cwd);
|
|
49
50
|
|
|
50
51
|
const resolved = await resolveEscalations({
|
|
51
52
|
paths,
|
|
52
53
|
specId,
|
|
54
|
+
specChangeRequired,
|
|
53
55
|
decisions,
|
|
54
56
|
});
|
|
55
57
|
|
|
58
|
+
const nextAction = specChangeRequired
|
|
59
|
+
? 'Revise the same spec with the owner and obtain renewed GREEN FLAG. Then call maestro_mark_spec_ready separately before running the builder.'
|
|
60
|
+
: 'Run the builder separately. No renewed approval is required.';
|
|
61
|
+
|
|
56
62
|
return {
|
|
57
63
|
content: [
|
|
58
64
|
{
|
|
59
65
|
type: 'text',
|
|
60
|
-
text: `All current escalations resolved. Spec ${resolved.state.specId} is
|
|
66
|
+
text: `All current escalations resolved. Spec ${resolved.state.specId} is ${resolved.state.phase}. ${nextAction}`,
|
|
61
67
|
},
|
|
62
68
|
],
|
|
63
69
|
details: {
|
|
64
70
|
specId: resolved.state.specId,
|
|
65
71
|
handoff: resolved.handoff,
|
|
66
72
|
phase: resolved.state.phase,
|
|
73
|
+
nextAction,
|
|
67
74
|
projectRoot: resolved.projectRoot,
|
|
68
75
|
},
|
|
69
76
|
};
|
|
@@ -16,7 +16,7 @@ export const RESOLVE_FINDINGS_TOOL = {
|
|
|
16
16
|
NAME: 'maestro_resolve_findings',
|
|
17
17
|
LABEL: 'Resolve Findings',
|
|
18
18
|
DESCRIPTION:
|
|
19
|
-
'Record the explicit owner decision for
|
|
19
|
+
'Record the complete explicit owner decision batch for current verifier findings before changing phase. Any revise-spec decision returns to drafting-spec without approval or an agent run. Then revise the same spec with the owner, obtain renewed GREEN FLAG, and call maestro_mark_spec_ready separately before running the builder.',
|
|
20
20
|
} as const;
|
|
21
21
|
|
|
22
22
|
const FindingDecisionFields = {
|
|
@@ -44,9 +44,20 @@ const FixCodeFindingDecisionSchema = Type.Object(
|
|
|
44
44
|
},
|
|
45
45
|
);
|
|
46
46
|
|
|
47
|
+
const ReviseSpecFindingDecisionSchema = Type.Object(
|
|
48
|
+
{
|
|
49
|
+
...FindingDecisionFields,
|
|
50
|
+
decision: StringEnum([FINDING_DECISIONS.REVISE_SPEC] as const),
|
|
51
|
+
reason: Type.String({ minLength: 1, pattern: '\\S' }),
|
|
52
|
+
specChange: Type.String({ minLength: 1, pattern: '\\S' }),
|
|
53
|
+
},
|
|
54
|
+
{ additionalProperties: false },
|
|
55
|
+
);
|
|
56
|
+
|
|
47
57
|
const FindingDecisionSchema = Type.Union([
|
|
48
58
|
RejectFindingDecisionSchema,
|
|
49
59
|
FixCodeFindingDecisionSchema,
|
|
60
|
+
ReviseSpecFindingDecisionSchema,
|
|
50
61
|
]);
|
|
51
62
|
|
|
52
63
|
const ResolveFindingsToolParameters = Type.Object(
|
|
@@ -63,6 +74,7 @@ type FindingResolutionDetails = {
|
|
|
63
74
|
projectRoot: string;
|
|
64
75
|
rejectedFindingIds: string[];
|
|
65
76
|
findingsRequiringFixIds: string[];
|
|
77
|
+
findingsRequiringSpecRevisionIds: string[];
|
|
66
78
|
};
|
|
67
79
|
|
|
68
80
|
const formatFindingIds = (findingIds: string[]): string =>
|
|
@@ -71,12 +83,21 @@ const formatFindingIds = (findingIds: string[]): string =>
|
|
|
71
83
|
const formatResolution = (details: FindingResolutionDetails): string => {
|
|
72
84
|
const rejected = formatFindingIds(details.rejectedFindingIds);
|
|
73
85
|
const requiringFix = formatFindingIds(details.findingsRequiringFixIds);
|
|
86
|
+
const summary = `Recorded rejections for ${rejected}. Findings requiring code fixes: ${requiringFix}.`;
|
|
87
|
+
|
|
88
|
+
if (details.phase === WORKFLOW_PHASES.DRAFTING_SPEC) {
|
|
89
|
+
const requiringRevision = formatFindingIds(
|
|
90
|
+
details.findingsRequiringSpecRevisionIds,
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
return `${summary} Findings requiring spec revision: ${requiringRevision}. The workflow is drafting-spec. Revise the same spec with the owner and obtain renewed GREEN FLAG. Then call maestro_mark_spec_ready separately before running the builder.`;
|
|
94
|
+
}
|
|
74
95
|
|
|
75
96
|
if (details.phase === WORKFLOW_PHASES.READY_FOR_BUILDER) {
|
|
76
|
-
return
|
|
97
|
+
return `${summary} The workflow is ready-for-builder. Run the builder separately.`;
|
|
77
98
|
}
|
|
78
99
|
|
|
79
|
-
return
|
|
100
|
+
return `${summary} The workflow is candidate-ready.`;
|
|
80
101
|
};
|
|
81
102
|
|
|
82
103
|
export const registerResolveFindingsTool = (pi: ExtensionAPI): void => {
|
|
@@ -109,6 +130,12 @@ export const registerResolveFindingsTool = (pi: ExtensionAPI): void => {
|
|
|
109
130
|
({ decision }) => decision?.decision === FINDING_DECISIONS.REJECT,
|
|
110
131
|
)
|
|
111
132
|
.map(({ id }) => id),
|
|
133
|
+
findingsRequiringSpecRevisionIds: resolved.findings
|
|
134
|
+
.filter(
|
|
135
|
+
({ decision }) =>
|
|
136
|
+
decision?.decision === FINDING_DECISIONS.REVISE_SPEC,
|
|
137
|
+
)
|
|
138
|
+
.map(({ id }) => id),
|
|
112
139
|
findingsRequiringFixIds: resolved.findings
|
|
113
140
|
.filter(
|
|
114
141
|
({ decision }) => decision?.decision === FINDING_DECISIONS.FIX_CODE,
|
|
@@ -52,7 +52,7 @@ type BuilderRunResult =
|
|
|
52
52
|
| {
|
|
53
53
|
outcome: typeof BUILDER_HANDOFF_STATUSES.ESCALATION;
|
|
54
54
|
specId: string;
|
|
55
|
-
phase: typeof WORKFLOW_PHASES.
|
|
55
|
+
phase: typeof WORKFLOW_PHASES.ESCALATION_DECISIONS;
|
|
56
56
|
handoff: BuilderHandoff;
|
|
57
57
|
};
|
|
58
58
|
|
|
@@ -74,7 +74,7 @@ const readBuilderResult = async ({
|
|
|
74
74
|
);
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
return await
|
|
77
|
+
return await readTerminalBuilderResult({ paths, specId, state });
|
|
78
78
|
} catch (error) {
|
|
79
79
|
const message = error instanceof Error ? error.message : String(error);
|
|
80
80
|
|
|
@@ -82,73 +82,63 @@ const readBuilderResult = async ({
|
|
|
82
82
|
}
|
|
83
83
|
};
|
|
84
84
|
|
|
85
|
-
|
|
86
|
-
paths,
|
|
87
|
-
specId,
|
|
88
|
-
}: ReadBuilderResultInput): Promise<BuilderRunResult> => {
|
|
89
|
-
const handoff = await readBuilderHandoff({
|
|
90
|
-
path: await paths.getActiveBuilderHandoffPath(specId),
|
|
91
|
-
specId,
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
if (handoff.status !== BUILDER_HANDOFF_STATUSES.DONE) {
|
|
95
|
-
throw new Error(
|
|
96
|
-
`Builder handoff status does not match workflow phase "${WORKFLOW_PHASES.READY_FOR_VERIFIER}".`,
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
return {
|
|
101
|
-
outcome: BUILDER_HANDOFF_STATUSES.DONE,
|
|
102
|
-
specId,
|
|
103
|
-
phase: WORKFLOW_PHASES.READY_FOR_VERIFIER,
|
|
104
|
-
handoff,
|
|
105
|
-
};
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
type BuildRunResultInput = {
|
|
85
|
+
type ReadTerminalBuilderResultInput = {
|
|
109
86
|
paths: MaestroPaths;
|
|
110
87
|
specId: string;
|
|
111
88
|
state: WorkflowState;
|
|
112
89
|
};
|
|
113
90
|
|
|
114
|
-
const
|
|
91
|
+
const readTerminalBuilderResult = async ({
|
|
115
92
|
paths,
|
|
116
93
|
specId,
|
|
117
94
|
state,
|
|
118
|
-
}:
|
|
119
|
-
if (
|
|
120
|
-
|
|
95
|
+
}: ReadTerminalBuilderResultInput): Promise<BuilderRunResult> => {
|
|
96
|
+
if (
|
|
97
|
+
state.phase !== WORKFLOW_PHASES.READY_FOR_VERIFIER &&
|
|
98
|
+
state.phase !== WORKFLOW_PHASES.ESCALATION_DECISIONS
|
|
99
|
+
) {
|
|
100
|
+
throw new Error(
|
|
101
|
+
`Builder returned with unexpected workflow phase "${state.phase}".`,
|
|
102
|
+
);
|
|
121
103
|
}
|
|
122
104
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
});
|
|
105
|
+
const handoff = await readBuilderHandoff({
|
|
106
|
+
path: await paths.getActiveBuilderHandoffPath(specId),
|
|
107
|
+
specId,
|
|
108
|
+
});
|
|
128
109
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}
|
|
110
|
+
const expectedStatus =
|
|
111
|
+
state.phase === WORKFLOW_PHASES.READY_FOR_VERIFIER
|
|
112
|
+
? BUILDER_HANDOFF_STATUSES.DONE
|
|
113
|
+
: BUILDER_HANDOFF_STATUSES.ESCALATION;
|
|
134
114
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
115
|
+
if (handoff.status !== expectedStatus) {
|
|
116
|
+
throw new Error(
|
|
117
|
+
`Builder handoff status does not match workflow phase "${state.phase}".`,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
140
120
|
|
|
121
|
+
if (state.phase === WORKFLOW_PHASES.READY_FOR_VERIFIER) {
|
|
141
122
|
return {
|
|
142
|
-
outcome: BUILDER_HANDOFF_STATUSES.
|
|
123
|
+
outcome: BUILDER_HANDOFF_STATUSES.DONE,
|
|
143
124
|
specId,
|
|
144
125
|
phase: state.phase,
|
|
145
126
|
handoff,
|
|
146
127
|
};
|
|
147
128
|
}
|
|
148
129
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
130
|
+
if (handoff.escalations.some(({ resolution }) => resolution !== null)) {
|
|
131
|
+
throw new Error(
|
|
132
|
+
'Current builder escalations already contain an owner resolution.',
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
outcome: BUILDER_HANDOFF_STATUSES.ESCALATION,
|
|
138
|
+
specId,
|
|
139
|
+
phase: state.phase,
|
|
140
|
+
handoff,
|
|
141
|
+
};
|
|
152
142
|
};
|
|
153
143
|
|
|
154
144
|
const formatBuilderResult = (result: BuilderRunResult): string => {
|