@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
package/extensions/maestro.ts
CHANGED
|
@@ -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
package/src/MaestroPaths.ts
CHANGED
|
@@ -100,36 +100,15 @@ export class MaestroPaths {
|
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
public getSpecFilePath(specId: string): string {
|
|
103
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
211
|
-
|
|
189
|
+
public getSimplifierHandoffPath(specId: string): string {
|
|
190
|
+
return resolve(this.getSpecPath(specId), 'handoffs/simplifier/S.json');
|
|
191
|
+
}
|
|
212
192
|
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
99
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
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
|
-
|
|
67
|
-
|
|
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
|
-
|
|
76
|
-
|
|
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
|
-
!
|
|
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
|
|
package/src/config/defaults.ts
CHANGED
|
@@ -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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
|
27
|
+
const resolveSpecDirectory = ({
|
|
61
28
|
projectRoot,
|
|
62
29
|
directory,
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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(
|
|
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(
|
|
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 =
|
|
66
|
+
const specDirectory = resolveSpecDirectory({
|
|
95
67
|
projectRoot,
|
|
96
68
|
directory: config.specDirectory,
|
|
97
|
-
name: 'specDirectory',
|
|
98
69
|
});
|
|
99
70
|
|
|
100
71
|
return {
|
package/src/config/schema.ts
CHANGED
|
@@ -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 [
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|