@emiliosp/pi-maestro 0.5.2 → 0.6.1
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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Objective: Resolve the current builder escalation and
|
|
2
|
+
* Objective: Resolve the current builder escalation and save the decision.
|
|
3
3
|
* Used: When the owner keeps the approved contract and makes a decision.
|
|
4
4
|
*/
|
|
5
5
|
|
|
@@ -9,7 +9,6 @@ import type {
|
|
|
9
9
|
Escalation,
|
|
10
10
|
EscalationResolution,
|
|
11
11
|
} from '#artifacts/escalation/schema.ts';
|
|
12
|
-
import { createWorkflowCheckpointCommit } from '#git/commits/createWorkflowCheckpointCommit.ts';
|
|
13
12
|
import type { MaestroPaths } from '#MaestroPaths.ts';
|
|
14
13
|
import { readWorkflowState } from '#workflow/state/readWorkflowState.ts';
|
|
15
14
|
import {
|
|
@@ -23,8 +22,7 @@ import { transitionWorkflow } from '#workflow/transitions.ts';
|
|
|
23
22
|
export type ResolvedBuilderEscalation = {
|
|
24
23
|
escalation: Escalation;
|
|
25
24
|
state: WorkflowState;
|
|
26
|
-
|
|
27
|
-
checkpointCommit: string;
|
|
25
|
+
projectRoot: string;
|
|
28
26
|
};
|
|
29
27
|
|
|
30
28
|
type AssertCurrentEscalationInput<CurrentEscalation = Escalation | undefined> =
|
|
@@ -70,6 +68,12 @@ export const resolveBuilderEscalation = async ({
|
|
|
70
68
|
const escalationsPath = paths.getEscalationsPath(specId);
|
|
71
69
|
const currentState = await readWorkflowState(workflowPath);
|
|
72
70
|
|
|
71
|
+
if (currentState.specId !== specId) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
`Workflow spec ID mismatch: expected "${specId}", found "${currentState.specId}".`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
73
77
|
if (currentState.phase !== WORKFLOW_PHASES.ESCALATION_DECISION) {
|
|
74
78
|
throw new Error(
|
|
75
79
|
`Escalation resolution requires escalation-decision state, found "${currentState.phase}".`,
|
|
@@ -79,7 +83,6 @@ export const resolveBuilderEscalation = async ({
|
|
|
79
83
|
const history = await readEscalationHistory({
|
|
80
84
|
directory: escalationsPath,
|
|
81
85
|
specId,
|
|
82
|
-
currentRevision: currentState.revision,
|
|
83
86
|
});
|
|
84
87
|
|
|
85
88
|
const escalationInput = { currentEscalation: history.at(-1), escalationId };
|
|
@@ -99,25 +102,17 @@ export const resolveBuilderEscalation = async ({
|
|
|
99
102
|
const resolvedEscalation = await resolveEscalation({
|
|
100
103
|
path: escalationPath,
|
|
101
104
|
specId,
|
|
102
|
-
revision: nextState.revision,
|
|
103
105
|
resolution,
|
|
104
106
|
});
|
|
105
107
|
|
|
106
108
|
await writeWorkflowState({
|
|
107
109
|
path: workflowPath,
|
|
108
110
|
state: nextState,
|
|
109
|
-
currentRevision: currentState.revision,
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
const checkpointCommit = await createWorkflowCheckpointCommit({
|
|
113
|
-
repositoryRoot: paths.getRepositoryRoot(),
|
|
114
|
-
expectedPaths: [workflowPath, escalationPath],
|
|
115
111
|
});
|
|
116
112
|
|
|
117
113
|
return {
|
|
118
114
|
escalation: resolvedEscalation,
|
|
119
115
|
state: nextState,
|
|
120
|
-
|
|
121
|
-
checkpointCommit,
|
|
116
|
+
projectRoot: paths.getProjectRoot(),
|
|
122
117
|
};
|
|
123
118
|
};
|
|
@@ -1,21 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Objective: Record owner decisions for current verifier findings on the
|
|
2
|
+
* Objective: Record owner decisions for current verifier findings on the active handoff.
|
|
3
3
|
* Used: When the owner resolves a findings-decision handoff.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { relative } from 'node:path';
|
|
7
6
|
import { assertVerifierHandoff } from '#artifacts/verifier-handoff/assertVerifierHandoff.ts';
|
|
8
7
|
import { readVerifierHandoff } from '#artifacts/verifier-handoff/readVerifierHandoff.ts';
|
|
9
8
|
import type {
|
|
10
9
|
VerifierFinding,
|
|
11
10
|
VerifierHandoff,
|
|
12
11
|
} from '#artifacts/verifier-handoff/schema.ts';
|
|
13
|
-
import {
|
|
14
|
-
import { createWorkflowCheckpointCommit } from '#git/commits/createWorkflowCheckpointCommit.ts';
|
|
15
|
-
import { getHeadCommit } from '#git/repository/getHeadCommit.ts';
|
|
16
|
-
import { getRepositoryStatus } from '#git/repository/getRepositoryStatus.ts';
|
|
12
|
+
import { FINDING_DECISIONS } from '#artifacts/verifier-handoff/schema.ts';
|
|
17
13
|
import type { MaestroPaths } from '#MaestroPaths.ts';
|
|
18
|
-
import {
|
|
14
|
+
import { writeJson } from '#utils/write-json.ts';
|
|
19
15
|
import { readWorkflowState } from '#workflow/state/readWorkflowState.ts';
|
|
20
16
|
import {
|
|
21
17
|
WORKFLOW_EVENTS,
|
|
@@ -25,11 +21,6 @@ import {
|
|
|
25
21
|
import { writeWorkflowState } from '#workflow/state/writeWorkflowState.ts';
|
|
26
22
|
import { transitionWorkflow } from '#workflow/transitions.ts';
|
|
27
23
|
|
|
28
|
-
export const FINDING_DECISIONS = {
|
|
29
|
-
REJECT: 'reject',
|
|
30
|
-
FIX_CODE: 'fix-code',
|
|
31
|
-
} as const;
|
|
32
|
-
|
|
33
24
|
type FindingDecisionRejected = {
|
|
34
25
|
findingId: string;
|
|
35
26
|
decision: typeof FINDING_DECISIONS.REJECT;
|
|
@@ -46,8 +37,7 @@ export type FindingDecision = FindingDecisionApproved | FindingDecisionRejected;
|
|
|
46
37
|
export type ResolvedFindings = {
|
|
47
38
|
findings: VerifierFinding[];
|
|
48
39
|
state: WorkflowState;
|
|
49
|
-
|
|
50
|
-
checkpointCommit: string;
|
|
40
|
+
projectRoot: string;
|
|
51
41
|
};
|
|
52
42
|
|
|
53
43
|
type AssertDecisionsInput = {
|
|
@@ -118,9 +108,8 @@ const readFindingWorkflow = async ({
|
|
|
118
108
|
}
|
|
119
109
|
|
|
120
110
|
const handoff = await readVerifierHandoff({
|
|
121
|
-
path: paths.
|
|
111
|
+
path: await paths.getActiveVerifierHandoffPath(specId),
|
|
122
112
|
specId,
|
|
123
|
-
revision: state.revision,
|
|
124
113
|
});
|
|
125
114
|
|
|
126
115
|
if (handoff.findings.length === 0) {
|
|
@@ -129,8 +118,10 @@ const readFindingWorkflow = async ({
|
|
|
129
118
|
);
|
|
130
119
|
}
|
|
131
120
|
|
|
132
|
-
if (handoff.findings.some(({
|
|
133
|
-
throw new Error(
|
|
121
|
+
if (handoff.findings.some(({ decision }) => decision !== null)) {
|
|
122
|
+
throw new Error(
|
|
123
|
+
'Current verifier findings already contain an owner decision.',
|
|
124
|
+
);
|
|
134
125
|
}
|
|
135
126
|
|
|
136
127
|
return { state, handoff };
|
|
@@ -155,11 +146,17 @@ const buildFindingWorkflow = ({
|
|
|
155
146
|
const findings = handoff.findings.map((finding) => {
|
|
156
147
|
const decision = decisionsById.get(finding.id);
|
|
157
148
|
|
|
158
|
-
if (decision?.decision
|
|
159
|
-
return
|
|
149
|
+
if (decision?.decision === FINDING_DECISIONS.REJECT) {
|
|
150
|
+
return {
|
|
151
|
+
...finding,
|
|
152
|
+
decision: {
|
|
153
|
+
decision: FINDING_DECISIONS.REJECT,
|
|
154
|
+
reason: decision.reason.trim(),
|
|
155
|
+
},
|
|
156
|
+
};
|
|
160
157
|
}
|
|
161
158
|
|
|
162
|
-
return { ...finding,
|
|
159
|
+
return { ...finding, decision: { decision: FINDING_DECISIONS.FIX_CODE } };
|
|
163
160
|
});
|
|
164
161
|
|
|
165
162
|
const isFixCodeRequested = decisions.some(
|
|
@@ -173,121 +170,15 @@ const buildFindingWorkflow = ({
|
|
|
173
170
|
: WORKFLOW_EVENTS.REJECT_FINDINGS,
|
|
174
171
|
});
|
|
175
172
|
|
|
176
|
-
const nextHandoff = { ...handoff,
|
|
173
|
+
const nextHandoff = { ...handoff, findings };
|
|
177
174
|
assertVerifierHandoff({
|
|
178
175
|
handoff: nextHandoff,
|
|
179
176
|
specId: state.specId,
|
|
180
|
-
revision: nextState.revision,
|
|
181
177
|
});
|
|
182
178
|
|
|
183
179
|
return { state: nextState, handoff: nextHandoff };
|
|
184
180
|
};
|
|
185
181
|
|
|
186
|
-
type AssertNoChangesOutsideFindingArtifactsInput = {
|
|
187
|
-
repositoryRoot: string;
|
|
188
|
-
workflowPath: string;
|
|
189
|
-
handoffPath: string;
|
|
190
|
-
};
|
|
191
|
-
|
|
192
|
-
async function assertNoChangesOutsideFindingArtifacts({
|
|
193
|
-
repositoryRoot,
|
|
194
|
-
workflowPath,
|
|
195
|
-
handoffPath,
|
|
196
|
-
}: AssertNoChangesOutsideFindingArtifactsInput): Promise<void> {
|
|
197
|
-
const headCommit = await getHeadCommit(repositoryRoot);
|
|
198
|
-
|
|
199
|
-
// Compare both the checkout and index with HEAD. The index comparison also
|
|
200
|
-
// catches staged changes when the working file was restored on disk.
|
|
201
|
-
const [diff, stagedDiff, status] = await Promise.all([
|
|
202
|
-
runGitCommand({
|
|
203
|
-
arguments: [
|
|
204
|
-
'diff',
|
|
205
|
-
'--no-renames',
|
|
206
|
-
'--name-only',
|
|
207
|
-
'-z',
|
|
208
|
-
headCommit,
|
|
209
|
-
'--',
|
|
210
|
-
],
|
|
211
|
-
cwd: repositoryRoot,
|
|
212
|
-
}),
|
|
213
|
-
runGitCommand({
|
|
214
|
-
arguments: [
|
|
215
|
-
'diff',
|
|
216
|
-
'--cached',
|
|
217
|
-
'--no-renames',
|
|
218
|
-
'--name-only',
|
|
219
|
-
'-z',
|
|
220
|
-
headCommit,
|
|
221
|
-
'--',
|
|
222
|
-
],
|
|
223
|
-
cwd: repositoryRoot,
|
|
224
|
-
}),
|
|
225
|
-
getRepositoryStatus(repositoryRoot),
|
|
226
|
-
]);
|
|
227
|
-
|
|
228
|
-
const allowedPaths = new Set([
|
|
229
|
-
relative(repositoryRoot, workflowPath),
|
|
230
|
-
relative(repositoryRoot, handoffPath),
|
|
231
|
-
]);
|
|
232
|
-
|
|
233
|
-
const changedTrackedPaths = [diff.stdout, stagedDiff.stdout]
|
|
234
|
-
.flatMap((output) => output.split('\0'))
|
|
235
|
-
.filter((path) => path.length > 0);
|
|
236
|
-
|
|
237
|
-
if (
|
|
238
|
-
changedTrackedPaths.some((path) => !allowedPaths.has(path)) ||
|
|
239
|
-
status.untracked.some((path) => !allowedPaths.has(path))
|
|
240
|
-
) {
|
|
241
|
-
throw new Error(
|
|
242
|
-
'Finding resolution requires no changes outside its protocol files.',
|
|
243
|
-
);
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
type CommitFindingWorkflowInput = {
|
|
248
|
-
paths: MaestroPaths;
|
|
249
|
-
specId: string;
|
|
250
|
-
currentRevision: number;
|
|
251
|
-
workflow: FindingWorkflow;
|
|
252
|
-
};
|
|
253
|
-
|
|
254
|
-
const commitFindingWorkflow = async ({
|
|
255
|
-
paths,
|
|
256
|
-
specId,
|
|
257
|
-
currentRevision,
|
|
258
|
-
workflow,
|
|
259
|
-
}: CommitFindingWorkflowInput): Promise<string> => {
|
|
260
|
-
const repositoryRoot = paths.getRepositoryRoot();
|
|
261
|
-
const workflowPath = paths.getWorkflowPath(specId);
|
|
262
|
-
const handoffPath = paths.getVerifierHandoffPath(specId);
|
|
263
|
-
|
|
264
|
-
await assertNoChangesOutsideFindingArtifacts({
|
|
265
|
-
repositoryRoot,
|
|
266
|
-
workflowPath,
|
|
267
|
-
handoffPath,
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
await writeJsonAtomically({ path: handoffPath, data: workflow.handoff });
|
|
271
|
-
await writeWorkflowState({
|
|
272
|
-
path: workflowPath,
|
|
273
|
-
state: workflow.state,
|
|
274
|
-
currentRevision,
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
const checkpointCommit = await createWorkflowCheckpointCommit({
|
|
278
|
-
repositoryRoot,
|
|
279
|
-
expectedPaths: [handoffPath, workflowPath],
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
if (!(await getRepositoryStatus(repositoryRoot)).clean) {
|
|
283
|
-
throw new Error(
|
|
284
|
-
'Finding resolution requires a clean checkout after its commit.',
|
|
285
|
-
);
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
return checkpointCommit;
|
|
289
|
-
};
|
|
290
|
-
|
|
291
182
|
type ResolveFindingsInput = {
|
|
292
183
|
paths: MaestroPaths;
|
|
293
184
|
specId: string;
|
|
@@ -306,17 +197,16 @@ export const resolveFindings = async ({
|
|
|
306
197
|
decisions,
|
|
307
198
|
});
|
|
308
199
|
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
workflow,
|
|
200
|
+
const handoffPath = await paths.getActiveVerifierHandoffPath(specId);
|
|
201
|
+
await writeJson({ path: handoffPath, data: workflow.handoff });
|
|
202
|
+
await writeWorkflowState({
|
|
203
|
+
path: paths.getWorkflowPath(specId),
|
|
204
|
+
state: workflow.state,
|
|
314
205
|
});
|
|
315
206
|
|
|
316
207
|
return {
|
|
317
208
|
findings: workflow.handoff.findings,
|
|
318
209
|
state: workflow.state,
|
|
319
|
-
|
|
320
|
-
checkpointCommit,
|
|
210
|
+
projectRoot: paths.getProjectRoot(),
|
|
321
211
|
};
|
|
322
212
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Objective: Read and validate a workflow state file.
|
|
3
|
-
* Used: When Maestro loads a workflow phase
|
|
3
|
+
* Used: When Maestro loads a workflow phase.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { readJsonFile } from '#utils/read-json.ts';
|
|
@@ -15,5 +15,5 @@ export const readWorkflowState = async (
|
|
|
15
15
|
const state = await readJsonFile({ path, description: 'Workflow state' });
|
|
16
16
|
assertWorkflowState(state);
|
|
17
17
|
|
|
18
|
-
return state;
|
|
18
|
+
return { version: state.version, specId: state.specId, phase: state.phase };
|
|
19
19
|
};
|
|
@@ -51,7 +51,6 @@ export const WorkflowStateSchema = Type.Object(
|
|
|
51
51
|
{
|
|
52
52
|
version: Type.Literal(WORKFLOW_STATE_VERSION),
|
|
53
53
|
specId: Type.String({ pattern: SPEC_ID_PATTERN.source }),
|
|
54
|
-
revision: Type.Integer({ minimum: 1 }),
|
|
55
54
|
phase: WorkflowPhaseSchema,
|
|
56
55
|
},
|
|
57
56
|
{ additionalProperties: false },
|
|
@@ -1,82 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Objective:
|
|
3
|
-
* Used: When Maestro persists a workflow transition.
|
|
2
|
+
* Objective: Write a validated workflow state.
|
|
3
|
+
* Used: When Maestro persists a sequential workflow transition.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
import { isErrnoException } from '#utils/is-errno-exception.ts';
|
|
8
|
-
import { pathExists } from '#utils/path-exists.ts';
|
|
9
|
-
import { writeJsonAtomically } from '#utils/write-json-atomically.ts';
|
|
10
|
-
import { readWorkflowState } from '#workflow/state/readWorkflowState.ts';
|
|
6
|
+
import { writeJson } from '#utils/write-json.ts';
|
|
11
7
|
import {
|
|
12
8
|
assertWorkflowState,
|
|
13
9
|
type WorkflowState,
|
|
14
10
|
} from '#workflow/state/schema.ts';
|
|
15
11
|
|
|
16
|
-
const acquireWorkflowLock = async (path: string): Promise<FileHandle> => {
|
|
17
|
-
try {
|
|
18
|
-
return await open(path, 'wx', 0o600);
|
|
19
|
-
} catch (error) {
|
|
20
|
-
if (isErrnoException(error) && error.code === 'EEXIST') {
|
|
21
|
-
throw new Error('Another workflow state update is in progress.', {
|
|
22
|
-
cause: error,
|
|
23
|
-
});
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
throw error;
|
|
27
|
-
}
|
|
28
|
-
};
|
|
29
|
-
|
|
30
12
|
type WriteWorkflowStateInput = {
|
|
31
13
|
path: string;
|
|
32
14
|
state: WorkflowState;
|
|
33
|
-
currentRevision: number;
|
|
34
15
|
};
|
|
35
16
|
|
|
36
17
|
export const writeWorkflowState = async ({
|
|
37
18
|
path,
|
|
38
19
|
state,
|
|
39
|
-
currentRevision,
|
|
40
20
|
}: WriteWorkflowStateInput): Promise<void> => {
|
|
41
21
|
assertWorkflowState(state);
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
if (state.revision <= currentRevision) {
|
|
50
|
-
throw new Error(
|
|
51
|
-
`Workflow revision must be higher than expected revision ${currentRevision}.`,
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const lockPath = `${path}.lock`;
|
|
56
|
-
const lock = await acquireWorkflowLock(lockPath);
|
|
57
|
-
|
|
58
|
-
try {
|
|
59
|
-
const exists = await pathExists(path);
|
|
60
|
-
|
|
61
|
-
if (!exists && currentRevision !== 0) {
|
|
62
|
-
throw new Error(
|
|
63
|
-
`Stale workflow revision: expected ${currentRevision}, but no state exists.`,
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (exists) {
|
|
68
|
-
const current = await readWorkflowState(path);
|
|
69
|
-
|
|
70
|
-
if (current.revision !== currentRevision) {
|
|
71
|
-
throw new Error(
|
|
72
|
-
`Stale workflow revision: expected ${currentRevision}, found ${current.revision}.`,
|
|
73
|
-
);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
await writeJsonAtomically({ path, data: state });
|
|
78
|
-
} finally {
|
|
79
|
-
await lock.close();
|
|
80
|
-
await rm(lockPath, { force: true });
|
|
81
|
-
}
|
|
22
|
+
await writeJson({
|
|
23
|
+
path,
|
|
24
|
+
data: { version: state.version, specId: state.specId, phase: state.phase },
|
|
25
|
+
});
|
|
82
26
|
};
|
|
@@ -4,12 +4,10 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { mkdir } from 'node:fs/promises';
|
|
7
|
-
import { dirname } from 'node:path';
|
|
8
7
|
import { assertVerifierHandoff } from '#artifacts/verifier-handoff/assertVerifierHandoff.ts';
|
|
9
8
|
import type { VerifierHandoff } from '#artifacts/verifier-handoff/schema.ts';
|
|
10
9
|
import { writeVerifierHandoff } from '#artifacts/verifier-handoff/writeVerifierHandoff.ts';
|
|
11
10
|
import type { MaestroPaths } from '#MaestroPaths.ts';
|
|
12
|
-
import { pathExists } from '#utils/path-exists.ts';
|
|
13
11
|
import { readWorkflowState } from '#workflow/state/readWorkflowState.ts';
|
|
14
12
|
import {
|
|
15
13
|
WORKFLOW_EVENTS,
|
|
@@ -22,7 +20,8 @@ import { transitionWorkflow } from '#workflow/transitions.ts';
|
|
|
22
20
|
export type CompletedVerifierPass = {
|
|
23
21
|
handoff: VerifierHandoff;
|
|
24
22
|
state: WorkflowState;
|
|
25
|
-
|
|
23
|
+
projectRoot: string;
|
|
24
|
+
handoffPath: string;
|
|
26
25
|
};
|
|
27
26
|
|
|
28
27
|
type CompleteVerifierPassInput = {
|
|
@@ -36,9 +35,8 @@ export const completeVerifierPass = async ({
|
|
|
36
35
|
specId,
|
|
37
36
|
handoff: draftHandoff,
|
|
38
37
|
}: CompleteVerifierPassInput): Promise<CompletedVerifierPass> => {
|
|
39
|
-
const
|
|
38
|
+
const projectRoot = paths.getProjectRoot();
|
|
40
39
|
const workflowPath = paths.getWorkflowPath(specId);
|
|
41
|
-
const handoffPath = paths.getVerifierHandoffPath(specId);
|
|
42
40
|
const currentState = await readWorkflowState(workflowPath);
|
|
43
41
|
|
|
44
42
|
if (currentState.specId !== specId) {
|
|
@@ -53,14 +51,9 @@ export const completeVerifierPass = async ({
|
|
|
53
51
|
);
|
|
54
52
|
}
|
|
55
53
|
|
|
56
|
-
if (await pathExists(handoffPath)) {
|
|
57
|
-
throw new Error('Verifier terminal handoff already exists.');
|
|
58
|
-
}
|
|
59
|
-
|
|
60
54
|
const handoffInput = {
|
|
61
55
|
handoff: draftHandoff,
|
|
62
56
|
specId,
|
|
63
|
-
revision: currentState.revision + 1,
|
|
64
57
|
};
|
|
65
58
|
|
|
66
59
|
assertVerifierHandoff(handoffInput);
|
|
@@ -74,22 +67,23 @@ export const completeVerifierPass = async ({
|
|
|
74
67
|
: WORKFLOW_EVENTS.VERIFIER_APPROVED,
|
|
75
68
|
});
|
|
76
69
|
|
|
77
|
-
await mkdir(
|
|
70
|
+
await mkdir(paths.getVerifierHandoffsPath(specId), { recursive: true });
|
|
71
|
+
const handoffPath = await paths.getNextVerifierHandoffPath(specId);
|
|
72
|
+
|
|
78
73
|
await writeVerifierHandoff({
|
|
79
74
|
path: handoffPath,
|
|
80
75
|
handoff,
|
|
81
76
|
specId,
|
|
82
|
-
revision: nextState.revision,
|
|
83
77
|
});
|
|
84
78
|
await writeWorkflowState({
|
|
85
79
|
path: workflowPath,
|
|
86
80
|
state: nextState,
|
|
87
|
-
currentRevision: currentState.revision,
|
|
88
81
|
});
|
|
89
82
|
|
|
90
83
|
return {
|
|
91
84
|
handoff,
|
|
85
|
+
handoffPath,
|
|
92
86
|
state: nextState,
|
|
93
|
-
|
|
87
|
+
projectRoot,
|
|
94
88
|
};
|
|
95
89
|
};
|
|
@@ -1,13 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Objective: Prepare a
|
|
2
|
+
* Objective: Prepare a verifier run transition in the project directory.
|
|
3
3
|
* Used: When Maestro starts a verifier 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
|
-
import { pathExists } from '#utils/path-exists.ts';
|
|
11
7
|
import { readWorkflowState } from '#workflow/state/readWorkflowState.ts';
|
|
12
8
|
import { WORKFLOW_EVENTS, WORKFLOW_PHASES } from '#workflow/state/schema.ts';
|
|
13
9
|
import { writeWorkflowState } from '#workflow/state/writeWorkflowState.ts';
|
|
@@ -15,10 +11,7 @@ import { transitionWorkflow } from '#workflow/transitions.ts';
|
|
|
15
11
|
|
|
16
12
|
export type VerifierRun = {
|
|
17
13
|
specId: string;
|
|
18
|
-
|
|
19
|
-
candidateCommit: string;
|
|
20
|
-
checkpointCommit: string;
|
|
21
|
-
revision: number;
|
|
14
|
+
projectRoot: string;
|
|
22
15
|
};
|
|
23
16
|
|
|
24
17
|
type PrepareVerifierRunInput = {
|
|
@@ -30,9 +23,8 @@ export const prepareVerifierRun = async ({
|
|
|
30
23
|
paths,
|
|
31
24
|
specId,
|
|
32
25
|
}: PrepareVerifierRunInput): Promise<VerifierRun> => {
|
|
33
|
-
const
|
|
26
|
+
const projectRoot = paths.getProjectRoot();
|
|
34
27
|
const workflowPath = paths.getWorkflowPath(specId);
|
|
35
|
-
const handoffPath = paths.getVerifierHandoffPath(specId);
|
|
36
28
|
const currentState = await readWorkflowState(workflowPath);
|
|
37
29
|
|
|
38
30
|
if (currentState.specId !== specId) {
|
|
@@ -47,47 +39,18 @@ export const prepareVerifierRun = async ({
|
|
|
47
39
|
);
|
|
48
40
|
}
|
|
49
41
|
|
|
50
|
-
const status = await getRepositoryStatus(repositoryRoot);
|
|
51
|
-
|
|
52
|
-
if (!status.clean) {
|
|
53
|
-
throw new Error(
|
|
54
|
-
'Verifier run requires a committed ready-for-verifier candidate.',
|
|
55
|
-
);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
42
|
const nextState = transitionWorkflow({
|
|
59
43
|
state: currentState,
|
|
60
44
|
event: WORKFLOW_EVENTS.RUN_VERIFIER,
|
|
61
45
|
});
|
|
62
46
|
|
|
63
|
-
const handoffExists = await pathExists(handoffPath);
|
|
64
|
-
|
|
65
|
-
if (handoffExists) {
|
|
66
|
-
await rm(handoffPath);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
47
|
await writeWorkflowState({
|
|
70
48
|
path: workflowPath,
|
|
71
49
|
state: nextState,
|
|
72
|
-
currentRevision: currentState.revision,
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
const expectedPaths = [workflowPath];
|
|
76
|
-
|
|
77
|
-
if (handoffExists) {
|
|
78
|
-
expectedPaths.push(handoffPath);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const checkpointCommit = await createWorkflowCheckpointCommit({
|
|
82
|
-
repositoryRoot,
|
|
83
|
-
expectedPaths,
|
|
84
50
|
});
|
|
85
51
|
|
|
86
52
|
return {
|
|
87
53
|
specId,
|
|
88
|
-
|
|
89
|
-
candidateCommit: checkpointCommit,
|
|
90
|
-
checkpointCommit,
|
|
91
|
-
revision: nextState.revision,
|
|
54
|
+
projectRoot,
|
|
92
55
|
};
|
|
93
56
|
};
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Objective: Record an owner rejection on one verifier finding.
|
|
3
|
-
* Used: When Maestro handles verifier handoff artifacts.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { assertVerifierHandoff } from '#artifacts/verifier-handoff/assertVerifierHandoff.ts';
|
|
7
|
-
import { readVerifierHandoff } from '#artifacts/verifier-handoff/readVerifierHandoff.ts';
|
|
8
|
-
import type { VerifierHandoff } from '#artifacts/verifier-handoff/schema.ts';
|
|
9
|
-
import { writeJsonAtomically } from '#utils/write-json-atomically.ts';
|
|
10
|
-
|
|
11
|
-
type RejectVerifierFindingInput = {
|
|
12
|
-
path: string;
|
|
13
|
-
specId: string;
|
|
14
|
-
revision: number;
|
|
15
|
-
findingId: string;
|
|
16
|
-
reason: string;
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
export const rejectVerifierFinding = async ({
|
|
20
|
-
path,
|
|
21
|
-
specId,
|
|
22
|
-
revision,
|
|
23
|
-
findingId,
|
|
24
|
-
reason,
|
|
25
|
-
}: RejectVerifierFindingInput): Promise<VerifierHandoff> => {
|
|
26
|
-
const handoff = await readVerifierHandoff({ path, specId, revision });
|
|
27
|
-
const finding = handoff.findings.find(({ id }) => id === findingId);
|
|
28
|
-
|
|
29
|
-
if (finding === undefined) {
|
|
30
|
-
throw new Error(`Verifier finding "${findingId}" does not exist.`);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
if (finding.rejection !== null) {
|
|
34
|
-
throw new Error(`Verifier finding "${findingId}" is already rejected.`);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
if (reason.length === 0) {
|
|
38
|
-
throw new Error('Verifier finding rejection reason must be non-empty.');
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const rejectedHandoff = {
|
|
42
|
-
...handoff,
|
|
43
|
-
findings: handoff.findings.map((currentFinding) =>
|
|
44
|
-
currentFinding.id === findingId
|
|
45
|
-
? { ...currentFinding, rejection: { reason } }
|
|
46
|
-
: currentFinding,
|
|
47
|
-
),
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
assertVerifierHandoff({ handoff: rejectedHandoff, specId, revision });
|
|
51
|
-
await writeJsonAtomically({ path, data: rejectedHandoff });
|
|
52
|
-
|
|
53
|
-
return rejectedHandoff;
|
|
54
|
-
};
|