@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
@@ -29,6 +29,10 @@ import {
29
29
  RUN_BUILDER_TOOL,
30
30
  registerRunBuilderTool,
31
31
  } from '#tools/main/run-builder.ts';
32
+ import {
33
+ RUN_SIMPLIFIER_TOOL,
34
+ registerRunSimplifierTool,
35
+ } from '#tools/main/run-simplifier.ts';
32
36
  import {
33
37
  RUN_VERIFIER_TOOL,
34
38
  registerRunVerifierTool,
@@ -41,6 +45,7 @@ const MAIN_TOOL_NAMES: readonly string[] = [
41
45
  RESOLVE_ESCALATIONS_TOOL.NAME,
42
46
  RUN_VERIFIER_TOOL.NAME,
43
47
  RESOLVE_FINDINGS_TOOL.NAME,
48
+ RUN_SIMPLIFIER_TOOL.NAME,
44
49
  ];
45
50
 
46
51
  export default (pi: ExtensionAPI): void => {
@@ -50,6 +55,7 @@ export default (pi: ExtensionAPI): void => {
50
55
  registerResolveEscalationsTool(pi);
51
56
  registerRunVerifierTool(pi);
52
57
  registerResolveFindingsTool(pi);
58
+ registerRunSimplifierTool(pi);
53
59
 
54
60
  const syncMainTools = (): void => {
55
61
  const foreignTools = pi
@@ -104,6 +110,10 @@ export default (pi: ExtensionAPI): void => {
104
110
  'accent',
105
111
  `Verifier (${config.verifier.model} ${config.verifier.thinking})`,
106
112
  ),
113
+ context.ui.theme.fg(
114
+ 'accent',
115
+ `Simplifier (${config.simplifier.model} ${config.simplifier.thinking})`,
116
+ ),
107
117
  ].join('\n'),
108
118
  'info',
109
119
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@emiliosp/pi-maestro",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "A spec-driven multiagent development workflow for Pi.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -100,36 +100,15 @@ export class MaestroPaths {
100
100
  }
101
101
 
102
102
  public getSpecFilePath(specId: string): string {
103
- assertSpecId(specId);
104
-
105
- return resolve(
106
- this.projectRoot,
107
- this.specDirectoryFromRoot,
108
- specId,
109
- PATHS.SPEC_FILE,
110
- );
103
+ return resolve(this.getSpecPath(specId), PATHS.SPEC_FILE);
111
104
  }
112
105
 
113
106
  public getWorkflowPath(specId: string): string {
114
- assertSpecId(specId);
115
-
116
- return resolve(
117
- this.projectRoot,
118
- this.specDirectoryFromRoot,
119
- specId,
120
- PATHS.WORKFLOW_FILE,
121
- );
107
+ return resolve(this.getSpecPath(specId), PATHS.WORKFLOW_FILE);
122
108
  }
123
109
 
124
110
  public getHandoffsPath(specId: string): string {
125
- assertSpecId(specId);
126
-
127
- return resolve(
128
- this.projectRoot,
129
- this.specDirectoryFromRoot,
130
- specId,
131
- PATHS.HANDOFFS_PATH,
132
- );
111
+ return resolve(this.getSpecPath(specId), PATHS.HANDOFFS_PATH);
133
112
  }
134
113
 
135
114
  public getBuilderHandoffsPath(specId: string): string {
@@ -207,15 +186,12 @@ export class MaestroPaths {
207
186
  });
208
187
  }
209
188
 
210
- public getPrototypesPath(specId: string): string {
211
- assertSpecId(specId);
189
+ public getSimplifierHandoffPath(specId: string): string {
190
+ return resolve(this.getSpecPath(specId), 'handoffs/simplifier/S.json');
191
+ }
212
192
 
213
- return resolve(
214
- this.projectRoot,
215
- this.specDirectoryFromRoot,
216
- specId,
217
- PATHS.PROTOTYPES_PATH,
218
- );
193
+ public getPrototypesPath(specId: string): string {
194
+ return resolve(this.getSpecPath(specId), PATHS.PROTOTYPES_PATH);
219
195
  }
220
196
 
221
197
  public getPrototypePath({
@@ -66,37 +66,31 @@ export function assertBuilderHandoff(
66
66
  throw new Error('Done builder handoff requires every probe to pass.');
67
67
  }
68
68
 
69
- if (handoff.status === BUILDER_HANDOFF_STATUSES.ESCALATION) {
70
- for (const [index, escalation] of handoff.escalations.entries()) {
71
- if (escalation.id !== `E${index + 1}`) {
72
- throw new Error('Escalation IDs must be sequential in array order.');
73
- }
74
-
75
- const optionIds = new Set(escalation.options.map(({ id }) => id));
76
-
77
- if (optionIds.size !== escalation.options.length) {
78
- throw new Error('Escalation option IDs must be unique.');
79
- }
80
-
81
- if (
82
- escalation.recommendation !== null &&
83
- !optionIds.has(escalation.recommendation.optionId)
84
- ) {
85
- throw new Error('Escalation recommendation references unknown option.');
86
- }
87
-
88
- if (
89
- escalation.resolution !== null &&
90
- escalation.resolution.selectedOptionId !== null &&
91
- !optionIds.has(escalation.resolution.selectedOptionId)
92
- ) {
93
- throw new Error('Escalation resolution references unknown option.');
94
- }
69
+ for (const [index, escalation] of handoff.escalations.entries()) {
70
+ if (escalation.id !== `E${index + 1}`) {
71
+ throw new Error('Escalation IDs must be sequential in array order.');
72
+ }
73
+
74
+ const optionIds = new Set(escalation.options.map(({ id }) => id));
75
+
76
+ if (optionIds.size !== escalation.options.length) {
77
+ throw new Error('Escalation option IDs must be unique.');
95
78
  }
96
- }
97
79
 
98
- if (!isValidSpecId(specId)) {
99
- throw new Error(`Invalid expected builder handoff spec ID: "${specId}".`);
80
+ if (
81
+ escalation.recommendation !== null &&
82
+ !optionIds.has(escalation.recommendation.optionId)
83
+ ) {
84
+ throw new Error('Escalation recommendation references unknown option.');
85
+ }
86
+
87
+ if (
88
+ escalation.resolution !== null &&
89
+ escalation.resolution.selectedOptionId !== null &&
90
+ !optionIds.has(escalation.resolution.selectedOptionId)
91
+ ) {
92
+ throw new Error('Escalation resolution references unknown option.');
93
+ }
100
94
  }
101
95
 
102
96
  if (handoff.specId !== specId) {
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Objective: Validate the simplifier result and its project-relative file paths.
3
+ * Used: Before reading or saving a simplifier handoff.
4
+ */
5
+
6
+ import { isAbsolute, resolve } from 'node:path';
7
+ import { Value } from 'typebox/value';
8
+ import {
9
+ type SimplifierHandoff,
10
+ SimplifierHandoffSchema,
11
+ } from '#artifacts/simplifier-handoff/schema.ts';
12
+ import { isPathStrictlyWithin } from '#utils/path-strictly-within.ts';
13
+
14
+ type AssertSimplifierHandoffInput<Handoff = unknown> = {
15
+ handoff: Handoff;
16
+ specId: string;
17
+ projectRoot: string;
18
+ };
19
+
20
+ function assertSimplifierHandoffSchema(
21
+ input: unknown,
22
+ ): asserts input is SimplifierHandoff {
23
+ const [error] = Value.Errors(SimplifierHandoffSchema, input);
24
+
25
+ if (error !== undefined) {
26
+ throw new Error(`Invalid simplifier handoff: ${error.message}.`);
27
+ }
28
+ }
29
+
30
+ export function assertSimplifierHandoff(
31
+ input: AssertSimplifierHandoffInput,
32
+ ): asserts input is AssertSimplifierHandoffInput<SimplifierHandoff> {
33
+ assertSimplifierHandoffSchema(input.handoff);
34
+ const { handoff } = input;
35
+
36
+ if (handoff.specId !== input.specId) {
37
+ throw new Error(
38
+ `Simplifier handoff spec ID mismatch: expected "${input.specId}", found "${handoff.specId}".`,
39
+ );
40
+ }
41
+
42
+ for (const file of handoff.refactoredFiles) {
43
+ if (
44
+ file.includes('\0') ||
45
+ isAbsolute(file) ||
46
+ !isPathStrictlyWithin({
47
+ parent: input.projectRoot,
48
+ path: resolve(input.projectRoot, file),
49
+ })
50
+ ) {
51
+ throw new Error(
52
+ `Refactored file path must be relative and stay inside the project root: "${file}".`,
53
+ );
54
+ }
55
+ }
56
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Objective: Read the fixed simplifier handoff and validate its identity.
3
+ * Used: After foreground simplifier execution completes.
4
+ */
5
+
6
+ import { assertSimplifierHandoff } from '#artifacts/simplifier-handoff/assertSimplifierHandoff.ts';
7
+ import { readJsonFile } from '#utils/read-json.ts';
8
+
9
+ type ReadSimplifierHandoffInput = {
10
+ path: string;
11
+ specId: string;
12
+ projectRoot: string;
13
+ };
14
+
15
+ export const readSimplifierHandoff = async ({
16
+ path,
17
+ specId,
18
+ projectRoot,
19
+ }: ReadSimplifierHandoffInput) => {
20
+ const input = {
21
+ handoff: await readJsonFile({ path, description: 'Simplifier handoff' }),
22
+ specId,
23
+ projectRoot,
24
+ };
25
+
26
+ assertSimplifierHandoff(input);
27
+
28
+ return input.handoff;
29
+ };
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Objective: Define the fixed simplifier result contract.
3
+ * Used: When a simplifier submits or Maestro reads its handoff.
4
+ */
5
+
6
+ import { type Static, Type } from 'typebox';
7
+ import { SPEC_ID_PATTERN } from '#ids/isValidSpecId.ts';
8
+
9
+ export const SIMPLIFIER_HANDOFF_VERSION = '1.0.0';
10
+
11
+ export const SimplifierSubmissionFields = {
12
+ specId: Type.String({ pattern: SPEC_ID_PATTERN.source }),
13
+ refactoredFiles: Type.Array(Type.String({ minLength: 1 })),
14
+ summary: Type.String({ minLength: 1 }),
15
+ notes: Type.Array(Type.String()),
16
+ };
17
+
18
+ export const SimplifierHandoffSchema = Type.Object(
19
+ {
20
+ version: Type.Literal(SIMPLIFIER_HANDOFF_VERSION),
21
+ ...SimplifierSubmissionFields,
22
+ },
23
+ { additionalProperties: false },
24
+ );
25
+
26
+ export type SimplifierHandoff = Static<typeof SimplifierHandoffSchema>;
@@ -40,18 +40,17 @@ export function assertVerifierHandoff(
40
40
  throw new Error(`Invalid verifier handoff spec ID: "${handoff.specId}".`);
41
41
  }
42
42
 
43
- if (
44
- new Set(handoff.acceptanceCriteria.map((criterion) => criterion.id))
45
- .size !== handoff.acceptanceCriteria.length
46
- ) {
43
+ const acceptanceCriterionIds = new Set(
44
+ handoff.acceptanceCriteria.map((criterion) => criterion.id),
45
+ );
46
+
47
+ if (acceptanceCriterionIds.size !== handoff.acceptanceCriteria.length) {
47
48
  throw new Error(
48
49
  'Verifier handoff acceptance criterion IDs must be unique.',
49
50
  );
50
51
  }
51
52
 
52
- const acceptanceCriterionIds = new Set(
53
- handoff.acceptanceCriteria.map((criterion) => criterion.id),
54
- );
53
+ const criterionIdsWithFindings = new Set<string>();
55
54
 
56
55
  for (const [index, finding] of handoff.findings.entries()) {
57
56
  const expectedId = `F${index + 1}`;
@@ -62,26 +61,23 @@ export function assertVerifierHandoff(
62
61
  );
63
62
  }
64
63
 
65
- if (
66
- finding.acceptanceCriterion !== null &&
67
- !acceptanceCriterionIds.has(finding.acceptanceCriterion)
68
- ) {
64
+ if (finding.acceptanceCriterion === null) {
65
+ continue;
66
+ }
67
+
68
+ if (!acceptanceCriterionIds.has(finding.acceptanceCriterion)) {
69
69
  throw new Error(
70
70
  `Verifier handoff finding "${finding.id}" references unknown acceptance criterion "${finding.acceptanceCriterion}".`,
71
71
  );
72
72
  }
73
- }
74
73
 
75
- const findingCriteria = new Set(
76
- handoff.findings.flatMap((finding) =>
77
- finding.acceptanceCriterion === null ? [] : [finding.acceptanceCriterion],
78
- ),
79
- );
74
+ criterionIdsWithFindings.add(finding.acceptanceCriterion);
75
+ }
80
76
 
81
77
  for (const criterion of handoff.acceptanceCriteria) {
82
78
  if (
83
79
  criterion.probeStatus !== PROBE_STATUSES.PASSED &&
84
- !findingCriteria.has(criterion.id)
80
+ !criterionIdsWithFindings.has(criterion.id)
85
81
  ) {
86
82
  throw new Error(
87
83
  `Verifier handoff incomplete acceptance criterion "${criterion.id}" requires a finding.`,
@@ -41,7 +41,7 @@ const formatError = (error: SchemaValidationError): string => {
41
41
  return `Invalid model identifier at "${path}". Must use "provider/model" format.`;
42
42
  }
43
43
 
44
- if (path === 'builder' || path === 'verifier') {
44
+ if (path === 'builder' || path === 'verifier' || path === 'simplifier') {
45
45
  return `Invalid ${path} configuration: expected an object.`;
46
46
  }
47
47
 
@@ -21,6 +21,8 @@ export const DEFAULT_VERIFIER_MODEL = 'openai-codex/gpt-6.1-sol';
21
21
 
22
22
  export const DEFAULT_VERIFIER_THINKING = THINKING_LEVELS.HIGH;
23
23
 
24
+ export const DEFAULT_SIMPLIFIER_MODEL = 'openai-codex/gpt-6.1-sol';
25
+
24
26
  export const DEFAULT_TIMEOUT_MINUTES = 60;
25
27
 
26
28
  export const CONFIG_DIRECTORY_NAME = '.pi';
@@ -37,6 +39,11 @@ export const DEFAULT_CONFIG: MaestroConfig = {
37
39
  thinking: DEFAULT_BUILDER_THINKING,
38
40
  timeoutMinutes: DEFAULT_TIMEOUT_MINUTES,
39
41
  },
42
+ simplifier: {
43
+ model: DEFAULT_SIMPLIFIER_MODEL,
44
+ thinking: THINKING_LEVELS.HIGH,
45
+ timeoutMinutes: DEFAULT_TIMEOUT_MINUTES,
46
+ },
40
47
  verifier: {
41
48
  model: DEFAULT_VERIFIER_MODEL,
42
49
  thinking: DEFAULT_VERIFIER_THINKING,
@@ -11,63 +11,35 @@ 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
- const resolved: MaestroConfig = {
16
- version: DEFAULT_CONFIG.version,
17
- specDirectory: input.specDirectory ?? DEFAULT_CONFIG.specDirectory,
18
- builder: {
19
- model: input.builder?.model ?? DEFAULT_CONFIG.builder.model,
20
- thinking: input.builder?.thinking ?? DEFAULT_CONFIG.builder.thinking,
21
- timeoutMinutes:
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
+ simplifier: { ...DEFAULT_CONFIG.simplifier, ...input.simplifier },
20
+ });
21
+
22
+ type ResolveSpecDirectoryInput = {
55
23
  projectRoot: string;
56
24
  directory: string;
57
- name: string;
58
25
  };
59
26
 
60
- const resolveSafeDirectory = ({
27
+ const resolveSpecDirectory = ({
61
28
  projectRoot,
62
29
  directory,
63
- name,
64
- }: ResolveSafeDirectoryInput): string => {
65
- assertSafeDirectoryInput({ value: directory, name });
30
+ }: ResolveSpecDirectoryInput): string => {
31
+ if (directory.includes('\0')) {
32
+ throw new Error('specDirectory must not contain a null byte.');
33
+ }
34
+
35
+ if (isAbsolute(directory)) {
36
+ throw new Error('specDirectory must be relative to the project root.');
37
+ }
66
38
 
67
39
  const requestedDirectory = resolve(projectRoot, directory);
68
40
 
69
41
  if (requestedDirectory === projectRoot) {
70
- throw new Error(`${name} must not be the project root.`);
42
+ throw new Error('specDirectory must not be the project root.');
71
43
  }
72
44
 
73
45
  if (
@@ -76,7 +48,7 @@ const resolveSafeDirectory = ({
76
48
  path: requestedDirectory,
77
49
  })
78
50
  ) {
79
- throw new Error(`${name} must stay inside the project root.`);
51
+ throw new Error('specDirectory must stay inside the project root.');
80
52
  }
81
53
 
82
54
  return requestedDirectory;
@@ -91,10 +63,9 @@ const resolveDirectories = ({
91
63
  projectRoot,
92
64
  config,
93
65
  }: ResolveDirectoriesInput): MaestroConfig => {
94
- const specDirectory = resolveSafeDirectory({
66
+ const specDirectory = resolveSpecDirectory({
95
67
  projectRoot,
96
68
  directory: config.specDirectory,
97
- name: 'specDirectory',
98
69
  });
99
70
 
100
71
  return {
@@ -8,6 +8,7 @@ import { type Static, Type } from 'typebox';
8
8
  export const AGENTS = {
9
9
  BUILDER: 'maestro.builder',
10
10
  VERIFIER: 'maestro.verifier',
11
+ SIMPLIFIER: 'maestro.simplifier',
11
12
  } as const;
12
13
 
13
14
  export const THINKING_LEVELS = {
@@ -61,6 +62,7 @@ export const MaestroConfigInputSchema = Type.Object(
61
62
  specDirectory: Type.Optional(Type.String({ minLength: 1 })),
62
63
  builder: Type.Optional(PartialAgentConfigSchema),
63
64
  verifier: Type.Optional(PartialAgentConfigSchema),
65
+ simplifier: Type.Optional(PartialAgentConfigSchema),
64
66
  },
65
67
  { additionalProperties: false },
66
68
  );
@@ -83,6 +85,7 @@ export const ResolvedMaestroConfigSchema = Type.Object(
83
85
  specDirectory: Type.String({ minLength: 1 }),
84
86
  builder: ResolvedAgentConfigSchema,
85
87
  verifier: ResolvedAgentConfigSchema,
88
+ simplifier: ResolvedAgentConfigSchema,
86
89
  },
87
90
  { additionalProperties: false },
88
91
  );
@@ -63,7 +63,11 @@ function assertModelsAvailable({
63
63
  }: AssertModelsAvailableInput): void {
64
64
  const availableModels = context.modelRegistry.getAvailable();
65
65
 
66
- for (const model of [config.builder.model, config.verifier.model]) {
66
+ for (const model of [
67
+ config.builder.model,
68
+ config.verifier.model,
69
+ config.simplifier.model,
70
+ ]) {
67
71
  const [provider, id] = model.split('/');
68
72
 
69
73
  if (
@@ -77,7 +81,7 @@ function assertModelsAvailable({
77
81
  }
78
82
 
79
83
  async function assertAgentsAvailable(projectRoot: string): Promise<void> {
80
- for (const agent of [AGENTS.BUILDER, AGENTS.VERIFIER]) {
84
+ for (const agent of [AGENTS.BUILDER, AGENTS.VERIFIER, AGENTS.SIMPLIFIER]) {
81
85
  const result = await resolveSubagentLaunchContract({
82
86
  agent,
83
87
  cwd: projectRoot,
@@ -171,7 +175,7 @@ export async function assertEnvironment({
171
175
  await assertAgentsAvailable(projectRoot);
172
176
  } catch (error) {
173
177
  throw new Error(
174
- `Builder or verifier agent check failed: ${getErrorMessage(error)}`,
178
+ `Builder, verifier, or simplifier agent check failed: ${getErrorMessage(error)}`,
175
179
  );
176
180
  }
177
181
 
@@ -15,8 +15,8 @@ const MAESTRO_INSTRUCTIONS = `## Maestro mode
15
15
 
16
16
  You are Maestro. Coordinate one sequential spec-driven workflow in the current session.
17
17
  The owner decides requirements, scope, technical decisions in the spec, spec approval, escalation answers, and finding decisions.
18
- The builder implements the approved spec. The verifier independently checks the candidate. Do not perform their work yourself.
19
- The approved spec.md is the contract for all three roles. Do not infer owner approval from an agent's recommendation.
18
+ The builder implements the approved spec. The verifier independently checks the candidate. The simplifier refactors accepted code. Do not perform their work yourself.
19
+ The approved spec.md is the contract for all roles. Do not infer owner approval from an agent's recommendation.
20
20
 
21
21
  ### Maestro documentation
22
22
 
@@ -37,17 +37,17 @@ The spec, prototype, and experiment permissions below are the only exceptions fo
37
37
 
38
38
  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.
39
39
  Use tool schemas for arguments and tool results for outcomes. Do not infer a completed transition from an agent's final message.
40
- Builder and verifier runs are foreground operations with fresh contexts. Start them only through maestro_run_builder and maestro_run_verifier.
40
+ Builder, verifier, and simplifier runs are foreground operations with fresh contexts. Start them only through maestro_run_builder, maestro_run_verifier, and maestro_run_simplifier.
41
41
  Wait for each tool result before taking the next workflow action. Do not launch parallel or background runs.
42
42
 
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.
@@ -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.
@@ -85,7 +93,7 @@ Edit the active spec.md and its prototypes/ directory only in drafting-spec.
85
93
  For either revision origin, record the complete owner decision batch through the corresponding resolution tool before editing in drafting-spec.
86
94
  The numbering-only recovery procedure above also permits spec.md corrections after failed numbering validation in drafting-spec.
87
95
  Do not edit the spec or prototypes in other phases.
88
- The approved spec is frozen. Never change it during builder or verifier execution.
96
+ The approved spec is frozen. Never change it during builder, verifier, or simplifier execution.
89
97
  Use any available tool for these permitted edits. Do not change other workflow artifacts.
90
98
 
91
99
  Only in those same circumstances, run tests and checks to answer specification questions.
@@ -120,7 +128,14 @@ The verifier restores only its temporary changes and removes only its own tempor
120
128
  Each completed pass creates the next numbered file: handoffs/builder/B1.json, B2.json or handoffs/verifier/V1.json, V2.json.
121
129
  Earlier handoffs remain on the file system. The highest numeric sequence identifies the active handoff for each role.
122
130
  Use earlier handoffs only as references. Validate the active artifact's spec identity, schema, and outcome against the phase.
123
- Read the returned verifier handoff. If there are findings, follow finding-decisions. If there are none, follow candidate-ready.
131
+ Read the returned verifier handoff. If there are findings, follow finding-decisions. If there are none, follow simplifier-running.
132
+
133
+ In simplifier-running, call maestro_run_simplifier next with the active specId, after either successful verification or rejection of all findings.
134
+ Do not ask for another approval or run the verifier again. Both routes enter this phase regardless of Git availability.
135
+ Only maestro_run_simplifier checks Git readiness immediately before delegation. Do not check Git in activation or either entry route.
136
+ If Git is unavailable, the tool warns and skips simplification, saves candidate-ready, and creates no simplifier handoff. Report simplification as skipped.
137
+ Otherwise the simplifier records handoffs/simplifier/S.json before candidate-ready. This fixed result is unnumbered.
138
+ Simplifier execution or protocol errors stop the workflow under the same error policy as other runs.
124
139
 
125
140
  ### Explain findings and escalations
126
141
 
@@ -160,7 +175,7 @@ Submit the complete batch through maestro_resolve_findings before editing the sp
160
175
  The tool saves every decision in the active verifier handoff before saving the next phase. Earlier numbered handoffs remain unchanged.
161
176
  If any decision is revise-spec, the tool returns drafting-spec without approving the spec or running an agent.
162
177
  Revision takes priority over fix-code and reject in mixed batches, while all decisions are saved.
163
- Without revision, all rejections return candidate-ready and any fix-code returns ready-for-builder.
178
+ Without revision, all rejections return simplifier-running and any fix-code returns ready-for-builder.
164
179
  For ready-for-builder, call maestro_run_builder separately. Do not fix the code yourself.
165
180
 
166
181
  For either an escalation revision or a finding revision, first record the complete batch and enter drafting-spec as described above.
@@ -185,6 +200,9 @@ The owner handles execution errors, protocol errors, and interrupted workflows m
185
200
 
186
201
  At candidate-ready, the workflow is complete. No final tool call is required.
187
202
  You own the final summary. Read the artifacts to summarize changes, verification results, rejected findings with reasons, and applicable builder notes.
203
+ For completed simplification, read handoffs/simplifier/S.json and report its saved summary and notes as the simplifier's report.
204
+ For example, summary "Simplified counting" and note "Updated the caller to read the Map." belong to the simplifier, not verifier evidence.
205
+ Do not claim the verifier independently checked later refactoring. Keep verification evidence and the simplifier report separate.
188
206
  Do not describe rejected findings as passed verification.
189
207
  Do not change files, run verification again, or modify the concluded workflow to prepare this summary.
190
208
  The owner controls later review and changes. Later changes are outside this verification.`;
@@ -18,6 +18,7 @@ export const WORKFLOW_PHASE_LABELS = {
18
18
  [WORKFLOW_PHASES.READY_FOR_VERIFIER]: 'Ready for verifier',
19
19
  [WORKFLOW_PHASES.VERIFIER_RUNNING]: 'Verifier running',
20
20
  [WORKFLOW_PHASES.FINDING_DECISIONS]: 'Owner decision needed: findings',
21
+ [WORKFLOW_PHASES.SIMPLIFIER_RUNNING]: 'Simplifier running',
21
22
  [WORKFLOW_PHASES.CANDIDATE_READY]: 'Completed',
22
23
  } as const;
23
24
 
@@ -29,6 +30,7 @@ export const WORKFLOW_PHASE_ICONS = {
29
30
  [WORKFLOW_PHASES.READY_FOR_VERIFIER]: '๐Ÿ“‹',
30
31
  [WORKFLOW_PHASES.VERIFIER_RUNNING]: '๐Ÿ”',
31
32
  [WORKFLOW_PHASES.FINDING_DECISIONS]: '๐Ÿ’ฌ',
33
+ [WORKFLOW_PHASES.SIMPLIFIER_RUNNING]: '๐Ÿงน',
32
34
  [WORKFLOW_PHASES.CANDIDATE_READY]: 'โœ…',
33
35
  } as const;
34
36
 
@@ -40,6 +42,7 @@ export const WORKFLOW_PHASE_COLORS = {
40
42
  [WORKFLOW_PHASES.READY_FOR_VERIFIER]: 'accent',
41
43
  [WORKFLOW_PHASES.VERIFIER_RUNNING]: 'accent',
42
44
  [WORKFLOW_PHASES.FINDING_DECISIONS]: 'warning',
45
+ [WORKFLOW_PHASES.SIMPLIFIER_RUNNING]: 'accent',
43
46
  [WORKFLOW_PHASES.CANDIDATE_READY]: 'success',
44
47
  } as const satisfies Record<WorkflowPhase, ThemeColor>;
45
48