@emiliosp/pi-maestro 0.10.1 → 0.12.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 (75) hide show
  1. package/ADR.md +94 -18
  2. package/CHANGELOG.md +87 -33
  3. package/MISSION.md +4 -4
  4. package/README.md +66 -22
  5. package/ROADMAP.md +141 -112
  6. package/TECH_STACK.md +12 -11
  7. package/agents/builder.md +53 -57
  8. package/agents/simplifier.md +232 -0
  9. package/agents/verifier.md +52 -79
  10. package/docs/configuration.md +27 -16
  11. package/docs/glossary.md +47 -39
  12. package/docs/subagent-integration.md +22 -13
  13. package/docs/workflow.md +207 -103
  14. package/extensions/maestro-subagent.ts +4 -1
  15. package/extensions/maestro.ts +15 -26
  16. package/package.json +38 -39
  17. package/src/MaestroPaths.ts +26 -35
  18. package/src/artifacts/builder-handoff/assertBuilderHandoff.ts +6 -15
  19. package/src/artifacts/builder-handoff/schema.ts +3 -6
  20. package/src/artifacts/simplifier-handoff/assertSimplifierHandoff.ts +56 -0
  21. package/src/artifacts/simplifier-handoff/readSimplifierHandoff.ts +29 -0
  22. package/src/artifacts/simplifier-handoff/schema.ts +27 -0
  23. package/src/artifacts/verifier-handoff/assertVerifierHandoff.ts +4 -10
  24. package/src/artifacts/verifier-handoff/schema.ts +3 -6
  25. package/src/artifacts/verifier-handoff/writeVerifierHandoff.ts +1 -3
  26. package/src/config/assertConfiguration.ts +4 -7
  27. package/src/config/defaults.ts +8 -5
  28. package/src/config/loadConfiguration.ts +5 -11
  29. package/src/config/schema.ts +4 -2
  30. package/src/ids/createSpecId.ts +1 -2
  31. package/src/ids/isValidSpecId.ts +1 -2
  32. package/src/maestro/checks/assertEnvironment.ts +21 -48
  33. package/src/maestro/instructions/getMaestroInstructions.ts +59 -30
  34. package/src/maestro/status/formatMaestroStatus.ts +7 -13
  35. package/src/maestro/status/refreshMaestroStatus.ts +3 -7
  36. package/src/specs/create.ts +14 -4
  37. package/src/specs/template.ts +10 -14
  38. package/src/tools/child/record-builder-handoff.ts +1 -0
  39. package/src/tools/child/record-simplifier-handoff.ts +73 -0
  40. package/src/tools/child/record-verifier-handoff.ts +2 -3
  41. package/src/tools/child/utils/resolveWorkflowContext.ts +1 -4
  42. package/src/tools/main/create-spec.ts +3 -2
  43. package/src/tools/main/mark-spec-ready.ts +2 -1
  44. package/src/tools/main/resolve-escalations.ts +1 -0
  45. package/src/tools/main/resolve-findings.ts +7 -21
  46. package/src/tools/main/run-builder.ts +37 -57
  47. package/src/tools/main/run-simplifier.ts +172 -0
  48. package/src/tools/main/run-verifier.ts +34 -58
  49. package/src/tools/utils/pi-subagent-delegation.ts +13 -22
  50. package/src/tools/utils/resolveToolRunContext.ts +2 -3
  51. package/src/utils/is-errno-exception.ts +1 -3
  52. package/src/utils/path-exists.ts +1 -0
  53. package/src/utils/path-strictly-within.ts +3 -6
  54. package/src/utils/path-within-or-equal.ts +1 -4
  55. package/src/utils/read-json.ts +1 -4
  56. package/src/utils/write-json.ts +1 -4
  57. package/src/workflow/builder/completeBuilderPass.ts +3 -2
  58. package/src/workflow/builder/prepareBuilderRun.ts +3 -8
  59. package/src/workflow/builder/resolveEscalations.ts +2 -6
  60. package/src/workflow/builder/utils/getWorkflowEventByBuilderHandoff.ts +1 -3
  61. package/src/workflow/roles.ts +2 -1
  62. package/src/workflow/simplifier/completeSimplifierPass.ts +56 -0
  63. package/src/workflow/spec/markSpecReady.ts +10 -4
  64. package/src/workflow/state/readWorkflowState.ts +2 -7
  65. package/src/workflow/state/schema.ts +9 -10
  66. package/src/workflow/state/writeWorkflowState.ts +1 -4
  67. package/src/workflow/transitions.ts +12 -12
  68. package/src/workflow/utils/assertSpecAcceptanceCriteria.ts +2 -1
  69. package/src/workflow/utils/getAgentRole.ts +6 -4
  70. package/src/workflow/verifier/completeVerifierPass.ts +1 -0
  71. package/src/workflow/verifier/resolveFindings.ts +9 -29
  72. package/src/workflow/verifier/utils/getWorkflowEventByVerifierHandoff.ts +5 -20
  73. package/templates/design.md +20 -0
  74. package/templates/proposal.md +23 -0
  75. package/templates/spec.md +21 -50
package/package.json CHANGED
@@ -1,30 +1,23 @@
1
1
  {
2
2
  "name": "@emiliosp/pi-maestro",
3
- "version": "0.10.1",
3
+ "version": "0.12.0",
4
4
  "description": "A spec-driven multiagent development workflow for Pi.",
5
- "license": "MIT",
6
- "type": "module",
7
- "repository": {
8
- "type": "git",
9
- "url": "git+https://github.com/emiliosp/pi-maestro.git"
10
- },
11
- "homepage": "https://github.com/emiliosp/pi-maestro#readme",
12
- "bugs": {
13
- "url": "https://github.com/emiliosp/pi-maestro/issues"
14
- },
15
5
  "keywords": [
16
- "pi-package",
6
+ "orchestration",
17
7
  "pi",
18
8
  "pi-coding-agent",
19
- "workflow",
9
+ "pi-package",
20
10
  "subagents",
21
- "orchestration"
22
- ],
23
- "os": [
24
- "darwin"
11
+ "workflow"
25
12
  ],
26
- "engines": {
27
- "node": ">=26.0.0"
13
+ "homepage": "https://github.com/emiliosp/pi-maestro#readme",
14
+ "bugs": {
15
+ "url": "https://github.com/emiliosp/pi-maestro/issues"
16
+ },
17
+ "license": "MIT",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/emiliosp/pi-maestro.git"
28
21
  },
29
22
  "files": [
30
23
  "extensions/",
@@ -41,6 +34,10 @@
41
34
  "README.md",
42
35
  "LICENSE"
43
36
  ],
37
+ "os": [
38
+ "darwin"
39
+ ],
40
+ "type": "module",
44
41
  "imports": {
45
42
  "#config/*": "./src/config/*",
46
43
  "#specs/*": "./src/specs/*",
@@ -53,41 +50,46 @@
53
50
  "#ids/*": "./src/ids/*",
54
51
  "#*": "./*"
55
52
  },
53
+ "publishConfig": {
54
+ "access": "public"
55
+ },
56
56
  "scripts": {
57
57
  "typecheck": "tsc --noEmit",
58
58
  "test": "vitest run --coverage --config vitest.config.ts",
59
- "check": "npm run lint:fix && npm run typecheck && npm test",
59
+ "check": "npm run fmt && npm run lint && npm run typecheck && npm test",
60
60
  "prepublishOnly": "npm run check",
61
61
  "publish:npm": "npm publish --access public",
62
- "lint": "biome check .",
63
- "lint:anti-slop": "oxlint --fix",
64
- "lint:fix": "biome check --write . && npm run lint:anti-slop"
62
+ "fmt": "oxfmt",
63
+ "lint": "oxlint --fix"
65
64
  },
66
65
  "dependencies": {
67
66
  "pi-subagents": "0.78.0"
68
67
  },
69
- "bundleDependencies": [
70
- "pi-subagents"
71
- ],
72
- "peerDependencies": {
73
- "@earendil-works/pi-ai": "*",
74
- "@earendil-works/pi-coding-agent": "*",
75
- "typebox": "*"
76
- },
77
68
  "devDependencies": {
78
- "@biomejs/biome": "2.5.15",
79
69
  "@earendil-works/pi-agent-core": "1.0.3",
80
70
  "@earendil-works/pi-tui": "1.0.3",
81
- "@vitest/coverage-v8": "5.0.3",
82
71
  "@types/node": "26.6.4",
72
+ "@vitest/coverage-v8": "5.0.3",
73
+ "oxfmt": "0.73.0",
83
74
  "oxlint": "1.87.0",
84
- "oxlint-anti-slop": "0.3.6",
75
+ "oxlint-anti-slop": "0.3.8",
85
76
  "typebox": "1.3.35",
86
77
  "typescript": "7.0.2",
87
78
  "vitest": "5.0.3"
88
79
  },
89
- "publishConfig": {
90
- "access": "public"
80
+ "peerDependencies": {
81
+ "@earendil-works/pi-ai": "*",
82
+ "@earendil-works/pi-coding-agent": "*",
83
+ "typebox": "*"
84
+ },
85
+ "bundleDependencies": [
86
+ "pi-subagents"
87
+ ],
88
+ "engines": {
89
+ "node": ">=26.0.0"
90
+ },
91
+ "allowScripts": {
92
+ "fsevents@2.3.3": true
91
93
  },
92
94
  "pi": {
93
95
  "extensions": [
@@ -98,8 +100,5 @@
98
100
  "./agents"
99
101
  ]
100
102
  }
101
- },
102
- "allowScripts": {
103
- "fsevents@2.3.3": true
104
103
  }
105
104
  }
@@ -5,12 +5,15 @@
5
5
 
6
6
  import { readdir } from 'node:fs/promises';
7
7
  import { isAbsolute, relative, resolve } from 'node:path';
8
+
8
9
  import type { MaestroConfig } from '#config/schema.ts';
9
10
  import { isValidSpecId } from '#ids/isValidSpecId.ts';
10
11
  import { isPathStrictlyWithin } from '#utils/path-strictly-within.ts';
11
12
 
12
13
  const PATHS = {
14
+ PROPOSAL_FILE: 'proposal.md',
13
15
  SPEC_FILE: 'spec.md',
16
+ DESIGN_FILE: 'design.md',
14
17
  WORKFLOW_FILE: 'workflow.json',
15
18
  HANDOFFS_PATH: 'handoffs',
16
19
  BUILDER_HANDOFFS_PATH: 'handoffs/builder',
@@ -25,9 +28,7 @@ const AGENTS_PREFIX = {
25
28
 
26
29
  function assertSpecId(specId: string): void {
27
30
  if (!isValidSpecId(specId)) {
28
- throw new Error(
29
- `Invalid spec ID: "${specId}". Expected YYYYMMDD-HHmmss-<slug>.`,
30
- );
31
+ throw new Error(`Invalid spec ID: "${specId}". Expected YYYYMMDD-HHmmss-<slug>.`);
31
32
  }
32
33
  }
33
34
 
@@ -43,9 +44,7 @@ function assertSafeRelativePrototypePath(value: string): void {
43
44
  }
44
45
 
45
46
  if (isAbsolute(value)) {
46
- throw new Error(
47
- 'Prototype path must be relative to the prototypes directory.',
48
- );
47
+ throw new Error('Prototype path must be relative to the prototypes directory.');
49
48
  }
50
49
  }
51
50
 
@@ -99,10 +98,18 @@ export class MaestroPaths {
99
98
  return resolve(this.projectRoot, this.specDirectoryFromRoot, specId);
100
99
  }
101
100
 
101
+ public getProposalFilePath(specId: string): string {
102
+ return resolve(this.getSpecPath(specId), PATHS.PROPOSAL_FILE);
103
+ }
104
+
102
105
  public getSpecFilePath(specId: string): string {
103
106
  return resolve(this.getSpecPath(specId), PATHS.SPEC_FILE);
104
107
  }
105
108
 
109
+ public getDesignFilePath(specId: string): string {
110
+ return resolve(this.getSpecPath(specId), PATHS.DESIGN_FILE);
111
+ }
112
+
106
113
  public getWorkflowPath(specId: string): string {
107
114
  return resolve(this.getSpecPath(specId), PATHS.WORKFLOW_FILE);
108
115
  }
@@ -119,10 +126,7 @@ export class MaestroPaths {
119
126
  return resolve(this.getSpecPath(specId), PATHS.VERIFIER_HANDOFFS_PATH);
120
127
  }
121
128
 
122
- public getBuilderHandoffPath({
123
- specId,
124
- handoffPassNumber,
125
- }: HandoffPathInput): string {
129
+ public getBuilderHandoffPath({ specId, handoffPassNumber }: HandoffPathInput): string {
126
130
  assertArtifactNumber(handoffPassNumber);
127
131
 
128
132
  return resolve(
@@ -131,10 +135,7 @@ export class MaestroPaths {
131
135
  );
132
136
  }
133
137
 
134
- public getVerifierHandoffPath({
135
- specId,
136
- handoffPassNumber,
137
- }: HandoffPathInput): string {
138
+ public getVerifierHandoffPath({ specId, handoffPassNumber }: HandoffPathInput): string {
138
139
  assertArtifactNumber(handoffPassNumber);
139
140
 
140
141
  return resolve(
@@ -144,9 +145,7 @@ export class MaestroPaths {
144
145
  }
145
146
 
146
147
  public async getActiveBuilderHandoffPath(specId: string): Promise<string> {
147
- const handoffPassNumber = (
148
- await readdir(this.getBuilderHandoffsPath(specId))
149
- ).length;
148
+ const handoffPassNumber = (await readdir(this.getBuilderHandoffsPath(specId))).length;
150
149
 
151
150
  if (handoffPassNumber === 0) throw new Error('Builder handoff is missing.');
152
151
 
@@ -154,20 +153,15 @@ export class MaestroPaths {
154
153
  }
155
154
 
156
155
  public async getActiveVerifierHandoffPath(specId: string): Promise<string> {
157
- const handoffPassNumber = (
158
- await readdir(this.getVerifierHandoffsPath(specId))
159
- ).length;
156
+ const handoffPassNumber = (await readdir(this.getVerifierHandoffsPath(specId))).length;
160
157
 
161
- if (handoffPassNumber === 0)
162
- throw new Error('Verifier handoff is missing.');
158
+ if (handoffPassNumber === 0) throw new Error('Verifier handoff is missing.');
163
159
 
164
160
  return this.getVerifierHandoffPath({ specId, handoffPassNumber });
165
161
  }
166
162
 
167
163
  public async getNextBuilderHandoffPath(specId: string): Promise<string> {
168
- const handoffPassNumber = (
169
- await readdir(this.getBuilderHandoffsPath(specId))
170
- ).length;
164
+ const handoffPassNumber = (await readdir(this.getBuilderHandoffsPath(specId))).length;
171
165
 
172
166
  return this.getBuilderHandoffPath({
173
167
  specId,
@@ -176,9 +170,7 @@ export class MaestroPaths {
176
170
  }
177
171
 
178
172
  public async getNextVerifierHandoffPath(specId: string): Promise<string> {
179
- const handoffPassNumber = (
180
- await readdir(this.getVerifierHandoffsPath(specId))
181
- ).length;
173
+ const handoffPassNumber = (await readdir(this.getVerifierHandoffsPath(specId))).length;
182
174
 
183
175
  return this.getVerifierHandoffPath({
184
176
  specId,
@@ -186,23 +178,22 @@ export class MaestroPaths {
186
178
  });
187
179
  }
188
180
 
181
+ public getSimplifierHandoffPath(specId: string): string {
182
+ return resolve(this.getSpecPath(specId), 'handoffs/simplifier/S.json');
183
+ }
184
+
189
185
  public getPrototypesPath(specId: string): string {
190
186
  return resolve(this.getSpecPath(specId), PATHS.PROTOTYPES_PATH);
191
187
  }
192
188
 
193
- public getPrototypePath({
194
- specId,
195
- relativePath,
196
- }: PrototypePathInput): string {
189
+ public getPrototypePath({ specId, relativePath }: PrototypePathInput): string {
197
190
  assertSafeRelativePrototypePath(relativePath);
198
191
 
199
192
  const prototypesPath = this.getPrototypesPath(specId);
200
193
  const target = resolve(prototypesPath, relativePath);
201
194
 
202
195
  if (!isPathStrictlyWithin({ parent: prototypesPath, path: target })) {
203
- throw new Error(
204
- 'Prototype path must stay inside the prototypes directory.',
205
- );
196
+ throw new Error('Prototype path must stay inside the prototypes directory.');
206
197
  }
207
198
 
208
199
  return target;
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  import { Value } from 'typebox/value';
7
+
7
8
  import {
8
9
  BUILDER_HANDOFF_STATUSES,
9
10
  type BuilderAcceptanceCriterion,
@@ -16,19 +17,12 @@ import { isValidSpecId } from '#ids/isValidSpecId.ts';
16
17
  const hasUniqueAcceptanceCriterionIds = (
17
18
  acceptanceCriteria: BuilderAcceptanceCriterion[],
18
19
  ): boolean =>
19
- new Set(acceptanceCriteria.map((criterion) => criterion.id)).size ===
20
- acceptanceCriteria.length;
20
+ new Set(acceptanceCriteria.map((criterion) => criterion.id)).size === acceptanceCriteria.length;
21
21
 
22
- const hasOnlyPassedProbes = (
23
- acceptanceCriteria: BuilderAcceptanceCriterion[],
24
- ): boolean =>
25
- acceptanceCriteria.every(
26
- (criterion) => criterion.probeStatus === PROBE_STATUSES.PASSED,
27
- );
22
+ const hasOnlyPassedProbes = (acceptanceCriteria: BuilderAcceptanceCriterion[]): boolean =>
23
+ acceptanceCriteria.every((criterion) => criterion.probeStatus === PROBE_STATUSES.PASSED);
28
24
 
29
- function assertBuilderHandoffSchema(
30
- input: unknown,
31
- ): asserts input is BuilderHandoff {
25
+ function assertBuilderHandoffSchema(input: unknown): asserts input is BuilderHandoff {
32
26
  if (typeof input !== 'object' || input === null || Array.isArray(input)) {
33
27
  throw new Error('Builder handoff must be a JSON object.');
34
28
  }
@@ -77,10 +71,7 @@ export function assertBuilderHandoff(
77
71
  throw new Error('Escalation option IDs must be unique.');
78
72
  }
79
73
 
80
- if (
81
- escalation.recommendation !== null &&
82
- !optionIds.has(escalation.recommendation.optionId)
83
- ) {
74
+ if (escalation.recommendation !== null && !optionIds.has(escalation.recommendation.optionId)) {
84
75
  throw new Error('Escalation recommendation references unknown option.');
85
76
  }
86
77
 
@@ -5,6 +5,7 @@
5
5
 
6
6
  import { StringEnum } from '@earendil-works/pi-ai';
7
7
  import { type Static, Type } from 'typebox';
8
+
8
9
  import { SPEC_ID_PATTERN } from '#ids/isValidSpecId.ts';
9
10
 
10
11
  export const BUILDER_HANDOFF_VERSION = '1.0.0';
@@ -36,9 +37,7 @@ export const BuilderAcceptanceCriterionSchema = Type.Object(
36
37
  { additionalProperties: false },
37
38
  );
38
39
 
39
- export type BuilderAcceptanceCriterion = Static<
40
- typeof BuilderAcceptanceCriterionSchema
41
- >;
40
+ export type BuilderAcceptanceCriterion = Static<typeof BuilderAcceptanceCriterionSchema>;
42
41
 
43
42
  const BuilderHandoffContentFields = {
44
43
  summary: Type.String({ minLength: 1 }),
@@ -158,6 +157,4 @@ export const BuilderHandoffSubmissionSchema = Type.Union([
158
157
  BuilderEscalationHandoffSubmissionSchema,
159
158
  ]);
160
159
 
161
- export type BuilderHandoffSubmissionInput = Static<
162
- typeof BuilderHandoffSubmissionSchema
163
- >;
160
+ export type BuilderHandoffSubmissionInput = Static<typeof BuilderHandoffSubmissionSchema>;
@@ -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
+
8
+ import { Value } from 'typebox/value';
9
+
10
+ import {
11
+ type SimplifierHandoff,
12
+ SimplifierHandoffSchema,
13
+ } from '#artifacts/simplifier-handoff/schema.ts';
14
+ import { isPathStrictlyWithin } from '#utils/path-strictly-within.ts';
15
+
16
+ type AssertSimplifierHandoffInput<Handoff = unknown> = {
17
+ handoff: Handoff;
18
+ specId: string;
19
+ projectRoot: string;
20
+ };
21
+
22
+ function assertSimplifierHandoffSchema(input: unknown): 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,27 @@
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
+
8
+ import { SPEC_ID_PATTERN } from '#ids/isValidSpecId.ts';
9
+
10
+ export const SIMPLIFIER_HANDOFF_VERSION = '1.0.0';
11
+
12
+ export const SimplifierSubmissionFields = {
13
+ specId: Type.String({ pattern: SPEC_ID_PATTERN.source }),
14
+ refactoredFiles: Type.Array(Type.String({ minLength: 1 })),
15
+ summary: Type.String({ minLength: 1 }),
16
+ notes: Type.Array(Type.String()),
17
+ };
18
+
19
+ export const SimplifierHandoffSchema = Type.Object(
20
+ {
21
+ version: Type.Literal(SIMPLIFIER_HANDOFF_VERSION),
22
+ ...SimplifierSubmissionFields,
23
+ },
24
+ { additionalProperties: false },
25
+ );
26
+
27
+ export type SimplifierHandoff = Static<typeof SimplifierHandoffSchema>;
@@ -4,16 +4,12 @@
4
4
  */
5
5
 
6
6
  import { Value } from 'typebox/value';
7
+
7
8
  import { PROBE_STATUSES } from '#artifacts/builder-handoff/schema.ts';
8
- import {
9
- type VerifierHandoff,
10
- VerifierHandoffSchema,
11
- } from '#artifacts/verifier-handoff/schema.ts';
9
+ import { type VerifierHandoff, VerifierHandoffSchema } from '#artifacts/verifier-handoff/schema.ts';
12
10
  import { isValidSpecId } from '#ids/isValidSpecId.ts';
13
11
 
14
- function assertVerifierHandoffSchema(
15
- input: unknown,
16
- ): asserts input is VerifierHandoff {
12
+ function assertVerifierHandoffSchema(input: unknown): asserts input is VerifierHandoff {
17
13
  if (typeof input !== 'object' || input === null || Array.isArray(input)) {
18
14
  throw new Error('Verifier handoff must be a JSON object.');
19
15
  }
@@ -45,9 +41,7 @@ export function assertVerifierHandoff(
45
41
  );
46
42
 
47
43
  if (acceptanceCriterionIds.size !== handoff.acceptanceCriteria.length) {
48
- throw new Error(
49
- 'Verifier handoff acceptance criterion IDs must be unique.',
50
- );
44
+ throw new Error('Verifier handoff acceptance criterion IDs must be unique.');
51
45
  }
52
46
 
53
47
  const criterionIdsWithFindings = new Set<string>();
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  import { type Static, Type } from 'typebox';
7
+
7
8
  import { BuilderAcceptanceCriterionSchema } from '#artifacts/builder-handoff/schema.ts';
8
9
  import { SPEC_ID_PATTERN } from '#ids/isValidSpecId.ts';
9
10
 
@@ -15,8 +16,7 @@ export const FINDING_SEVERITIES = {
15
16
  LOW: 'low',
16
17
  } as const;
17
18
 
18
- export type FindingSeverity =
19
- (typeof FINDING_SEVERITIES)[keyof typeof FINDING_SEVERITIES];
19
+ export type FindingSeverity = (typeof FINDING_SEVERITIES)[keyof typeof FINDING_SEVERITIES];
20
20
 
21
21
  const FindingSeveritySchema = Type.Union([
22
22
  Type.Literal(FINDING_SEVERITIES.HIGH),
@@ -64,10 +64,7 @@ const FindingDecisionSchema = Type.Union([
64
64
  export const VerifierFindingSchema = Type.Object(
65
65
  {
66
66
  id: Type.String({ pattern: '^F[1-9]\\d*$' }),
67
- acceptanceCriterion: Type.Union([
68
- Type.String({ minLength: 1 }),
69
- Type.Null(),
70
- ]),
67
+ acceptanceCriterion: Type.Union([Type.String({ minLength: 1 }), Type.Null()]),
71
68
  severity: FindingSeveritySchema,
72
69
  confidence: Type.Number({ minimum: 0, maximum: 1 }),
73
70
  summary: Type.String({ minLength: 1 }),
@@ -22,9 +22,7 @@ export const writeVerifierHandoff = async ({
22
22
  const { handoff } = input;
23
23
 
24
24
  if (handoff.findings.some((finding) => finding.decision !== null)) {
25
- throw new Error(
26
- 'New verifier handoff findings must have no owner decision.',
27
- );
25
+ throw new Error('New verifier handoff findings must have no owner decision.');
28
26
  }
29
27
 
30
28
  await writeJson({ path, data: handoff });
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  import { Value } from 'typebox/value';
7
+
7
8
  import {
8
9
  MAX_TIMEOUT_MINUTES,
9
10
  MaestroConfigInputSchema,
@@ -41,7 +42,7 @@ const formatError = (error: SchemaValidationError): string => {
41
42
  return `Invalid model identifier at "${path}". Must use "provider/model" format.`;
42
43
  }
43
44
 
44
- if (path === 'builder' || path === 'verifier') {
45
+ if (path === 'builder' || path === 'verifier' || path === 'simplifier') {
45
46
  return `Invalid ${path} configuration: expected an object.`;
46
47
  }
47
48
 
@@ -52,9 +53,7 @@ const formatError = (error: SchemaValidationError): string => {
52
53
  return `Invalid configuration at "${path}": ${error.message}.`;
53
54
  };
54
55
 
55
- export function assertConfiguration(
56
- input: unknown,
57
- ): asserts input is PartialMaestroConfig {
56
+ export function assertConfiguration(input: unknown): asserts input is PartialMaestroConfig {
58
57
  if (typeof input !== 'object' || input === null || Array.isArray(input)) {
59
58
  throw new Error('Configuration must be a JSON object.');
60
59
  }
@@ -62,9 +61,7 @@ export function assertConfiguration(
62
61
  const raw = input;
63
62
 
64
63
  if (!('version' in raw) || raw.version === undefined) {
65
- throw new Error(
66
- "Missing configuration version. The 'version' field is required.",
67
- );
64
+ throw new Error("Missing configuration version. The 'version' field is required.");
68
65
  }
69
66
 
70
67
  if (typeof raw.version !== 'string') {
@@ -3,11 +3,7 @@
3
3
  * Used: When configuration is resolved from missing or partial input.
4
4
  */
5
5
 
6
- import {
7
- type MaestroConfig,
8
- SUPPORTED_CONFIG_VERSION,
9
- THINKING_LEVELS,
10
- } from '#config/schema.ts';
6
+ import { type MaestroConfig, SUPPORTED_CONFIG_VERSION, THINKING_LEVELS } from '#config/schema.ts';
11
7
 
12
8
  export const DEFAULT_CONFIG_VERSION = SUPPORTED_CONFIG_VERSION;
13
9
 
@@ -21,6 +17,8 @@ export const DEFAULT_VERIFIER_MODEL = 'openai-codex/gpt-6.1-sol';
21
17
 
22
18
  export const DEFAULT_VERIFIER_THINKING = THINKING_LEVELS.HIGH;
23
19
 
20
+ export const DEFAULT_SIMPLIFIER_MODEL = 'openai-codex/gpt-6.1-sol';
21
+
24
22
  export const DEFAULT_TIMEOUT_MINUTES = 60;
25
23
 
26
24
  export const CONFIG_DIRECTORY_NAME = '.pi';
@@ -37,6 +35,11 @@ export const DEFAULT_CONFIG: MaestroConfig = {
37
35
  thinking: DEFAULT_BUILDER_THINKING,
38
36
  timeoutMinutes: DEFAULT_TIMEOUT_MINUTES,
39
37
  },
38
+ simplifier: {
39
+ model: DEFAULT_SIMPLIFIER_MODEL,
40
+ thinking: THINKING_LEVELS.HIGH,
41
+ timeoutMinutes: DEFAULT_TIMEOUT_MINUTES,
42
+ },
40
43
  verifier: {
41
44
  model: DEFAULT_VERIFIER_MODEL,
42
45
  thinking: DEFAULT_VERIFIER_THINKING,
@@ -5,6 +5,7 @@
5
5
 
6
6
  import { readFile, realpath } from 'node:fs/promises';
7
7
  import { isAbsolute, join, resolve } from 'node:path';
8
+
8
9
  import { assertConfiguration } from '#config/assertConfiguration.ts';
9
10
  import { CONFIG_FILE_PATH, DEFAULT_CONFIG } from '#config/defaults.ts';
10
11
  import type { MaestroConfig, PartialMaestroConfig } from '#config/schema.ts';
@@ -16,6 +17,7 @@ const resolveConfiguration = (input: PartialMaestroConfig): MaestroConfig => ({
16
17
  specDirectory: input.specDirectory ?? DEFAULT_CONFIG.specDirectory,
17
18
  builder: { ...DEFAULT_CONFIG.builder, ...input.builder },
18
19
  verifier: { ...DEFAULT_CONFIG.verifier, ...input.verifier },
20
+ simplifier: { ...DEFAULT_CONFIG.simplifier, ...input.simplifier },
19
21
  });
20
22
 
21
23
  type ResolveSpecDirectoryInput = {
@@ -23,10 +25,7 @@ type ResolveSpecDirectoryInput = {
23
25
  directory: string;
24
26
  };
25
27
 
26
- const resolveSpecDirectory = ({
27
- projectRoot,
28
- directory,
29
- }: ResolveSpecDirectoryInput): string => {
28
+ const resolveSpecDirectory = ({ projectRoot, directory }: ResolveSpecDirectoryInput): string => {
30
29
  if (directory.includes('\0')) {
31
30
  throw new Error('specDirectory must not contain a null byte.');
32
31
  }
@@ -58,10 +57,7 @@ type ResolveDirectoriesInput = {
58
57
  config: MaestroConfig;
59
58
  };
60
59
 
61
- const resolveDirectories = ({
62
- projectRoot,
63
- config,
64
- }: ResolveDirectoriesInput): MaestroConfig => {
60
+ const resolveDirectories = ({ projectRoot, config }: ResolveDirectoriesInput): MaestroConfig => {
65
61
  const specDirectory = resolveSpecDirectory({
66
62
  projectRoot,
67
63
  directory: config.specDirectory,
@@ -84,9 +80,7 @@ const readConfigurationFile = async (path: string): Promise<unknown> => {
84
80
  }
85
81
  };
86
82
 
87
- export const loadConfiguration = async (
88
- cwd: string = process.cwd(),
89
- ): Promise<MaestroConfig> => {
83
+ export const loadConfiguration = async (cwd: string = process.cwd()): Promise<MaestroConfig> => {
90
84
  const projectRoot = await realpath(cwd);
91
85
  const targetPath = join(projectRoot, CONFIG_FILE_PATH);
92
86