@emiliosp/pi-maestro 0.11.0 → 0.13.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 +82 -23
- package/CHANGELOG.md +48 -28
- package/MISSION.md +2 -4
- package/README.md +55 -25
- package/TECH_STACK.md +3 -3
- package/agents/builder.md +53 -57
- package/agents/simplifier.md +79 -61
- package/agents/verifier.md +52 -64
- package/docs/configuration.md +1 -1
- package/docs/glossary.md +43 -40
- package/docs/subagent-integration.md +7 -8
- package/docs/workflow.md +137 -63
- package/extensions/maestro-subagent.ts +1 -0
- package/extensions/maestro.ts +60 -30
- package/package.json +38 -40
- package/src/MaestroPaths.ts +22 -35
- package/src/artifacts/builder-handoff/assertBuilderHandoff.ts +6 -15
- package/src/artifacts/builder-handoff/schema.ts +3 -6
- package/src/artifacts/simplifier-handoff/assertSimplifierHandoff.ts +3 -3
- package/src/artifacts/simplifier-handoff/schema.ts +1 -0
- package/src/artifacts/verifier-handoff/assertVerifierHandoff.ts +4 -10
- package/src/artifacts/verifier-handoff/schema.ts +3 -6
- package/src/artifacts/verifier-handoff/writeVerifierHandoff.ts +1 -3
- package/src/config/assertConfiguration.ts +3 -6
- package/src/config/defaults.ts +1 -5
- package/src/config/loadConfiguration.ts +4 -11
- package/src/config/schema.ts +1 -2
- package/src/ids/createSpecId.ts +1 -2
- package/src/ids/isValidSpecId.ts +1 -2
- package/src/maestro/checks/assertEnvironment.ts +19 -50
- package/src/maestro/instructions/getMaestroInstructions.ts +47 -26
- package/src/maestro/session/MaestroSessionState.ts +15 -0
- package/src/maestro/status/formatMaestroStatus.ts +4 -13
- package/src/maestro/status/refreshMaestroStatus.ts +3 -7
- package/src/specs/create.ts +14 -4
- package/src/specs/template.ts +10 -14
- package/src/tools/child/record-builder-handoff.ts +1 -0
- package/src/tools/child/record-simplifier-handoff.ts +4 -4
- package/src/tools/child/record-verifier-handoff.ts +2 -3
- package/src/tools/child/utils/resolveWorkflowContext.ts +1 -4
- package/src/tools/main/create-spec.ts +3 -2
- package/src/tools/main/mark-spec-ready.ts +2 -1
- package/src/tools/main/resolve-escalations.ts +1 -0
- package/src/tools/main/resolve-findings.ts +6 -20
- package/src/tools/main/run-builder.ts +37 -57
- package/src/tools/main/run-simplifier.ts +10 -20
- package/src/tools/main/run-verifier.ts +31 -55
- package/src/tools/utils/pi-subagent-delegation.ts +13 -22
- package/src/tools/utils/resolveToolRunContext.ts +2 -3
- package/src/utils/is-errno-exception.ts +1 -3
- package/src/utils/path-exists.ts +1 -0
- package/src/utils/path-strictly-within.ts +3 -6
- package/src/utils/path-within-or-equal.ts +1 -4
- package/src/utils/read-json.ts +1 -4
- package/src/utils/write-json.ts +1 -4
- package/src/workflow/builder/completeBuilderPass.ts +3 -2
- package/src/workflow/builder/prepareBuilderRun.ts +3 -8
- package/src/workflow/builder/resolveEscalations.ts +2 -6
- package/src/workflow/builder/utils/getWorkflowEventByBuilderHandoff.ts +1 -3
- package/src/workflow/simplifier/completeSimplifierPass.ts +2 -3
- package/src/workflow/spec/markSpecReady.ts +10 -4
- package/src/workflow/state/readWorkflowState.ts +2 -7
- package/src/workflow/state/schema.ts +5 -10
- package/src/workflow/state/writeWorkflowState.ts +1 -4
- package/src/workflow/transitions.ts +3 -11
- package/src/workflow/utils/assertSpecAcceptanceCriteria.ts +2 -1
- package/src/workflow/utils/getAgentRole.ts +1 -3
- package/src/workflow/verifier/completeVerifierPass.ts +1 -0
- package/src/workflow/verifier/resolveFindings.ts +9 -29
- package/src/workflow/verifier/utils/getWorkflowEventByVerifierHandoff.ts +5 -20
- package/templates/design.md +20 -0
- package/templates/proposal.md +23 -0
- package/templates/spec.md +21 -50
- package/ROADMAP.md +0 -319
package/extensions/maestro.ts
CHANGED
|
@@ -4,19 +4,14 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
7
|
+
|
|
7
8
|
import { loadConfiguration } from '#config/loadConfiguration.ts';
|
|
8
9
|
import { assertEnvironment } from '#maestro/checks/assertEnvironment.ts';
|
|
9
10
|
import { getMaestroInstructions } from '#maestro/instructions/getMaestroInstructions.ts';
|
|
10
11
|
import maestroSessionState from '#maestro/session/MaestroSessionState.ts';
|
|
11
12
|
import { refreshMaestroStatus } from '#maestro/status/refreshMaestroStatus.ts';
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
registerCreateSpecTool,
|
|
15
|
-
} from '#tools/main/create-spec.ts';
|
|
16
|
-
import {
|
|
17
|
-
MARK_SPEC_READY_TOOL,
|
|
18
|
-
registerMarkSpecReadyTool,
|
|
19
|
-
} from '#tools/main/mark-spec-ready.ts';
|
|
13
|
+
import { CREATE_SPEC_TOOL, registerCreateSpecTool } from '#tools/main/create-spec.ts';
|
|
14
|
+
import { MARK_SPEC_READY_TOOL, registerMarkSpecReadyTool } from '#tools/main/mark-spec-ready.ts';
|
|
20
15
|
import {
|
|
21
16
|
RESOLVE_ESCALATIONS_TOOL,
|
|
22
17
|
registerResolveEscalationsTool,
|
|
@@ -25,18 +20,10 @@ import {
|
|
|
25
20
|
RESOLVE_FINDINGS_TOOL,
|
|
26
21
|
registerResolveFindingsTool,
|
|
27
22
|
} from '#tools/main/resolve-findings.ts';
|
|
28
|
-
import {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
} from '#
|
|
32
|
-
import {
|
|
33
|
-
RUN_SIMPLIFIER_TOOL,
|
|
34
|
-
registerRunSimplifierTool,
|
|
35
|
-
} from '#tools/main/run-simplifier.ts';
|
|
36
|
-
import {
|
|
37
|
-
RUN_VERIFIER_TOOL,
|
|
38
|
-
registerRunVerifierTool,
|
|
39
|
-
} from '#tools/main/run-verifier.ts';
|
|
23
|
+
import { RUN_BUILDER_TOOL, registerRunBuilderTool } from '#tools/main/run-builder.ts';
|
|
24
|
+
import { RUN_SIMPLIFIER_TOOL, registerRunSimplifierTool } from '#tools/main/run-simplifier.ts';
|
|
25
|
+
import { RUN_VERIFIER_TOOL, registerRunVerifierTool } from '#tools/main/run-verifier.ts';
|
|
26
|
+
import { WORKFLOW_PHASES } from '#workflow/state/schema.ts';
|
|
40
27
|
|
|
41
28
|
const MAIN_TOOL_NAMES: readonly string[] = [
|
|
42
29
|
CREATE_SPEC_TOOL.NAME,
|
|
@@ -48,6 +35,19 @@ const MAIN_TOOL_NAMES: readonly string[] = [
|
|
|
48
35
|
RUN_SIMPLIFIER_TOOL.NAME,
|
|
49
36
|
];
|
|
50
37
|
|
|
38
|
+
type CompletedWorkflowResult = {
|
|
39
|
+
specId: string;
|
|
40
|
+
phase: typeof WORKFLOW_PHASES.CANDIDATE_READY;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const isCompletedWorkflowResult = (details: unknown): details is CompletedWorkflowResult =>
|
|
44
|
+
typeof details === 'object' &&
|
|
45
|
+
details !== null &&
|
|
46
|
+
'specId' in details &&
|
|
47
|
+
typeof details.specId === 'string' &&
|
|
48
|
+
'phase' in details &&
|
|
49
|
+
details.phase === WORKFLOW_PHASES.CANDIDATE_READY;
|
|
50
|
+
|
|
51
51
|
export default (pi: ExtensionAPI): void => {
|
|
52
52
|
registerCreateSpecTool(pi);
|
|
53
53
|
registerMarkSpecReadyTool(pi);
|
|
@@ -58,14 +58,10 @@ export default (pi: ExtensionAPI): void => {
|
|
|
58
58
|
registerRunSimplifierTool(pi);
|
|
59
59
|
|
|
60
60
|
const syncMainTools = (): void => {
|
|
61
|
-
const foreignTools = pi
|
|
62
|
-
.getActiveTools()
|
|
63
|
-
.filter((name) => !MAIN_TOOL_NAMES.includes(name));
|
|
61
|
+
const foreignTools = pi.getActiveTools().filter((name) => !MAIN_TOOL_NAMES.includes(name));
|
|
64
62
|
|
|
65
63
|
pi.setActiveTools(
|
|
66
|
-
maestroSessionState.isActive()
|
|
67
|
-
? [...foreignTools, ...MAIN_TOOL_NAMES]
|
|
68
|
-
: foreignTools,
|
|
64
|
+
maestroSessionState.isActive() ? [...foreignTools, ...MAIN_TOOL_NAMES] : foreignTools,
|
|
69
65
|
);
|
|
70
66
|
};
|
|
71
67
|
|
|
@@ -118,10 +114,7 @@ export default (pi: ExtensionAPI): void => {
|
|
|
118
114
|
'info',
|
|
119
115
|
);
|
|
120
116
|
} catch (error) {
|
|
121
|
-
context.ui.notify(
|
|
122
|
-
error instanceof Error ? error.message : String(error),
|
|
123
|
-
'error',
|
|
124
|
-
);
|
|
117
|
+
context.ui.notify(error instanceof Error ? error.message : String(error), 'error');
|
|
125
118
|
|
|
126
119
|
return;
|
|
127
120
|
}
|
|
@@ -146,8 +139,45 @@ export default (pi: ExtensionAPI): void => {
|
|
|
146
139
|
// Pi fires this after a tool returns, before its result reaches the AI.
|
|
147
140
|
// Refresh status after main Maestro tools, without changing the tool result.
|
|
148
141
|
pi.on('tool_result', async (event, context) => {
|
|
142
|
+
if (!event.isError && isCompletedWorkflowResult(event.details)) {
|
|
143
|
+
maestroSessionState.setWaitingForFinalResponseMode(event.details.specId);
|
|
144
|
+
}
|
|
145
|
+
|
|
149
146
|
if (MAIN_TOOL_NAMES.includes(event.toolName)) {
|
|
150
147
|
await refreshMaestroStatus(context);
|
|
151
148
|
}
|
|
152
149
|
});
|
|
150
|
+
|
|
151
|
+
// Finish at the response boundary, before Pi can process a queued follow-up.
|
|
152
|
+
// Assistant messages that request tools are still preparing the final summary.
|
|
153
|
+
pi.on('message_end', async (event, context) => {
|
|
154
|
+
const message = event.message;
|
|
155
|
+
|
|
156
|
+
if (
|
|
157
|
+
!maestroSessionState.isWaitingForFinalResponse() ||
|
|
158
|
+
message.role !== 'assistant' ||
|
|
159
|
+
message.stopReason === 'toolUse'
|
|
160
|
+
) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
maestroSessionState.stopWaitingForFinalResponse();
|
|
165
|
+
|
|
166
|
+
if (
|
|
167
|
+
!maestroSessionState.isActive() ||
|
|
168
|
+
context.signal?.aborted ||
|
|
169
|
+
message.stopReason !== 'stop' ||
|
|
170
|
+
!message.content.some((part) => part.type === 'text' && part.text.trim() !== '')
|
|
171
|
+
) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
maestroSessionState.deactivate();
|
|
176
|
+
syncMainTools();
|
|
177
|
+
await refreshMaestroStatus(context);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
pi.on('agent_end', () => {
|
|
181
|
+
maestroSessionState.stopWaitingForFinalResponse();
|
|
182
|
+
});
|
|
153
183
|
};
|
package/package.json
CHANGED
|
@@ -1,30 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@emiliosp/pi-maestro",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.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
|
-
"
|
|
6
|
+
"orchestration",
|
|
17
7
|
"pi",
|
|
18
8
|
"pi-coding-agent",
|
|
19
|
-
"
|
|
9
|
+
"pi-package",
|
|
20
10
|
"subagents",
|
|
21
|
-
"
|
|
22
|
-
],
|
|
23
|
-
"os": [
|
|
24
|
-
"darwin"
|
|
11
|
+
"workflow"
|
|
25
12
|
],
|
|
26
|
-
"
|
|
27
|
-
|
|
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/",
|
|
@@ -35,12 +28,15 @@
|
|
|
35
28
|
"docs/",
|
|
36
29
|
"MISSION.md",
|
|
37
30
|
"TECH_STACK.md",
|
|
38
|
-
"ROADMAP.md",
|
|
39
31
|
"CHANGELOG.md",
|
|
40
32
|
"ADR.md",
|
|
41
33
|
"README.md",
|
|
42
34
|
"LICENSE"
|
|
43
35
|
],
|
|
36
|
+
"os": [
|
|
37
|
+
"darwin"
|
|
38
|
+
],
|
|
39
|
+
"type": "module",
|
|
44
40
|
"imports": {
|
|
45
41
|
"#config/*": "./src/config/*",
|
|
46
42
|
"#specs/*": "./src/specs/*",
|
|
@@ -53,41 +49,46 @@
|
|
|
53
49
|
"#ids/*": "./src/ids/*",
|
|
54
50
|
"#*": "./*"
|
|
55
51
|
},
|
|
52
|
+
"publishConfig": {
|
|
53
|
+
"access": "public"
|
|
54
|
+
},
|
|
56
55
|
"scripts": {
|
|
57
56
|
"typecheck": "tsc --noEmit",
|
|
58
57
|
"test": "vitest run --coverage --config vitest.config.ts",
|
|
59
|
-
"check": "npm run lint
|
|
58
|
+
"check": "npm run fmt && npm run lint && npm run typecheck && npm test",
|
|
60
59
|
"prepublishOnly": "npm run check",
|
|
61
60
|
"publish:npm": "npm publish --access public",
|
|
62
|
-
"
|
|
63
|
-
"lint
|
|
64
|
-
"lint:fix": "biome check --write . && npm run lint:anti-slop"
|
|
61
|
+
"fmt": "oxfmt",
|
|
62
|
+
"lint": "oxlint --fix"
|
|
65
63
|
},
|
|
66
64
|
"dependencies": {
|
|
67
65
|
"pi-subagents": "0.78.0"
|
|
68
66
|
},
|
|
69
|
-
"bundleDependencies": [
|
|
70
|
-
"pi-subagents"
|
|
71
|
-
],
|
|
72
|
-
"peerDependencies": {
|
|
73
|
-
"@earendil-works/pi-ai": "*",
|
|
74
|
-
"@earendil-works/pi-coding-agent": "*",
|
|
75
|
-
"typebox": "*"
|
|
76
|
-
},
|
|
77
67
|
"devDependencies": {
|
|
78
|
-
"@biomejs/biome": "2.5.15",
|
|
79
68
|
"@earendil-works/pi-agent-core": "1.0.3",
|
|
80
69
|
"@earendil-works/pi-tui": "1.0.3",
|
|
81
|
-
"@vitest/coverage-v8": "5.0.3",
|
|
82
70
|
"@types/node": "26.6.4",
|
|
71
|
+
"@vitest/coverage-v8": "5.0.3",
|
|
72
|
+
"oxfmt": "0.73.0",
|
|
83
73
|
"oxlint": "1.87.0",
|
|
84
|
-
"oxlint-anti-slop": "0.3.
|
|
74
|
+
"oxlint-anti-slop": "0.3.8",
|
|
85
75
|
"typebox": "1.3.35",
|
|
86
76
|
"typescript": "7.0.2",
|
|
87
77
|
"vitest": "5.0.3"
|
|
88
78
|
},
|
|
89
|
-
"
|
|
90
|
-
"
|
|
79
|
+
"peerDependencies": {
|
|
80
|
+
"@earendil-works/pi-ai": "*",
|
|
81
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
82
|
+
"typebox": "*"
|
|
83
|
+
},
|
|
84
|
+
"bundleDependencies": [
|
|
85
|
+
"pi-subagents"
|
|
86
|
+
],
|
|
87
|
+
"engines": {
|
|
88
|
+
"node": ">=26.0.0"
|
|
89
|
+
},
|
|
90
|
+
"allowScripts": {
|
|
91
|
+
"fsevents@2.3.3": true
|
|
91
92
|
},
|
|
92
93
|
"pi": {
|
|
93
94
|
"extensions": [
|
|
@@ -98,8 +99,5 @@
|
|
|
98
99
|
"./agents"
|
|
99
100
|
]
|
|
100
101
|
}
|
|
101
|
-
},
|
|
102
|
-
"allowScripts": {
|
|
103
|
-
"fsevents@2.3.3": true
|
|
104
102
|
}
|
|
105
103
|
}
|
package/src/MaestroPaths.ts
CHANGED
|
@@ -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,
|
|
@@ -194,19 +186,14 @@ export class MaestroPaths {
|
|
|
194
186
|
return resolve(this.getSpecPath(specId), PATHS.PROTOTYPES_PATH);
|
|
195
187
|
}
|
|
196
188
|
|
|
197
|
-
public getPrototypePath({
|
|
198
|
-
specId,
|
|
199
|
-
relativePath,
|
|
200
|
-
}: PrototypePathInput): string {
|
|
189
|
+
public getPrototypePath({ specId, relativePath }: PrototypePathInput): string {
|
|
201
190
|
assertSafeRelativePrototypePath(relativePath);
|
|
202
191
|
|
|
203
192
|
const prototypesPath = this.getPrototypesPath(specId);
|
|
204
193
|
const target = resolve(prototypesPath, relativePath);
|
|
205
194
|
|
|
206
195
|
if (!isPathStrictlyWithin({ parent: prototypesPath, path: target })) {
|
|
207
|
-
throw new Error(
|
|
208
|
-
'Prototype path must stay inside the prototypes directory.',
|
|
209
|
-
);
|
|
196
|
+
throw new Error('Prototype path must stay inside the prototypes directory.');
|
|
210
197
|
}
|
|
211
198
|
|
|
212
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
|
|
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>;
|
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { isAbsolute, resolve } from 'node:path';
|
|
7
|
+
|
|
7
8
|
import { Value } from 'typebox/value';
|
|
9
|
+
|
|
8
10
|
import {
|
|
9
11
|
type SimplifierHandoff,
|
|
10
12
|
SimplifierHandoffSchema,
|
|
@@ -17,9 +19,7 @@ type AssertSimplifierHandoffInput<Handoff = unknown> = {
|
|
|
17
19
|
projectRoot: string;
|
|
18
20
|
};
|
|
19
21
|
|
|
20
|
-
function assertSimplifierHandoffSchema(
|
|
21
|
-
input: unknown,
|
|
22
|
-
): asserts input is SimplifierHandoff {
|
|
22
|
+
function assertSimplifierHandoffSchema(input: unknown): asserts input is SimplifierHandoff {
|
|
23
23
|
const [error] = Value.Errors(SimplifierHandoffSchema, input);
|
|
24
24
|
|
|
25
25
|
if (error !== undefined) {
|
|
@@ -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,
|
|
@@ -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') {
|
package/src/config/defaults.ts
CHANGED
|
@@ -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
|
|
|
@@ -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';
|
|
@@ -24,10 +25,7 @@ type ResolveSpecDirectoryInput = {
|
|
|
24
25
|
directory: string;
|
|
25
26
|
};
|
|
26
27
|
|
|
27
|
-
const resolveSpecDirectory = ({
|
|
28
|
-
projectRoot,
|
|
29
|
-
directory,
|
|
30
|
-
}: ResolveSpecDirectoryInput): string => {
|
|
28
|
+
const resolveSpecDirectory = ({ projectRoot, directory }: ResolveSpecDirectoryInput): string => {
|
|
31
29
|
if (directory.includes('\0')) {
|
|
32
30
|
throw new Error('specDirectory must not contain a null byte.');
|
|
33
31
|
}
|
|
@@ -59,10 +57,7 @@ type ResolveDirectoriesInput = {
|
|
|
59
57
|
config: MaestroConfig;
|
|
60
58
|
};
|
|
61
59
|
|
|
62
|
-
const resolveDirectories = ({
|
|
63
|
-
projectRoot,
|
|
64
|
-
config,
|
|
65
|
-
}: ResolveDirectoriesInput): MaestroConfig => {
|
|
60
|
+
const resolveDirectories = ({ projectRoot, config }: ResolveDirectoriesInput): MaestroConfig => {
|
|
66
61
|
const specDirectory = resolveSpecDirectory({
|
|
67
62
|
projectRoot,
|
|
68
63
|
directory: config.specDirectory,
|
|
@@ -85,9 +80,7 @@ const readConfigurationFile = async (path: string): Promise<unknown> => {
|
|
|
85
80
|
}
|
|
86
81
|
};
|
|
87
82
|
|
|
88
|
-
export const loadConfiguration = async (
|
|
89
|
-
cwd: string = process.cwd(),
|
|
90
|
-
): Promise<MaestroConfig> => {
|
|
83
|
+
export const loadConfiguration = async (cwd: string = process.cwd()): Promise<MaestroConfig> => {
|
|
91
84
|
const projectRoot = await realpath(cwd);
|
|
92
85
|
const targetPath = join(projectRoot, CONFIG_FILE_PATH);
|
|
93
86
|
|