@emiliosp/pi-maestro 0.5.2 → 0.6.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/README.md +38 -41
- package/agents/builder.md +16 -18
- package/agents/verifier.md +24 -21
- package/docs/configuration.md +16 -24
- package/docs/subagent-integration.md +18 -40
- package/docs/workflow.md +106 -169
- package/package.json +1 -2
- package/src/MaestroPaths.ts +111 -47
- package/src/artifacts/builder-handoff/assertBuilderHandoff.ts +2 -15
- package/src/artifacts/builder-handoff/readBuilderHandoff.ts +0 -3
- package/src/artifacts/builder-handoff/schema.ts +1 -1
- package/src/artifacts/builder-handoff/writeBuilderHandoff.ts +3 -5
- package/src/artifacts/escalation/createEscalation.ts +7 -7
- package/src/artifacts/escalation/getNextEscalationId.ts +0 -3
- package/src/artifacts/escalation/readEscalation.ts +1 -11
- package/src/artifacts/escalation/readEscalationHistory.ts +0 -3
- package/src/artifacts/escalation/resolveEscalation.ts +2 -12
- package/src/artifacts/escalation/schema.ts +0 -1
- package/src/artifacts/verifier-handoff/assertVerifierHandoff.ts +2 -9
- package/src/artifacts/verifier-handoff/readVerifierHandoff.ts +0 -3
- package/src/artifacts/verifier-handoff/schema.ts +20 -6
- package/src/artifacts/verifier-handoff/writeVerifierHandoff.ts +7 -7
- package/src/config/loadConfiguration.ts +18 -18
- package/src/maestro/checks/assertEnvironment.ts +13 -17
- package/src/maestro/instructions/getMaestroInstructions.ts +33 -24
- package/src/specs/create.ts +0 -2
- package/src/tools/child/open-escalation.ts +3 -4
- package/src/tools/child/record-builder-handoff.ts +4 -4
- package/src/tools/child/record-verifier-handoff.ts +13 -26
- package/src/tools/child/utils/resolveWorkflowContext.ts +3 -3
- package/src/tools/main/mark-spec-ready.ts +2 -2
- package/src/tools/main/resolve-escalation.ts +2 -4
- package/src/tools/main/resolve-findings.ts +17 -15
- package/src/tools/main/run-builder.ts +10 -38
- package/src/tools/main/run-verifier.ts +6 -33
- package/src/tools/utils/resolveToolRunContext.ts +8 -8
- package/src/utils/path-strictly-within.ts +4 -4
- package/src/utils/write-json.ts +24 -0
- package/src/workflow/builder/completeBuilderPass.ts +6 -15
- package/src/workflow/builder/prepareBuilderRun.ts +3 -34
- package/src/workflow/escalation/openBuilderEscalation.ts +2 -10
- package/src/workflow/escalation/resolveBuilderEscalation.ts +9 -14
- package/src/workflow/findings/resolveFindings.ts +25 -135
- package/src/workflow/spec/markSpecReady.ts +0 -1
- package/src/workflow/state/readWorkflowState.ts +2 -2
- package/src/workflow/state/schema.ts +0 -1
- package/src/workflow/state/writeWorkflowState.ts +7 -63
- package/src/workflow/transitions.ts +2 -2
- package/src/workflow/verifier/completeVerifierPass.ts +8 -14
- package/src/workflow/verifier/prepareVerifierRun.ts +4 -41
- package/src/artifacts/verifier-handoff/rejectVerifierFinding.ts +0 -54
- package/src/git/command.ts +0 -172
- package/src/git/commits/createCommit.ts +0 -76
- package/src/git/commits/createWorkflowCheckpointCommit.ts +0 -24
- package/src/git/commits/findCommitByMessage.ts +0 -39
- package/src/git/commits/getStagedPaths.ts +0 -23
- package/src/git/history/getParentCommit.ts +0 -25
- package/src/git/repository/assertRepositoryTrusted.ts +0 -16
- package/src/git/repository/findRepositoryRoot.ts +0 -19
- package/src/git/repository/getCurrentBranch.ts +0 -18
- package/src/git/repository/getHeadCommit.ts +0 -18
- package/src/git/repository/getRepositoryStatus.ts +0 -74
- package/src/git/utils/hasGitExitCode.ts +0 -19
- package/src/utils/write-atomically.ts +0 -41
- package/src/utils/write-json-atomically.ts +0 -28
|
@@ -6,19 +6,17 @@
|
|
|
6
6
|
import { StringEnum } from '@earendil-works/pi-ai';
|
|
7
7
|
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
8
8
|
import { Type } from 'typebox';
|
|
9
|
+
import { FINDING_DECISIONS } from '#artifacts/verifier-handoff/schema.ts';
|
|
9
10
|
import { SPEC_ID_PATTERN } from '#ids/isValidSpecId.ts';
|
|
10
11
|
import { resolveToolRunContext } from '#tools/utils/resolveToolRunContext.ts';
|
|
11
|
-
import {
|
|
12
|
-
FINDING_DECISIONS,
|
|
13
|
-
resolveFindings,
|
|
14
|
-
} from '#workflow/findings/resolveFindings.ts';
|
|
12
|
+
import { resolveFindings } from '#workflow/findings/resolveFindings.ts';
|
|
15
13
|
import { WORKFLOW_PHASES } from '#workflow/state/schema.ts';
|
|
16
14
|
|
|
17
15
|
export const RESOLVE_FINDINGS_TOOL = {
|
|
18
16
|
NAME: 'maestro_resolve_findings',
|
|
19
17
|
LABEL: 'Resolve Findings',
|
|
20
18
|
DESCRIPTION:
|
|
21
|
-
'Record the explicit owner decision for every current verifier finding when the approved contract remains valid.
|
|
19
|
+
'Record the explicit owner decision for every current verifier finding when the approved contract remains valid. Save the resolution and workflow transition. If the contract must change, edit spec.md and use maestro_mark_spec_ready instead.',
|
|
22
20
|
} as const;
|
|
23
21
|
|
|
24
22
|
const FindingDecisionFields = {
|
|
@@ -31,7 +29,9 @@ const RejectFindingDecisionSchema = Type.Object(
|
|
|
31
29
|
decision: StringEnum([FINDING_DECISIONS.REJECT] as const),
|
|
32
30
|
reason: Type.String({ minLength: 1 }),
|
|
33
31
|
},
|
|
34
|
-
{
|
|
32
|
+
{
|
|
33
|
+
additionalProperties: false,
|
|
34
|
+
},
|
|
35
35
|
);
|
|
36
36
|
|
|
37
37
|
const FixCodeFindingDecisionSchema = Type.Object(
|
|
@@ -39,7 +39,9 @@ const FixCodeFindingDecisionSchema = Type.Object(
|
|
|
39
39
|
...FindingDecisionFields,
|
|
40
40
|
decision: StringEnum([FINDING_DECISIONS.FIX_CODE] as const),
|
|
41
41
|
},
|
|
42
|
-
{
|
|
42
|
+
{
|
|
43
|
+
additionalProperties: false,
|
|
44
|
+
},
|
|
43
45
|
);
|
|
44
46
|
|
|
45
47
|
const FindingDecisionSchema = Type.Union([
|
|
@@ -57,10 +59,8 @@ const ResolveFindingsToolParameters = Type.Object(
|
|
|
57
59
|
|
|
58
60
|
type FindingResolutionDetails = {
|
|
59
61
|
specId: string;
|
|
60
|
-
revision: number;
|
|
61
62
|
phase: string;
|
|
62
|
-
|
|
63
|
-
checkpointCommit: string;
|
|
63
|
+
projectRoot: string;
|
|
64
64
|
rejectedFindingIds: string[];
|
|
65
65
|
findingsRequiringFixIds: string[];
|
|
66
66
|
};
|
|
@@ -102,15 +102,17 @@ export const registerResolveFindingsTool = (pi: ExtensionAPI): void => {
|
|
|
102
102
|
|
|
103
103
|
const details: FindingResolutionDetails = {
|
|
104
104
|
specId: resolved.state.specId,
|
|
105
|
-
revision: resolved.state.revision,
|
|
106
105
|
phase: resolved.state.phase,
|
|
107
|
-
|
|
108
|
-
checkpointCommit: resolved.checkpointCommit,
|
|
106
|
+
projectRoot: resolved.projectRoot,
|
|
109
107
|
rejectedFindingIds: resolved.findings
|
|
110
|
-
.filter(
|
|
108
|
+
.filter(
|
|
109
|
+
({ decision }) => decision?.decision === FINDING_DECISIONS.REJECT,
|
|
110
|
+
)
|
|
111
111
|
.map(({ id }) => id),
|
|
112
112
|
findingsRequiringFixIds: resolved.findings
|
|
113
|
-
.filter(
|
|
113
|
+
.filter(
|
|
114
|
+
({ decision }) => decision?.decision === FINDING_DECISIONS.FIX_CODE,
|
|
115
|
+
)
|
|
114
116
|
.map(({ id }) => id),
|
|
115
117
|
};
|
|
116
118
|
|
|
@@ -15,7 +15,6 @@ import {
|
|
|
15
15
|
import { readEscalationHistory } from '#artifacts/escalation/readEscalationHistory.ts';
|
|
16
16
|
import type { Escalation } from '#artifacts/escalation/schema.ts';
|
|
17
17
|
import { AGENTS } from '#config/schema.ts';
|
|
18
|
-
import { getRepositoryStatus } from '#git/repository/getRepositoryStatus.ts';
|
|
19
18
|
import { SPEC_ID_PATTERN } from '#ids/isValidSpecId.ts';
|
|
20
19
|
import type { MaestroPaths } from '#MaestroPaths.ts';
|
|
21
20
|
import { refreshMaestroStatus } from '#maestro/status/refreshMaestroStatus.ts';
|
|
@@ -33,7 +32,7 @@ export const RUN_BUILDER_TOOL = {
|
|
|
33
32
|
NAME: 'maestro_run_builder',
|
|
34
33
|
LABEL: 'Run Builder',
|
|
35
34
|
DESCRIPTION:
|
|
36
|
-
'Run the builder in the
|
|
35
|
+
'Run the builder in the project directory for an owner-approved spec. The builder runs in the foreground and cannot be retried after a recorded failure.',
|
|
37
36
|
} as const;
|
|
38
37
|
|
|
39
38
|
const RunBuilderToolParameters = Type.Object(
|
|
@@ -49,45 +48,32 @@ type BuilderRunResult =
|
|
|
49
48
|
| {
|
|
50
49
|
outcome: typeof BUILDER_HANDOFF_STATUSES.DONE;
|
|
51
50
|
specId: string;
|
|
52
|
-
revision: number;
|
|
53
51
|
phase: typeof WORKFLOW_PHASES.READY_FOR_VERIFIER;
|
|
54
52
|
handoff: BuilderHandoff;
|
|
55
53
|
}
|
|
56
54
|
| {
|
|
57
55
|
outcome: typeof BUILDER_HANDOFF_STATUSES.FAILED;
|
|
58
56
|
specId: string;
|
|
59
|
-
revision: number;
|
|
60
57
|
phase: typeof WORKFLOW_PHASES.BUILDER_FAILED;
|
|
61
58
|
handoff: BuilderHandoff;
|
|
62
59
|
}
|
|
63
60
|
| {
|
|
64
61
|
outcome: typeof BUILDER_HANDOFF_STATUSES.ESCALATION;
|
|
65
62
|
specId: string;
|
|
66
|
-
revision: number;
|
|
67
63
|
phase: typeof WORKFLOW_PHASES.ESCALATION_DECISION;
|
|
68
64
|
escalation: Escalation;
|
|
69
65
|
};
|
|
70
66
|
|
|
71
|
-
type
|
|
67
|
+
type ReadBuilderResultInput = {
|
|
72
68
|
paths: MaestroPaths;
|
|
73
69
|
specId: string;
|
|
74
70
|
};
|
|
75
71
|
|
|
76
|
-
const
|
|
72
|
+
const readBuilderResult = async ({
|
|
77
73
|
paths,
|
|
78
74
|
specId,
|
|
79
|
-
}:
|
|
75
|
+
}: ReadBuilderResultInput): Promise<BuilderRunResult> => {
|
|
80
76
|
try {
|
|
81
|
-
const repositoryStatus = await getRepositoryStatus(
|
|
82
|
-
paths.getRepositoryRoot(),
|
|
83
|
-
);
|
|
84
|
-
|
|
85
|
-
if (!repositoryStatus.clean) {
|
|
86
|
-
throw new Error(
|
|
87
|
-
'The builder returned without committing its final artifact and current work.',
|
|
88
|
-
);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
77
|
const state = await readWorkflowState(paths.getWorkflowPath(specId));
|
|
92
78
|
|
|
93
79
|
if (state.specId !== specId) {
|
|
@@ -107,7 +93,6 @@ const readCommittedBuilderResult = async ({
|
|
|
107
93
|
type ReadTerminalBuilderResultInput = {
|
|
108
94
|
paths: MaestroPaths;
|
|
109
95
|
specId: string;
|
|
110
|
-
revision: number;
|
|
111
96
|
phase:
|
|
112
97
|
| typeof WORKFLOW_PHASES.READY_FOR_VERIFIER
|
|
113
98
|
| typeof WORKFLOW_PHASES.BUILDER_FAILED;
|
|
@@ -116,13 +101,11 @@ type ReadTerminalBuilderResultInput = {
|
|
|
116
101
|
const readTerminalBuilderResult = async ({
|
|
117
102
|
paths,
|
|
118
103
|
specId,
|
|
119
|
-
revision,
|
|
120
104
|
phase,
|
|
121
105
|
}: ReadTerminalBuilderResultInput): Promise<BuilderRunResult> => {
|
|
122
106
|
const handoff = await readBuilderHandoff({
|
|
123
|
-
path: paths.
|
|
107
|
+
path: await paths.getActiveBuilderHandoffPath(specId),
|
|
124
108
|
specId,
|
|
125
|
-
revision,
|
|
126
109
|
});
|
|
127
110
|
|
|
128
111
|
const expectedStatus =
|
|
@@ -140,7 +123,6 @@ const readTerminalBuilderResult = async ({
|
|
|
140
123
|
return {
|
|
141
124
|
outcome: BUILDER_HANDOFF_STATUSES.DONE,
|
|
142
125
|
specId,
|
|
143
|
-
revision,
|
|
144
126
|
phase,
|
|
145
127
|
handoff,
|
|
146
128
|
};
|
|
@@ -149,7 +131,6 @@ const readTerminalBuilderResult = async ({
|
|
|
149
131
|
return {
|
|
150
132
|
outcome: BUILDER_HANDOFF_STATUSES.FAILED,
|
|
151
133
|
specId,
|
|
152
|
-
revision,
|
|
153
134
|
phase,
|
|
154
135
|
handoff,
|
|
155
136
|
};
|
|
@@ -173,7 +154,6 @@ const buildRunResult = async ({
|
|
|
173
154
|
return await readTerminalBuilderResult({
|
|
174
155
|
paths,
|
|
175
156
|
specId,
|
|
176
|
-
revision: state.revision,
|
|
177
157
|
phase: state.phase,
|
|
178
158
|
});
|
|
179
159
|
}
|
|
@@ -182,25 +162,17 @@ const buildRunResult = async ({
|
|
|
182
162
|
const history = await readEscalationHistory({
|
|
183
163
|
directory: paths.getEscalationsPath(specId),
|
|
184
164
|
specId,
|
|
185
|
-
currentRevision: state.revision,
|
|
186
165
|
});
|
|
187
166
|
|
|
188
167
|
const escalation = history.at(-1);
|
|
189
168
|
|
|
190
|
-
if (
|
|
191
|
-
escalation
|
|
192
|
-
escalation.revision !== state.revision ||
|
|
193
|
-
escalation.resolution !== null
|
|
194
|
-
) {
|
|
195
|
-
throw new Error(
|
|
196
|
-
'The builder escalation is missing, stale, or already resolved.',
|
|
197
|
-
);
|
|
169
|
+
if (escalation === undefined || escalation.resolution !== null) {
|
|
170
|
+
throw new Error('The builder escalation is missing or already resolved.');
|
|
198
171
|
}
|
|
199
172
|
|
|
200
173
|
return {
|
|
201
174
|
outcome: BUILDER_HANDOFF_STATUSES.ESCALATION,
|
|
202
175
|
specId,
|
|
203
|
-
revision: state.revision,
|
|
204
176
|
phase: state.phase,
|
|
205
177
|
escalation,
|
|
206
178
|
};
|
|
@@ -242,9 +214,9 @@ export const registerRunBuilderTool = (pi: ExtensionAPI): void => {
|
|
|
242
214
|
// pi-subagents loads this name from agents/builder.md through package.json.
|
|
243
215
|
// See docs/subagent-integration.md.
|
|
244
216
|
agent: AGENTS.BUILDER,
|
|
245
|
-
task: `Implement specId "${specId}" in the
|
|
217
|
+
task: `Implement specId "${specId}" in the project directory "${run.projectRoot}".`,
|
|
246
218
|
context: 'fresh',
|
|
247
|
-
cwd: run.
|
|
219
|
+
cwd: run.projectRoot,
|
|
248
220
|
model: config.builder.model,
|
|
249
221
|
thinking: config.builder.thinking,
|
|
250
222
|
timeoutMs: config.builder.timeoutMinutes * MILLISECONDS_PER_MINUTE,
|
|
@@ -257,7 +229,7 @@ export const registerRunBuilderTool = (pi: ExtensionAPI): void => {
|
|
|
257
229
|
});
|
|
258
230
|
|
|
259
231
|
assertDelegationResponse(response);
|
|
260
|
-
const result = await
|
|
232
|
+
const result = await readBuilderResult({ paths, specId });
|
|
261
233
|
|
|
262
234
|
return {
|
|
263
235
|
content: [{ type: 'text', text: formatBuilderResult(result) }],
|
|
@@ -10,7 +10,6 @@ import { Type } from 'typebox';
|
|
|
10
10
|
import { readVerifierHandoff } from '#artifacts/verifier-handoff/readVerifierHandoff.ts';
|
|
11
11
|
import type { VerifierHandoff } from '#artifacts/verifier-handoff/schema.ts';
|
|
12
12
|
import { AGENTS } from '#config/schema.ts';
|
|
13
|
-
import { getRepositoryStatus } from '#git/repository/getRepositoryStatus.ts';
|
|
14
13
|
import { SPEC_ID_PATTERN } from '#ids/isValidSpecId.ts';
|
|
15
14
|
import type { MaestroPaths } from '#MaestroPaths.ts';
|
|
16
15
|
import { refreshMaestroStatus } from '#maestro/status/refreshMaestroStatus.ts';
|
|
@@ -22,16 +21,13 @@ import { resolveToolRunContext } from '#tools/utils/resolveToolRunContext.ts';
|
|
|
22
21
|
import { WORKFLOW_ROLES } from '#workflow/roles.ts';
|
|
23
22
|
import { readWorkflowState } from '#workflow/state/readWorkflowState.ts';
|
|
24
23
|
import { WORKFLOW_PHASES, type WorkflowState } from '#workflow/state/schema.ts';
|
|
25
|
-
import {
|
|
26
|
-
prepareVerifierRun,
|
|
27
|
-
type VerifierRun,
|
|
28
|
-
} from '#workflow/verifier/prepareVerifierRun.ts';
|
|
24
|
+
import { prepareVerifierRun } from '#workflow/verifier/prepareVerifierRun.ts';
|
|
29
25
|
|
|
30
26
|
export const RUN_VERIFIER_TOOL = {
|
|
31
27
|
NAME: 'maestro_run_verifier',
|
|
32
28
|
LABEL: 'Run Verifier',
|
|
33
29
|
DESCRIPTION:
|
|
34
|
-
'Run the verifier in the
|
|
30
|
+
'Run the verifier in the project directory for a completed builder candidate. The verifier runs in the foreground.',
|
|
35
31
|
} as const;
|
|
36
32
|
|
|
37
33
|
const RunVerifierToolParameters = Type.Object(
|
|
@@ -46,20 +42,14 @@ const MILLISECONDS_PER_MINUTE = 60_000;
|
|
|
46
42
|
type CandidateReadyResult = {
|
|
47
43
|
outcome: typeof WORKFLOW_PHASES.CANDIDATE_READY;
|
|
48
44
|
specId: string;
|
|
49
|
-
revision: number;
|
|
50
45
|
phase: typeof WORKFLOW_PHASES.CANDIDATE_READY;
|
|
51
|
-
candidateCommit: string;
|
|
52
|
-
checkpointCommit: string;
|
|
53
46
|
handoff: VerifierHandoff;
|
|
54
47
|
};
|
|
55
48
|
|
|
56
49
|
type FindingsResult = {
|
|
57
50
|
outcome: typeof WORKFLOW_PHASES.FINDINGS_DECISION;
|
|
58
51
|
specId: string;
|
|
59
|
-
revision: number;
|
|
60
52
|
phase: typeof WORKFLOW_PHASES.FINDINGS_DECISION;
|
|
61
|
-
candidateCommit: string;
|
|
62
|
-
checkpointCommit: string;
|
|
63
53
|
handoff: VerifierHandoff;
|
|
64
54
|
};
|
|
65
55
|
|
|
@@ -68,14 +58,12 @@ type VerifierRunResult = CandidateReadyResult | FindingsResult;
|
|
|
68
58
|
type ReadTerminalVerifierResultInput = {
|
|
69
59
|
paths: MaestroPaths;
|
|
70
60
|
specId: string;
|
|
71
|
-
run: VerifierRun;
|
|
72
61
|
state: WorkflowState;
|
|
73
62
|
};
|
|
74
63
|
|
|
75
64
|
const readTerminalVerifierResult = async ({
|
|
76
65
|
paths,
|
|
77
66
|
specId,
|
|
78
|
-
run,
|
|
79
67
|
state,
|
|
80
68
|
}: ReadTerminalVerifierResultInput): Promise<
|
|
81
69
|
CandidateReadyResult | FindingsResult
|
|
@@ -89,18 +77,9 @@ const readTerminalVerifierResult = async ({
|
|
|
89
77
|
);
|
|
90
78
|
}
|
|
91
79
|
|
|
92
|
-
const repositoryStatus = await getRepositoryStatus(run.repositoryRoot);
|
|
93
|
-
|
|
94
|
-
if (!repositoryStatus.clean) {
|
|
95
|
-
throw new Error(
|
|
96
|
-
'The verifier returned without committing its final handoff.',
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
80
|
const handoff = await readVerifierHandoff({
|
|
101
|
-
path: paths.
|
|
81
|
+
path: await paths.getActiveVerifierHandoffPath(specId),
|
|
102
82
|
specId,
|
|
103
|
-
revision: state.revision,
|
|
104
83
|
});
|
|
105
84
|
|
|
106
85
|
const expectedPhase =
|
|
@@ -116,9 +95,6 @@ const readTerminalVerifierResult = async ({
|
|
|
116
95
|
|
|
117
96
|
const details = {
|
|
118
97
|
specId,
|
|
119
|
-
revision: state.revision,
|
|
120
|
-
candidateCommit: run.candidateCommit,
|
|
121
|
-
checkpointCommit: run.checkpointCommit,
|
|
122
98
|
handoff,
|
|
123
99
|
};
|
|
124
100
|
|
|
@@ -132,13 +108,11 @@ const readTerminalVerifierResult = async ({
|
|
|
132
108
|
type ReadVerifierResultInput = {
|
|
133
109
|
paths: MaestroPaths;
|
|
134
110
|
specId: string;
|
|
135
|
-
run: VerifierRun;
|
|
136
111
|
};
|
|
137
112
|
|
|
138
113
|
const readVerifierResult = async ({
|
|
139
114
|
paths,
|
|
140
115
|
specId,
|
|
141
|
-
run,
|
|
142
116
|
}: ReadVerifierResultInput): Promise<VerifierRunResult> => {
|
|
143
117
|
try {
|
|
144
118
|
const state = await readWorkflowState(paths.getWorkflowPath(specId));
|
|
@@ -158,7 +132,6 @@ const readVerifierResult = async ({
|
|
|
158
132
|
return await readTerminalVerifierResult({
|
|
159
133
|
paths,
|
|
160
134
|
specId,
|
|
161
|
-
run,
|
|
162
135
|
state,
|
|
163
136
|
});
|
|
164
137
|
} catch (error) {
|
|
@@ -194,9 +167,9 @@ export const registerRunVerifierTool = (pi: ExtensionAPI): void => {
|
|
|
194
167
|
nodeId: WORKFLOW_ROLES.VERIFIER,
|
|
195
168
|
// pi-subagents loads this name from agents/verifier.md through package.json.
|
|
196
169
|
agent: AGENTS.VERIFIER,
|
|
197
|
-
task: `Verify specId "${specId}"
|
|
170
|
+
task: `Verify specId "${specId}" in the live project directory "${run.projectRoot}"`,
|
|
198
171
|
context: 'fresh',
|
|
199
|
-
cwd: run.
|
|
172
|
+
cwd: run.projectRoot,
|
|
200
173
|
model: config.verifier.model,
|
|
201
174
|
thinking: config.verifier.thinking,
|
|
202
175
|
timeoutMs: config.verifier.timeoutMinutes * MILLISECONDS_PER_MINUTE,
|
|
@@ -209,7 +182,7 @@ export const registerRunVerifierTool = (pi: ExtensionAPI): void => {
|
|
|
209
182
|
});
|
|
210
183
|
|
|
211
184
|
assertDelegationResponse(response);
|
|
212
|
-
const result = await readVerifierResult({ paths, specId
|
|
185
|
+
const result = await readVerifierResult({ paths, specId });
|
|
213
186
|
|
|
214
187
|
return {
|
|
215
188
|
content: [{ type: 'text', text: formatVerifierResult(result) }],
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Objective: Resolve the
|
|
3
|
-
* Used: Whenever a Pi tool needs the
|
|
2
|
+
* Objective: Resolve the project context shared by Pi tools.
|
|
3
|
+
* Used: Whenever a Pi tool needs the project directory, configuration, and paths.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { realpath } from 'node:fs/promises';
|
|
6
7
|
import { loadConfiguration } from '#config/loadConfiguration.ts';
|
|
7
8
|
import type { MaestroConfig } from '#config/schema.ts';
|
|
8
|
-
import { findRepositoryRoot } from '#git/repository/findRepositoryRoot.ts';
|
|
9
9
|
import { MaestroPaths } from '#MaestroPaths.ts';
|
|
10
10
|
|
|
11
11
|
export type ToolRunContext = {
|
|
12
|
-
|
|
12
|
+
projectRoot: string;
|
|
13
13
|
config: MaestroConfig;
|
|
14
14
|
paths: MaestroPaths;
|
|
15
15
|
};
|
|
@@ -17,12 +17,12 @@ export type ToolRunContext = {
|
|
|
17
17
|
export const resolveToolRunContext = async (
|
|
18
18
|
cwd: string,
|
|
19
19
|
): Promise<ToolRunContext> => {
|
|
20
|
-
const
|
|
21
|
-
const config = await loadConfiguration(
|
|
20
|
+
const projectRoot = await realpath(cwd);
|
|
21
|
+
const config = await loadConfiguration(projectRoot);
|
|
22
22
|
|
|
23
23
|
return {
|
|
24
|
-
|
|
24
|
+
projectRoot,
|
|
25
25
|
config,
|
|
26
|
-
paths: new MaestroPaths({
|
|
26
|
+
paths: new MaestroPaths({ projectRoot, config }),
|
|
27
27
|
};
|
|
28
28
|
};
|
|
@@ -8,12 +8,12 @@ import { isPathWithinOrEqual } from '#utils/path-within-or-equal.ts';
|
|
|
8
8
|
|
|
9
9
|
type IsPathStrictlyWithinInput = {
|
|
10
10
|
parent: string;
|
|
11
|
-
|
|
11
|
+
path: string;
|
|
12
12
|
};
|
|
13
13
|
|
|
14
14
|
export const isPathStrictlyWithin = ({
|
|
15
15
|
parent,
|
|
16
|
-
|
|
16
|
+
path,
|
|
17
17
|
}: IsPathStrictlyWithinInput): boolean =>
|
|
18
|
-
relative(parent,
|
|
19
|
-
isPathWithinOrEqual({ parent, candidate });
|
|
18
|
+
relative(parent, path) !== '' &&
|
|
19
|
+
isPathWithinOrEqual({ parent, candidate: path });
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Objective: Write formatted JSON to a file.
|
|
3
|
+
* Used: When Maestro writes workflow state and artifact JSON files.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { writeFile } from 'node:fs/promises';
|
|
7
|
+
|
|
8
|
+
type WriteJsonInput = {
|
|
9
|
+
path: string;
|
|
10
|
+
data: unknown;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const writeJson = async ({
|
|
14
|
+
path,
|
|
15
|
+
data,
|
|
16
|
+
}: WriteJsonInput): Promise<void> => {
|
|
17
|
+
const content = JSON.stringify(data, null, 2);
|
|
18
|
+
|
|
19
|
+
if (content === undefined) {
|
|
20
|
+
throw new Error('JSON data must be serializable.');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
await writeFile(path, `${content}\n`);
|
|
24
|
+
};
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { mkdir } from 'node:fs/promises';
|
|
7
|
-
import { dirname } from 'node:path';
|
|
8
7
|
import { assertBuilderHandoff } from '#artifacts/builder-handoff/assertBuilderHandoff.ts';
|
|
9
8
|
import {
|
|
10
9
|
BUILDER_HANDOFF_STATUSES,
|
|
@@ -14,7 +13,6 @@ import {
|
|
|
14
13
|
} from '#artifacts/builder-handoff/schema.ts';
|
|
15
14
|
import { writeBuilderHandoff } from '#artifacts/builder-handoff/writeBuilderHandoff.ts';
|
|
16
15
|
import type { MaestroPaths } from '#MaestroPaths.ts';
|
|
17
|
-
import { pathExists } from '#utils/path-exists.ts';
|
|
18
16
|
import { readWorkflowState } from '#workflow/state/readWorkflowState.ts';
|
|
19
17
|
import {
|
|
20
18
|
WORKFLOW_EVENTS,
|
|
@@ -27,7 +25,8 @@ import { transitionWorkflow } from '#workflow/transitions.ts';
|
|
|
27
25
|
export type CompletedBuilderPass = {
|
|
28
26
|
handoff: BuilderHandoff;
|
|
29
27
|
state: WorkflowState;
|
|
30
|
-
|
|
28
|
+
projectRoot: string;
|
|
29
|
+
handoffPath: string;
|
|
31
30
|
};
|
|
32
31
|
|
|
33
32
|
type BuildBuilderHandoffInput = {
|
|
@@ -43,7 +42,6 @@ const buildBuilderHandoff = ({
|
|
|
43
42
|
return {
|
|
44
43
|
version: BUILDER_HANDOFF_VERSION,
|
|
45
44
|
specId: state.specId,
|
|
46
|
-
revision: state.revision + 1,
|
|
47
45
|
status: draftHandoff.status,
|
|
48
46
|
summary: draftHandoff.summary,
|
|
49
47
|
acceptanceCriteria: draftHandoff.acceptanceCriteria,
|
|
@@ -55,7 +53,6 @@ const buildBuilderHandoff = ({
|
|
|
55
53
|
return {
|
|
56
54
|
version: BUILDER_HANDOFF_VERSION,
|
|
57
55
|
specId: state.specId,
|
|
58
|
-
revision: state.revision + 1,
|
|
59
56
|
status: draftHandoff.status,
|
|
60
57
|
summary: draftHandoff.summary,
|
|
61
58
|
acceptanceCriteria: draftHandoff.acceptanceCriteria,
|
|
@@ -75,7 +72,6 @@ export const completeBuilderPass = async ({
|
|
|
75
72
|
handoff: draftHandoff,
|
|
76
73
|
}: CompleteBuilderPassInput): Promise<CompletedBuilderPass> => {
|
|
77
74
|
const workflowPath = paths.getWorkflowPath(specId);
|
|
78
|
-
const handoffPath = paths.getBuilderHandoffPath(specId);
|
|
79
75
|
const currentState = await readWorkflowState(workflowPath);
|
|
80
76
|
|
|
81
77
|
if (currentState.specId !== specId) {
|
|
@@ -90,14 +86,9 @@ export const completeBuilderPass = async ({
|
|
|
90
86
|
);
|
|
91
87
|
}
|
|
92
88
|
|
|
93
|
-
if (await pathExists(handoffPath)) {
|
|
94
|
-
throw new Error('Builder terminal handoff already exists.');
|
|
95
|
-
}
|
|
96
|
-
|
|
97
89
|
const handoffInput = {
|
|
98
90
|
handoff: buildBuilderHandoff({ draftHandoff, state: currentState }),
|
|
99
91
|
specId: currentState.specId,
|
|
100
|
-
revision: currentState.revision + 1,
|
|
101
92
|
};
|
|
102
93
|
|
|
103
94
|
assertBuilderHandoff(handoffInput);
|
|
@@ -111,24 +102,24 @@ export const completeBuilderPass = async ({
|
|
|
111
102
|
: WORKFLOW_EVENTS.BUILDER_FAILED,
|
|
112
103
|
});
|
|
113
104
|
|
|
114
|
-
await mkdir(
|
|
105
|
+
await mkdir(paths.getBuilderHandoffsPath(specId), { recursive: true });
|
|
106
|
+
const handoffPath = await paths.getNextBuilderHandoffPath(specId);
|
|
115
107
|
|
|
116
108
|
await writeBuilderHandoff({
|
|
117
109
|
path: handoffPath,
|
|
118
110
|
handoff,
|
|
119
111
|
specId: currentState.specId,
|
|
120
|
-
revision: nextState.revision,
|
|
121
112
|
});
|
|
122
113
|
|
|
123
114
|
await writeWorkflowState({
|
|
124
115
|
path: workflowPath,
|
|
125
116
|
state: nextState,
|
|
126
|
-
currentRevision: currentState.revision,
|
|
127
117
|
});
|
|
128
118
|
|
|
129
119
|
return {
|
|
130
120
|
handoff,
|
|
121
|
+
handoffPath,
|
|
131
122
|
state: nextState,
|
|
132
|
-
|
|
123
|
+
projectRoot: paths.getProjectRoot(),
|
|
133
124
|
};
|
|
134
125
|
};
|
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Objective: Prepare a
|
|
2
|
+
* Objective: Prepare a builder run transition in the project directory.
|
|
3
3
|
* Used: Before Maestro runs a builder pass.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { rm } from 'node:fs/promises';
|
|
7
|
-
import { createWorkflowCheckpointCommit } from '#git/commits/createWorkflowCheckpointCommit.ts';
|
|
8
|
-
import { getRepositoryStatus } from '#git/repository/getRepositoryStatus.ts';
|
|
9
6
|
import type { MaestroPaths } from '#MaestroPaths.ts';
|
|
10
7
|
import maestroSessionState from '#maestro/session/MaestroSessionState.ts';
|
|
11
8
|
import { pathExists } from '#utils/path-exists.ts';
|
|
@@ -16,9 +13,7 @@ import { transitionWorkflow } from '#workflow/transitions.ts';
|
|
|
16
13
|
|
|
17
14
|
export type BuilderRun = {
|
|
18
15
|
specId: string;
|
|
19
|
-
|
|
20
|
-
repositoryRoot: string;
|
|
21
|
-
checkpointCommit: string;
|
|
16
|
+
projectRoot: string;
|
|
22
17
|
};
|
|
23
18
|
|
|
24
19
|
type AssertBuilderRunBaseInput = {
|
|
@@ -42,12 +37,6 @@ async function assertBuilderRunBase({
|
|
|
42
37
|
throw new Error(`Builder run is not valid from phase "${state.phase}".`);
|
|
43
38
|
}
|
|
44
39
|
|
|
45
|
-
const status = await getRepositoryStatus(paths.getRepositoryRoot());
|
|
46
|
-
|
|
47
|
-
if (!status.clean) {
|
|
48
|
-
throw new Error('Builder run requires a clean current checkout.');
|
|
49
|
-
}
|
|
50
|
-
|
|
51
40
|
if (!(await pathExists(paths.getSpecFilePath(specId)))) {
|
|
52
41
|
throw new Error(`Spec file is missing: ${paths.getSpecFilePath(specId)}.`);
|
|
53
42
|
}
|
|
@@ -73,13 +62,7 @@ export const prepareBuilderRun = async ({
|
|
|
73
62
|
await assertBuilderRunBase({ paths, specId });
|
|
74
63
|
|
|
75
64
|
const workflowPath = paths.getWorkflowPath(specId);
|
|
76
|
-
const handoffPath = paths.getBuilderHandoffPath(specId);
|
|
77
65
|
const currentState = await readWorkflowState(workflowPath);
|
|
78
|
-
const handoffExists = await pathExists(handoffPath);
|
|
79
|
-
|
|
80
|
-
if (handoffExists) {
|
|
81
|
-
await rm(handoffPath);
|
|
82
|
-
}
|
|
83
66
|
|
|
84
67
|
const nextState = transitionWorkflow({
|
|
85
68
|
state: currentState,
|
|
@@ -89,24 +72,10 @@ export const prepareBuilderRun = async ({
|
|
|
89
72
|
await writeWorkflowState({
|
|
90
73
|
path: workflowPath,
|
|
91
74
|
state: nextState,
|
|
92
|
-
currentRevision: currentState.revision,
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
const expectedPaths = [workflowPath];
|
|
96
|
-
|
|
97
|
-
if (handoffExists) {
|
|
98
|
-
expectedPaths.push(handoffPath);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const checkpointCommit = await createWorkflowCheckpointCommit({
|
|
102
|
-
repositoryRoot: paths.getRepositoryRoot(),
|
|
103
|
-
expectedPaths,
|
|
104
75
|
});
|
|
105
76
|
|
|
106
77
|
return {
|
|
107
78
|
specId,
|
|
108
|
-
|
|
109
|
-
repositoryRoot: paths.getRepositoryRoot(),
|
|
110
|
-
checkpointCommit,
|
|
79
|
+
projectRoot: paths.getProjectRoot(),
|
|
111
80
|
};
|
|
112
81
|
};
|
|
@@ -10,7 +10,6 @@ import type {
|
|
|
10
10
|
NewEscalation,
|
|
11
11
|
} from '#artifacts/escalation/schema.ts';
|
|
12
12
|
import type { MaestroPaths } from '#MaestroPaths.ts';
|
|
13
|
-
import { pathExists } from '#utils/path-exists.ts';
|
|
14
13
|
import { readWorkflowState } from '#workflow/state/readWorkflowState.ts';
|
|
15
14
|
import {
|
|
16
15
|
WORKFLOW_EVENTS,
|
|
@@ -23,7 +22,7 @@ import { transitionWorkflow } from '#workflow/transitions.ts';
|
|
|
23
22
|
export type OpenedBuilderEscalation = {
|
|
24
23
|
escalation: Escalation;
|
|
25
24
|
state: WorkflowState;
|
|
26
|
-
|
|
25
|
+
projectRoot: string;
|
|
27
26
|
workflowPath: string;
|
|
28
27
|
escalationPath: string;
|
|
29
28
|
};
|
|
@@ -40,7 +39,6 @@ export const openBuilderEscalation = async ({
|
|
|
40
39
|
escalation,
|
|
41
40
|
}: OpenBuilderEscalationInput): Promise<OpenedBuilderEscalation> => {
|
|
42
41
|
const workflowPath = paths.getWorkflowPath(specId);
|
|
43
|
-
const handoffPath = paths.getBuilderHandoffPath(specId);
|
|
44
42
|
const escalationsPath = paths.getEscalationsPath(specId);
|
|
45
43
|
const currentState = await readWorkflowState(workflowPath);
|
|
46
44
|
|
|
@@ -56,10 +54,6 @@ export const openBuilderEscalation = async ({
|
|
|
56
54
|
);
|
|
57
55
|
}
|
|
58
56
|
|
|
59
|
-
if (await pathExists(handoffPath)) {
|
|
60
|
-
throw new Error('Builder terminal handoff already exists.');
|
|
61
|
-
}
|
|
62
|
-
|
|
63
57
|
const nextState = transitionWorkflow({
|
|
64
58
|
state: currentState,
|
|
65
59
|
event: WORKFLOW_EVENTS.OPEN_ESCALATION,
|
|
@@ -70,20 +64,18 @@ export const openBuilderEscalation = async ({
|
|
|
70
64
|
const created = await createEscalation({
|
|
71
65
|
directory: escalationsPath,
|
|
72
66
|
specId: currentState.specId,
|
|
73
|
-
revision: nextState.revision,
|
|
74
67
|
escalation,
|
|
75
68
|
});
|
|
76
69
|
|
|
77
70
|
await writeWorkflowState({
|
|
78
71
|
path: workflowPath,
|
|
79
72
|
state: nextState,
|
|
80
|
-
currentRevision: currentState.revision,
|
|
81
73
|
});
|
|
82
74
|
|
|
83
75
|
return {
|
|
84
76
|
escalation: created.escalation,
|
|
85
77
|
state: nextState,
|
|
86
|
-
|
|
78
|
+
projectRoot: paths.getProjectRoot(),
|
|
87
79
|
workflowPath,
|
|
88
80
|
escalationPath: created.path,
|
|
89
81
|
};
|