@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.
Files changed (43) hide show
  1. package/ADR.md +53 -9
  2. package/CHANGELOG.md +79 -36
  3. package/MISSION.md +4 -2
  4. package/README.md +31 -15
  5. package/ROADMAP.md +150 -61
  6. package/TECH_STACK.md +12 -11
  7. package/agents/simplifier.md +214 -0
  8. package/agents/verifier.md +1 -16
  9. package/docs/configuration.md +27 -16
  10. package/docs/glossary.md +41 -36
  11. package/docs/subagent-integration.md +23 -13
  12. package/docs/workflow.md +103 -71
  13. package/extensions/maestro-subagent.ts +3 -1
  14. package/extensions/maestro.ts +10 -0
  15. package/package.json +1 -1
  16. package/src/MaestroPaths.ts +8 -32
  17. package/src/artifacts/builder-handoff/assertBuilderHandoff.ts +23 -29
  18. package/src/artifacts/simplifier-handoff/assertSimplifierHandoff.ts +56 -0
  19. package/src/artifacts/simplifier-handoff/readSimplifierHandoff.ts +29 -0
  20. package/src/artifacts/simplifier-handoff/schema.ts +26 -0
  21. package/src/artifacts/verifier-handoff/assertVerifierHandoff.ts +14 -18
  22. package/src/config/assertConfiguration.ts +1 -1
  23. package/src/config/defaults.ts +7 -0
  24. package/src/config/loadConfiguration.ts +21 -50
  25. package/src/config/schema.ts +3 -0
  26. package/src/maestro/checks/assertEnvironment.ts +7 -3
  27. package/src/maestro/instructions/getMaestroInstructions.ts +27 -9
  28. package/src/maestro/status/formatMaestroStatus.ts +3 -0
  29. package/src/specs/create.ts +7 -24
  30. package/src/tools/child/record-simplifier-handoff.ts +73 -0
  31. package/src/tools/main/resolve-findings.ts +4 -3
  32. package/src/tools/main/run-builder.ts +38 -48
  33. package/src/tools/main/run-simplifier.ts +182 -0
  34. package/src/tools/main/run-verifier.ts +14 -25
  35. package/src/utils/path-within-or-equal.ts +3 -4
  36. package/src/workflow/builder/resolveEscalations.ts +43 -27
  37. package/src/workflow/roles.ts +2 -1
  38. package/src/workflow/simplifier/completeSimplifierPass.ts +57 -0
  39. package/src/workflow/state/schema.ts +4 -0
  40. package/src/workflow/transitions.ts +13 -6
  41. package/src/workflow/utils/getAgentRole.ts +5 -1
  42. package/src/workflow/verifier/resolveFindings.ts +53 -87
  43. 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.CANDIDATE_READY,
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(current.phase)) {
94
+ if (!transition.from.includes(state.phase)) {
88
95
  throw new Error(
89
- `Workflow event "${event}" is not allowed from phase "${current.phase}".`,
96
+ `Workflow event "${event}" is not allowed from phase "${state.phase}".`,
90
97
  );
91
98
  }
92
99
 
93
100
  return {
94
- version: current.version,
95
- specId: current.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 builder or verifier roles.
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 FindingDecisionRejected = {
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 FindingDecisionRevision = {
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 AssertDecisionsInput = {
33
+ type ApplyFindingDecisionsInput = {
51
34
  decisions: FindingDecision[];
52
35
  findings: VerifierFinding[];
53
36
  };
54
37
 
55
- function assertDecisions({ decisions, findings }: AssertDecisionsInput): void {
38
+ const applyFindingDecisions = ({
39
+ decisions,
40
+ findings,
41
+ }: ApplyFindingDecisionsInput): VerifierFinding[] => {
56
42
  const expectedIds = new Set(findings.map(({ id }) => id));
57
- const seenIds = new Set<string>();
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 (seenIds.has(decision.findingId)) {
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
- seenIds.add(decision.findingId);
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 }) => !seenIds.has(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
- type BuildFindingWorkflowInput = {
138
- workflow: FindingWorkflow;
139
- decisions: FindingDecision[];
140
- };
141
-
142
- const buildFindingWorkflow = ({
143
- workflow,
144
- decisions,
145
- }: BuildFindingWorkflowInput): FindingWorkflow => {
146
- const { state, handoff } = workflow;
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
- assertVerifierHandoff({
188
- handoff: nextHandoff,
189
- specId: state.specId,
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 { state: nextState, handoff: nextHandoff };
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 currentWorkflow = await readFindingWorkflow({ paths, specId });
160
+ const { state, handoff } = await readFindingWorkflow({ paths, specId });
207
161
 
208
- const workflow = buildFindingWorkflow({
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: workflow.handoff });
180
+ await writeJson({ path: handoffPath, data: nextHandoff });
215
181
  await writeWorkflowState({
216
182
  path: paths.getWorkflowPath(specId),
217
- state: workflow.state,
183
+ state: nextState,
218
184
  });
219
185
 
220
186
  return {
221
- findings: workflow.handoff.findings,
222
- state: workflow.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. Cover each required case in the acceptance criteria.>
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
- <Give each criterion a unique ID and one observable claim. A probe is the scenario used to check that claim. Describe it in plain language.>
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. Probe: <starting conditions and action or observation, without prescribing test implementation>
53
- 2. Expected result: <observable and measurable outcome>
54
- 3. Example: <specific starting conditions, concrete input or action, and exact expected result for this probe, not a restatement of the claim>
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".