@emiliosp/pi-maestro 0.10.0 → 0.11.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.
- package/ADR.md +53 -9
- package/CHANGELOG.md +79 -36
- package/MISSION.md +4 -2
- package/README.md +31 -15
- package/ROADMAP.md +150 -61
- package/TECH_STACK.md +12 -11
- package/agents/simplifier.md +214 -0
- package/agents/verifier.md +1 -16
- package/docs/configuration.md +27 -16
- package/docs/glossary.md +41 -36
- package/docs/subagent-integration.md +23 -13
- package/docs/workflow.md +103 -71
- package/extensions/maestro-subagent.ts +3 -1
- package/extensions/maestro.ts +10 -0
- package/package.json +1 -1
- package/src/MaestroPaths.ts +8 -32
- package/src/artifacts/builder-handoff/assertBuilderHandoff.ts +23 -29
- package/src/artifacts/simplifier-handoff/assertSimplifierHandoff.ts +56 -0
- package/src/artifacts/simplifier-handoff/readSimplifierHandoff.ts +29 -0
- package/src/artifacts/simplifier-handoff/schema.ts +26 -0
- package/src/artifacts/verifier-handoff/assertVerifierHandoff.ts +14 -18
- package/src/config/assertConfiguration.ts +1 -1
- package/src/config/defaults.ts +7 -0
- package/src/config/loadConfiguration.ts +21 -50
- package/src/config/schema.ts +3 -0
- package/src/maestro/checks/assertEnvironment.ts +7 -3
- package/src/maestro/instructions/getMaestroInstructions.ts +27 -9
- package/src/maestro/status/formatMaestroStatus.ts +3 -0
- package/src/specs/create.ts +7 -24
- package/src/tools/child/record-simplifier-handoff.ts +73 -0
- package/src/tools/main/resolve-findings.ts +4 -3
- package/src/tools/main/run-builder.ts +38 -48
- package/src/tools/main/run-simplifier.ts +182 -0
- package/src/tools/main/run-verifier.ts +14 -25
- package/src/utils/path-within-or-equal.ts +3 -4
- package/src/workflow/builder/resolveEscalations.ts +43 -27
- package/src/workflow/roles.ts +2 -1
- package/src/workflow/simplifier/completeSimplifierPass.ts +57 -0
- package/src/workflow/state/schema.ts +4 -0
- package/src/workflow/transitions.ts +13 -6
- package/src/workflow/utils/getAgentRole.ts +5 -1
- package/src/workflow/verifier/resolveFindings.ts +53 -87
- package/templates/spec.md +9 -5
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Objective: Save a validated simplifier handoff before completing the workflow.
|
|
3
|
+
* Used: When the simplifier records its single result.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { mkdir } from 'node:fs/promises';
|
|
7
|
+
import { dirname } from 'node:path';
|
|
8
|
+
import { assertSimplifierHandoff } from '#artifacts/simplifier-handoff/assertSimplifierHandoff.ts';
|
|
9
|
+
import type { MaestroPaths } from '#MaestroPaths.ts';
|
|
10
|
+
import { writeJson } from '#utils/write-json.ts';
|
|
11
|
+
import { readWorkflowState } from '#workflow/state/readWorkflowState.ts';
|
|
12
|
+
import { WORKFLOW_EVENTS, WORKFLOW_PHASES } from '#workflow/state/schema.ts';
|
|
13
|
+
import { writeWorkflowState } from '#workflow/state/writeWorkflowState.ts';
|
|
14
|
+
import { transitionWorkflow } from '#workflow/transitions.ts';
|
|
15
|
+
|
|
16
|
+
type CompleteSimplifierPassInput = {
|
|
17
|
+
paths: MaestroPaths;
|
|
18
|
+
specId: string;
|
|
19
|
+
handoff: unknown;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const completeSimplifierPass = async ({
|
|
23
|
+
paths,
|
|
24
|
+
specId,
|
|
25
|
+
handoff,
|
|
26
|
+
}: CompleteSimplifierPassInput) => {
|
|
27
|
+
const workflowPath = paths.getWorkflowPath(specId);
|
|
28
|
+
const state = await readWorkflowState(workflowPath);
|
|
29
|
+
|
|
30
|
+
if (state.specId !== specId) {
|
|
31
|
+
throw new Error(
|
|
32
|
+
`Workflow spec ID mismatch: expected "${specId}", found "${state.specId}".`,
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (state.phase !== WORKFLOW_PHASES.SIMPLIFIER_RUNNING) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
`Simplifier handoff requires simplifier-running state, found "${state.phase}".`,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const input = { handoff, specId, projectRoot: paths.getProjectRoot() };
|
|
43
|
+
assertSimplifierHandoff(input);
|
|
44
|
+
|
|
45
|
+
const handoffPath = paths.getSimplifierHandoffPath(specId);
|
|
46
|
+
await mkdir(dirname(handoffPath), { recursive: true });
|
|
47
|
+
await writeJson({ path: handoffPath, data: input.handoff });
|
|
48
|
+
|
|
49
|
+
const nextState = transitionWorkflow({
|
|
50
|
+
state,
|
|
51
|
+
event: WORKFLOW_EVENTS.SIMPLIFIER_DONE,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
await writeWorkflowState({ path: workflowPath, state: nextState });
|
|
55
|
+
|
|
56
|
+
return { handoff: input.handoff, handoffPath, state: nextState };
|
|
57
|
+
};
|
|
@@ -17,6 +17,7 @@ export const WORKFLOW_PHASES = {
|
|
|
17
17
|
READY_FOR_VERIFIER: 'ready-for-verifier',
|
|
18
18
|
VERIFIER_RUNNING: 'verifier-running',
|
|
19
19
|
FINDING_DECISIONS: 'finding-decisions',
|
|
20
|
+
SIMPLIFIER_RUNNING: 'simplifier-running',
|
|
20
21
|
CANDIDATE_READY: 'candidate-ready',
|
|
21
22
|
} as const;
|
|
22
23
|
|
|
@@ -30,6 +31,8 @@ export const WORKFLOW_EVENTS = {
|
|
|
30
31
|
RUN_VERIFIER: 'run-verifier',
|
|
31
32
|
VERIFIER_FOUND_FINDINGS: 'verifier-found-findings',
|
|
32
33
|
VERIFIER_DONE: 'verifier-done',
|
|
34
|
+
SIMPLIFIER_DONE: 'simplifier-done',
|
|
35
|
+
SIMPLIFIER_SKIPPED: 'simplifier-skipped',
|
|
33
36
|
OWNER_REJECT_FINDINGS: 'owner-reject-findings',
|
|
34
37
|
OWNER_REQUEST_FIXES: 'owner-request-fixes',
|
|
35
38
|
} as const;
|
|
@@ -42,6 +45,7 @@ export const WorkflowPhaseSchema = Type.Union([
|
|
|
42
45
|
Type.Literal(WORKFLOW_PHASES.READY_FOR_VERIFIER),
|
|
43
46
|
Type.Literal(WORKFLOW_PHASES.VERIFIER_RUNNING),
|
|
44
47
|
Type.Literal(WORKFLOW_PHASES.FINDING_DECISIONS),
|
|
48
|
+
Type.Literal(WORKFLOW_PHASES.SIMPLIFIER_RUNNING),
|
|
45
49
|
Type.Literal(WORKFLOW_PHASES.CANDIDATE_READY),
|
|
46
50
|
]);
|
|
47
51
|
|
|
@@ -55,10 +55,18 @@ export const WORKFLOW_TRANSITIONS: Record<WorkflowEvent, WorkflowTransition> = {
|
|
|
55
55
|
},
|
|
56
56
|
[WORKFLOW_EVENTS.VERIFIER_DONE]: {
|
|
57
57
|
from: [WORKFLOW_PHASES.VERIFIER_RUNNING],
|
|
58
|
-
to: WORKFLOW_PHASES.
|
|
58
|
+
to: WORKFLOW_PHASES.SIMPLIFIER_RUNNING,
|
|
59
59
|
},
|
|
60
60
|
[WORKFLOW_EVENTS.OWNER_REJECT_FINDINGS]: {
|
|
61
61
|
from: [WORKFLOW_PHASES.FINDING_DECISIONS],
|
|
62
|
+
to: WORKFLOW_PHASES.SIMPLIFIER_RUNNING,
|
|
63
|
+
},
|
|
64
|
+
[WORKFLOW_EVENTS.SIMPLIFIER_DONE]: {
|
|
65
|
+
from: [WORKFLOW_PHASES.SIMPLIFIER_RUNNING],
|
|
66
|
+
to: WORKFLOW_PHASES.CANDIDATE_READY,
|
|
67
|
+
},
|
|
68
|
+
[WORKFLOW_EVENTS.SIMPLIFIER_SKIPPED]: {
|
|
69
|
+
from: [WORKFLOW_PHASES.SIMPLIFIER_RUNNING],
|
|
62
70
|
to: WORKFLOW_PHASES.CANDIDATE_READY,
|
|
63
71
|
},
|
|
64
72
|
[WORKFLOW_EVENTS.OWNER_REQUEST_FIXES]: {
|
|
@@ -77,22 +85,21 @@ export const transitionWorkflow = ({
|
|
|
77
85
|
event,
|
|
78
86
|
}: TransitionWorkflowInput): WorkflowState => {
|
|
79
87
|
assertWorkflowState(state);
|
|
80
|
-
const current = state;
|
|
81
88
|
const transition = WORKFLOW_TRANSITIONS[event];
|
|
82
89
|
|
|
83
90
|
if (transition === undefined) {
|
|
84
91
|
throw new Error(`Unknown workflow event: "${event}".`);
|
|
85
92
|
}
|
|
86
93
|
|
|
87
|
-
if (!transition.from.includes(
|
|
94
|
+
if (!transition.from.includes(state.phase)) {
|
|
88
95
|
throw new Error(
|
|
89
|
-
`Workflow event "${event}" is not allowed from phase "${
|
|
96
|
+
`Workflow event "${event}" is not allowed from phase "${state.phase}".`,
|
|
90
97
|
);
|
|
91
98
|
}
|
|
92
99
|
|
|
93
100
|
return {
|
|
94
|
-
version:
|
|
95
|
-
specId:
|
|
101
|
+
version: state.version,
|
|
102
|
+
specId: state.specId,
|
|
96
103
|
phase: transition.to,
|
|
97
104
|
};
|
|
98
105
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Objective: Resolve the agent responsible for a workflow phase.
|
|
3
|
-
* Used: When workflow logic maps persisted phases to
|
|
3
|
+
* Used: When workflow logic maps persisted phases to agent roles.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import type { WorkflowRole } from '#workflow/roles.ts';
|
|
@@ -29,5 +29,9 @@ export const getAgentRole = (
|
|
|
29
29
|
return WORKFLOW_ROLES.VERIFIER;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
if (phase === WORKFLOW_PHASES.SIMPLIFIER_RUNNING) {
|
|
33
|
+
return WORKFLOW_ROLES.SIMPLIFIER;
|
|
34
|
+
}
|
|
35
|
+
|
|
32
36
|
return null;
|
|
33
37
|
};
|
|
@@ -18,56 +18,42 @@ import { writeWorkflowState } from '#workflow/state/writeWorkflowState.ts';
|
|
|
18
18
|
import { transitionWorkflow } from '#workflow/transitions.ts';
|
|
19
19
|
import { getWorkflowEventByVerifierHandoff } from '#workflow/verifier/utils/getWorkflowEventByVerifierHandoff.ts';
|
|
20
20
|
|
|
21
|
-
type
|
|
22
|
-
findingId: string;
|
|
23
|
-
decision: typeof FINDING_DECISIONS.REJECT;
|
|
24
|
-
reason: string;
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
type FindingDecisionApproved = {
|
|
28
|
-
findingId: string;
|
|
29
|
-
decision: typeof FINDING_DECISIONS.FIX_CODE;
|
|
30
|
-
};
|
|
21
|
+
type SavedFindingDecision = NonNullable<VerifierFinding['decision']>;
|
|
31
22
|
|
|
32
|
-
type
|
|
23
|
+
export type FindingDecision = SavedFindingDecision & {
|
|
33
24
|
findingId: string;
|
|
34
|
-
decision: typeof FINDING_DECISIONS.REVISE_SPEC;
|
|
35
|
-
reason: string;
|
|
36
|
-
specChange: string;
|
|
37
25
|
};
|
|
38
26
|
|
|
39
|
-
export type FindingDecision =
|
|
40
|
-
| FindingDecisionApproved
|
|
41
|
-
| FindingDecisionRejected
|
|
42
|
-
| FindingDecisionRevision;
|
|
43
|
-
|
|
44
27
|
export type ResolvedFindings = {
|
|
45
28
|
findings: VerifierFinding[];
|
|
46
29
|
state: WorkflowState;
|
|
47
30
|
projectRoot: string;
|
|
48
31
|
};
|
|
49
32
|
|
|
50
|
-
type
|
|
33
|
+
type ApplyFindingDecisionsInput = {
|
|
51
34
|
decisions: FindingDecision[];
|
|
52
35
|
findings: VerifierFinding[];
|
|
53
36
|
};
|
|
54
37
|
|
|
55
|
-
|
|
38
|
+
const applyFindingDecisions = ({
|
|
39
|
+
decisions,
|
|
40
|
+
findings,
|
|
41
|
+
}: ApplyFindingDecisionsInput): VerifierFinding[] => {
|
|
56
42
|
const expectedIds = new Set(findings.map(({ id }) => id));
|
|
57
|
-
const
|
|
43
|
+
const decisionsById = new Map<string, FindingDecision>();
|
|
58
44
|
|
|
59
45
|
for (const decision of decisions) {
|
|
60
46
|
if (!expectedIds.has(decision.findingId)) {
|
|
61
47
|
throw new Error(`Unknown finding ID: "${decision.findingId}".`);
|
|
62
48
|
}
|
|
63
49
|
|
|
64
|
-
if (
|
|
50
|
+
if (decisionsById.has(decision.findingId)) {
|
|
65
51
|
throw new Error(
|
|
66
52
|
`Duplicate decision for finding "${decision.findingId}".`,
|
|
67
53
|
);
|
|
68
54
|
}
|
|
69
55
|
|
|
70
|
-
|
|
56
|
+
decisionsById.set(decision.findingId, decision);
|
|
71
57
|
|
|
72
58
|
if (
|
|
73
59
|
decision.decision === FINDING_DECISIONS.REJECT &&
|
|
@@ -79,12 +65,17 @@ function assertDecisions({ decisions, findings }: AssertDecisionsInput): void {
|
|
|
79
65
|
}
|
|
80
66
|
}
|
|
81
67
|
|
|
82
|
-
const missing = findings.find(({ id }) => !
|
|
68
|
+
const missing = findings.find(({ id }) => !decisionsById.has(id));
|
|
83
69
|
|
|
84
70
|
if (missing !== undefined) {
|
|
85
71
|
throw new Error(`Missing decision for finding "${missing.id}".`);
|
|
86
72
|
}
|
|
87
|
-
|
|
73
|
+
|
|
74
|
+
return findings.map((finding) => ({
|
|
75
|
+
...finding,
|
|
76
|
+
decision: getSavedFindingDecision(decisionsById.get(finding.id)),
|
|
77
|
+
}));
|
|
78
|
+
};
|
|
88
79
|
|
|
89
80
|
type FindingWorkflow = {
|
|
90
81
|
state: WorkflowState;
|
|
@@ -134,62 +125,25 @@ const readFindingWorkflow = async ({
|
|
|
134
125
|
return { state, handoff };
|
|
135
126
|
};
|
|
136
127
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
assertDecisions({ decisions, findings: handoff.findings });
|
|
148
|
-
|
|
149
|
-
const decisionsById = new Map(
|
|
150
|
-
decisions.map((decision) => [decision.findingId, decision]),
|
|
151
|
-
);
|
|
152
|
-
|
|
153
|
-
const findings = handoff.findings.map((finding) => {
|
|
154
|
-
const decision = decisionsById.get(finding.id);
|
|
155
|
-
|
|
156
|
-
if (decision?.decision === FINDING_DECISIONS.REVISE_SPEC) {
|
|
157
|
-
return {
|
|
158
|
-
...finding,
|
|
159
|
-
decision: {
|
|
160
|
-
decision: FINDING_DECISIONS.REVISE_SPEC,
|
|
161
|
-
reason: decision.reason,
|
|
162
|
-
specChange: decision.specChange,
|
|
163
|
-
},
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
if (decision?.decision === FINDING_DECISIONS.REJECT) {
|
|
168
|
-
return {
|
|
169
|
-
...finding,
|
|
170
|
-
decision: {
|
|
171
|
-
decision: FINDING_DECISIONS.REJECT,
|
|
172
|
-
reason: decision.reason.trim(),
|
|
173
|
-
},
|
|
174
|
-
};
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
return { ...finding, decision: { decision: FINDING_DECISIONS.FIX_CODE } };
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
const nextHandoff = { ...handoff, findings };
|
|
181
|
-
|
|
182
|
-
const nextState = transitionWorkflow({
|
|
183
|
-
state,
|
|
184
|
-
event: getWorkflowEventByVerifierHandoff(nextHandoff),
|
|
185
|
-
});
|
|
128
|
+
const getSavedFindingDecision = (
|
|
129
|
+
decision: FindingDecision | undefined,
|
|
130
|
+
): SavedFindingDecision => {
|
|
131
|
+
if (decision?.decision === FINDING_DECISIONS.REVISE_SPEC) {
|
|
132
|
+
return {
|
|
133
|
+
decision: FINDING_DECISIONS.REVISE_SPEC,
|
|
134
|
+
reason: decision.reason.trim(),
|
|
135
|
+
specChange: decision.specChange,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
186
138
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
139
|
+
if (decision?.decision === FINDING_DECISIONS.REJECT) {
|
|
140
|
+
return {
|
|
141
|
+
decision: FINDING_DECISIONS.REJECT,
|
|
142
|
+
reason: decision.reason.trim(),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
191
145
|
|
|
192
|
-
return {
|
|
146
|
+
return { decision: FINDING_DECISIONS.FIX_CODE };
|
|
193
147
|
};
|
|
194
148
|
|
|
195
149
|
type ResolveFindingsInput = {
|
|
@@ -203,23 +157,35 @@ export const resolveFindings = async ({
|
|
|
203
157
|
specId,
|
|
204
158
|
decisions,
|
|
205
159
|
}: ResolveFindingsInput): Promise<ResolvedFindings> => {
|
|
206
|
-
const
|
|
160
|
+
const { state, handoff } = await readFindingWorkflow({ paths, specId });
|
|
207
161
|
|
|
208
|
-
const
|
|
209
|
-
workflow: currentWorkflow,
|
|
162
|
+
const findings = applyFindingDecisions({
|
|
210
163
|
decisions,
|
|
164
|
+
findings: handoff.findings,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const nextHandoff = { ...handoff, findings };
|
|
168
|
+
|
|
169
|
+
const nextState = transitionWorkflow({
|
|
170
|
+
state,
|
|
171
|
+
event: getWorkflowEventByVerifierHandoff(nextHandoff),
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
assertVerifierHandoff({
|
|
175
|
+
handoff: nextHandoff,
|
|
176
|
+
specId: state.specId,
|
|
211
177
|
});
|
|
212
178
|
|
|
213
179
|
const handoffPath = await paths.getActiveVerifierHandoffPath(specId);
|
|
214
|
-
await writeJson({ path: handoffPath, data:
|
|
180
|
+
await writeJson({ path: handoffPath, data: nextHandoff });
|
|
215
181
|
await writeWorkflowState({
|
|
216
182
|
path: paths.getWorkflowPath(specId),
|
|
217
|
-
state:
|
|
183
|
+
state: nextState,
|
|
218
184
|
});
|
|
219
185
|
|
|
220
186
|
return {
|
|
221
|
-
findings
|
|
222
|
-
state:
|
|
187
|
+
findings,
|
|
188
|
+
state: nextState,
|
|
223
189
|
projectRoot: paths.getProjectRoot(),
|
|
224
190
|
};
|
|
225
191
|
};
|
package/templates/spec.md
CHANGED
|
@@ -25,7 +25,7 @@ Leave local implementation choices, test code, fixtures, mocks, and commands to
|
|
|
25
25
|
|
|
26
26
|
### Edge cases and error handling
|
|
27
27
|
|
|
28
|
-
<Describe boundary conditions, failures, and required recovery behavior not already covered above.
|
|
28
|
+
<Describe boundary conditions, failures, and required recovery behavior not already covered above. Keep these requirements even when existing tests cover their scenarios.>
|
|
29
29
|
|
|
30
30
|
## 3. Technical decisions and prototypes
|
|
31
31
|
|
|
@@ -45,16 +45,20 @@ Leave local implementation choices, test code, fixtures, mocks, and commands to
|
|
|
45
45
|
|
|
46
46
|
## 4. Acceptance criteria
|
|
47
47
|
|
|
48
|
-
<
|
|
48
|
+
<Inspect existing tests before writing criteria. Omit a criterion only if a test covers the same starting conditions, action, and expected result. Keep criteria when coverage is partial or uncertain. During review, remove criteria already covered by tests. Keep numbering continuous from AC1 and update affected references. Keep required behavior in section 2 even when its criterion is omitted.>
|
|
49
|
+
|
|
50
|
+
<Give each criterion a unique ID, one observable claim, and one coverage label. A probe is the scenario used to check that claim. Describe it in plain language. Use success for a valid action that produces the required result. Use failure for an invalid action that is correctly rejected. Use unchanged when the criterion explicitly checks existing behavior that the change must preserve. The label describes the scenario, not the test outcome. A failure scenario passes when the invalid action is correctly rejected. Do not require all three categories.>
|
|
49
51
|
|
|
50
52
|
### AC1: <short observable claim>
|
|
51
53
|
|
|
52
|
-
1.
|
|
53
|
-
2.
|
|
54
|
-
3.
|
|
54
|
+
1. Coverage: <success | failure | unchanged>
|
|
55
|
+
2. Probe: <starting conditions and action or observation, without prescribing test implementation>
|
|
56
|
+
3. Expected result: <observable and measurable outcome>
|
|
57
|
+
4. Example: <specific starting conditions, concrete input or action, and exact expected result for this probe, not a restatement of the claim>
|
|
55
58
|
|
|
56
59
|
<!--
|
|
57
60
|
Example:
|
|
61
|
+
Coverage: success
|
|
58
62
|
Probe: Save a change to an item's title, then reopen the item.
|
|
59
63
|
Expected result: The saved title is still present.
|
|
60
64
|
Example: An item's title is "Draft". Change it to "Ready", save, and reopen the item. The title is "Ready".
|