@emiliosp/pi-maestro 0.2.0 → 0.4.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 +5 -3
- package/agents/builder.md +87 -10
- package/agents/verifier.md +91 -5
- package/docs/workflow.md +14 -4
- package/extensions/maestro-subagent.ts +1 -72
- package/package.json +1 -1
- package/src/maestro/instructions/getMaestroInstructions.ts +101 -11
- package/src/maestro/session/MaestroSessionState.ts +2 -29
- package/src/maestro/status/refreshMaestroStatus.ts +1 -1
- package/src/tools/child/record-verifier-handoff.ts +1 -37
- package/src/tools/main/run-builder.ts +2 -0
- package/src/tools/main/run-verifier.ts +32 -87
- package/src/workflow/builder/completeBuilderPass.ts +0 -3
- package/src/workflow/builder/prepareBuilderRun.ts +0 -2
- package/src/workflow/escalation/openBuilderEscalation.ts +0 -3
- package/src/workflow/findings/resolveFindings.ts +68 -13
- package/src/workflow/verifier/completeVerifierPass.ts +2 -35
- package/src/workflow/verifier/prepareVerifierRun.ts +0 -3
- package/src/tools/child/utils/isProtectedSpecPath.ts +0 -61
- package/src/utils/getFileSha256.ts +0 -13
- package/src/workflow/builder/assertBuilderProtocolUnchanged.ts +0 -33
- package/src/workflow/verifier/hasProductChanges.ts +0 -68
|
@@ -13,14 +13,9 @@ import {
|
|
|
13
13
|
import { createWorkflowCheckpointCommit } from '#git/commits/createWorkflowCheckpointCommit.ts';
|
|
14
14
|
import { getRepositoryStatus } from '#git/repository/getRepositoryStatus.ts';
|
|
15
15
|
import { SPEC_ID_PATTERN } from '#ids/isValidSpecId.ts';
|
|
16
|
-
import maestroSessionState from '#maestro/session/MaestroSessionState.ts';
|
|
17
16
|
import { resolveWorkflowContext } from '#tools/child/utils/resolveWorkflowContext.ts';
|
|
18
17
|
import { readWorkflowState } from '#workflow/state/readWorkflowState.ts';
|
|
19
|
-
import {
|
|
20
|
-
type CompletedVerifierPass,
|
|
21
|
-
completeVerifierPass,
|
|
22
|
-
type VerifierPassRejection,
|
|
23
|
-
} from '#workflow/verifier/completeVerifierPass.ts';
|
|
18
|
+
import { completeVerifierPass } from '#workflow/verifier/completeVerifierPass.ts';
|
|
24
19
|
|
|
25
20
|
export const VERIFIER_HANDOFF_TOOL = {
|
|
26
21
|
NAME: 'maestro_record_verifier_handoff',
|
|
@@ -48,20 +43,6 @@ const VerifierHandoffToolParameters = Type.Object(
|
|
|
48
43
|
{ additionalProperties: false },
|
|
49
44
|
);
|
|
50
45
|
|
|
51
|
-
type VerifierPassResult = CompletedVerifierPass | VerifierPassRejection;
|
|
52
|
-
|
|
53
|
-
const isVerifierPassRejection = (
|
|
54
|
-
result: VerifierPassResult,
|
|
55
|
-
): result is VerifierPassRejection => 'error' in result;
|
|
56
|
-
|
|
57
|
-
function assertVerifierCheckpointCommit(
|
|
58
|
-
commit: string | null,
|
|
59
|
-
): asserts commit is string {
|
|
60
|
-
if (commit === null) {
|
|
61
|
-
throw new Error('Verifier handoff requires a live verifier checkpoint.');
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
46
|
export const registerRecordVerifierHandoffTool = (pi: ExtensionAPI): void => {
|
|
66
47
|
pi.registerTool({
|
|
67
48
|
name: VERIFIER_HANDOFF_TOOL.NAME,
|
|
@@ -80,10 +61,6 @@ export const registerRecordVerifierHandoffTool = (pi: ExtensionAPI): void => {
|
|
|
80
61
|
paths.getWorkflowPath(specId),
|
|
81
62
|
);
|
|
82
63
|
|
|
83
|
-
const candidateCommit = maestroSessionState.getVerifierCheckpointCommit();
|
|
84
|
-
|
|
85
|
-
assertVerifierCheckpointCommit(candidateCommit);
|
|
86
|
-
|
|
87
64
|
const handoff = {
|
|
88
65
|
version: VERIFIER_HANDOFF_VERSION,
|
|
89
66
|
specId: currentState.specId,
|
|
@@ -94,21 +71,9 @@ export const registerRecordVerifierHandoffTool = (pi: ExtensionAPI): void => {
|
|
|
94
71
|
const completed = await completeVerifierPass({
|
|
95
72
|
paths,
|
|
96
73
|
specId,
|
|
97
|
-
candidateCommit,
|
|
98
74
|
handoff,
|
|
99
75
|
});
|
|
100
76
|
|
|
101
|
-
if (isVerifierPassRejection(completed)) {
|
|
102
|
-
return {
|
|
103
|
-
content: [{ type: 'text', text: completed.message }],
|
|
104
|
-
details: {
|
|
105
|
-
error: completed.error,
|
|
106
|
-
message: completed.message,
|
|
107
|
-
specId,
|
|
108
|
-
},
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
|
|
112
77
|
const workflowCheckpointCommit = await createWorkflowCheckpointCommit({
|
|
113
78
|
repositoryRoot,
|
|
114
79
|
expectedPaths: [
|
|
@@ -131,7 +96,6 @@ export const registerRecordVerifierHandoffTool = (pi: ExtensionAPI): void => {
|
|
|
131
96
|
},
|
|
132
97
|
],
|
|
133
98
|
details: {
|
|
134
|
-
candidateCommit,
|
|
135
99
|
phase: completed.state.phase,
|
|
136
100
|
workflowCheckpointCommit,
|
|
137
101
|
revision: completed.state.revision,
|
|
@@ -18,6 +18,7 @@ import { AGENTS } from '#config/schema.ts';
|
|
|
18
18
|
import { getRepositoryStatus } from '#git/repository/getRepositoryStatus.ts';
|
|
19
19
|
import { SPEC_ID_PATTERN } from '#ids/isValidSpecId.ts';
|
|
20
20
|
import type { MaestroPaths } from '#MaestroPaths.ts';
|
|
21
|
+
import { refreshMaestroStatus } from '#maestro/status/refreshMaestroStatus.ts';
|
|
21
22
|
import {
|
|
22
23
|
assertDelegationResponse,
|
|
23
24
|
waitForDelegationResponse,
|
|
@@ -232,6 +233,7 @@ export const registerRunBuilderTool = (pi: ExtensionAPI): void => {
|
|
|
232
233
|
const { paths, config } = await resolveToolRunContext(context.cwd);
|
|
233
234
|
|
|
234
235
|
const run = await prepareBuilderRun({ paths, specId });
|
|
236
|
+
await refreshMaestroStatus(context);
|
|
235
237
|
|
|
236
238
|
const request: SubagentDelegationRequest = {
|
|
237
239
|
requestId: randomUUID(),
|
|
@@ -13,7 +13,7 @@ import { AGENTS } from '#config/schema.ts';
|
|
|
13
13
|
import { getRepositoryStatus } from '#git/repository/getRepositoryStatus.ts';
|
|
14
14
|
import { SPEC_ID_PATTERN } from '#ids/isValidSpecId.ts';
|
|
15
15
|
import type { MaestroPaths } from '#MaestroPaths.ts';
|
|
16
|
-
import
|
|
16
|
+
import { refreshMaestroStatus } from '#maestro/status/refreshMaestroStatus.ts';
|
|
17
17
|
import {
|
|
18
18
|
assertDelegationResponse,
|
|
19
19
|
waitForDelegationResponse,
|
|
@@ -22,11 +22,6 @@ import { resolveToolRunContext } from '#tools/utils/resolveToolRunContext.ts';
|
|
|
22
22
|
import { WORKFLOW_ROLES } from '#workflow/roles.ts';
|
|
23
23
|
import { readWorkflowState } from '#workflow/state/readWorkflowState.ts';
|
|
24
24
|
import { WORKFLOW_PHASES, type WorkflowState } from '#workflow/state/schema.ts';
|
|
25
|
-
import {
|
|
26
|
-
VERIFIER_PASS_ERRORS,
|
|
27
|
-
VERIFIER_PASS_MESSAGES,
|
|
28
|
-
} from '#workflow/verifier/completeVerifierPass.ts';
|
|
29
|
-
import { hasProductChanges } from '#workflow/verifier/hasProductChanges.ts';
|
|
30
25
|
import {
|
|
31
26
|
prepareVerifierRun,
|
|
32
27
|
type VerifierRun,
|
|
@@ -68,28 +63,13 @@ type FindingsResult = {
|
|
|
68
63
|
handoff: VerifierHandoff;
|
|
69
64
|
};
|
|
70
65
|
|
|
71
|
-
type
|
|
72
|
-
outcome: typeof VERIFIER_PASS_ERRORS.PRODUCT_FILES_MODIFIED;
|
|
73
|
-
error: typeof VERIFIER_PASS_ERRORS.PRODUCT_FILES_MODIFIED;
|
|
74
|
-
message: typeof VERIFIER_PASS_MESSAGES.PRODUCT_FILES_MODIFIED;
|
|
75
|
-
specId: string;
|
|
76
|
-
revision: number;
|
|
77
|
-
phase: typeof WORKFLOW_PHASES.VERIFIER_RUNNING;
|
|
78
|
-
candidateCommit: string;
|
|
79
|
-
checkpointCommit: string;
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
type VerifierRunResult =
|
|
83
|
-
| CandidateReadyResult
|
|
84
|
-
| FindingsResult
|
|
85
|
-
| ProductChangesResult;
|
|
66
|
+
type VerifierRunResult = CandidateReadyResult | FindingsResult;
|
|
86
67
|
|
|
87
68
|
type ReadTerminalVerifierResultInput = {
|
|
88
69
|
paths: MaestroPaths;
|
|
89
70
|
specId: string;
|
|
90
71
|
run: VerifierRun;
|
|
91
72
|
state: WorkflowState;
|
|
92
|
-
productChanges: boolean;
|
|
93
73
|
};
|
|
94
74
|
|
|
95
75
|
const readTerminalVerifierResult = async ({
|
|
@@ -97,7 +77,6 @@ const readTerminalVerifierResult = async ({
|
|
|
97
77
|
specId,
|
|
98
78
|
run,
|
|
99
79
|
state,
|
|
100
|
-
productChanges,
|
|
101
80
|
}: ReadTerminalVerifierResultInput): Promise<
|
|
102
81
|
CandidateReadyResult | FindingsResult
|
|
103
82
|
> => {
|
|
@@ -110,12 +89,6 @@ const readTerminalVerifierResult = async ({
|
|
|
110
89
|
);
|
|
111
90
|
}
|
|
112
91
|
|
|
113
|
-
if (productChanges) {
|
|
114
|
-
throw new Error(
|
|
115
|
-
'Product files differ from the candidate after the verifier handoff.',
|
|
116
|
-
);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
92
|
const repositoryStatus = await getRepositoryStatus(run.repositoryRoot);
|
|
120
93
|
|
|
121
94
|
if (!repositoryStatus.clean) {
|
|
@@ -176,27 +149,7 @@ const readVerifierResult = async ({
|
|
|
176
149
|
);
|
|
177
150
|
}
|
|
178
151
|
|
|
179
|
-
const productChanges = await hasProductChanges({
|
|
180
|
-
repositoryRoot: run.repositoryRoot,
|
|
181
|
-
candidateCommit: run.candidateCommit,
|
|
182
|
-
workflowPath: paths.getWorkflowPath(specId),
|
|
183
|
-
handoffPath: paths.getVerifierHandoffPath(specId),
|
|
184
|
-
});
|
|
185
|
-
|
|
186
152
|
if (state.phase === WORKFLOW_PHASES.VERIFIER_RUNNING) {
|
|
187
|
-
if (productChanges) {
|
|
188
|
-
return {
|
|
189
|
-
outcome: VERIFIER_PASS_ERRORS.PRODUCT_FILES_MODIFIED,
|
|
190
|
-
error: VERIFIER_PASS_ERRORS.PRODUCT_FILES_MODIFIED,
|
|
191
|
-
message: VERIFIER_PASS_MESSAGES.PRODUCT_FILES_MODIFIED,
|
|
192
|
-
specId,
|
|
193
|
-
revision: state.revision,
|
|
194
|
-
phase: state.phase,
|
|
195
|
-
candidateCommit: run.candidateCommit,
|
|
196
|
-
checkpointCommit: run.checkpointCommit,
|
|
197
|
-
};
|
|
198
|
-
}
|
|
199
|
-
|
|
200
153
|
throw new Error(
|
|
201
154
|
'The verifier returned without recording a valid terminal handoff.',
|
|
202
155
|
);
|
|
@@ -207,7 +160,6 @@ const readVerifierResult = async ({
|
|
|
207
160
|
specId,
|
|
208
161
|
run,
|
|
209
162
|
state,
|
|
210
|
-
productChanges,
|
|
211
163
|
});
|
|
212
164
|
} catch (error) {
|
|
213
165
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -221,11 +173,7 @@ const formatVerifierResult = (result: VerifierRunResult): string => {
|
|
|
221
173
|
return `Verifier completed spec ${result.specId}. The workflow is candidate-ready.`;
|
|
222
174
|
}
|
|
223
175
|
|
|
224
|
-
|
|
225
|
-
return `Verifier found ${result.handoff.findings.length} finding(s) for spec ${result.specId}. The workflow is waiting for owner decisions.`;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
return `${result.message} The workflow remains verifier-running.`;
|
|
176
|
+
return `Verifier found ${result.handoff.findings.length} finding(s) for spec ${result.specId}. The workflow is waiting for owner decisions.`;
|
|
229
177
|
};
|
|
230
178
|
|
|
231
179
|
export const registerRunVerifierTool = (pi: ExtensionAPI): void => {
|
|
@@ -238,38 +186,35 @@ export const registerRunVerifierTool = (pi: ExtensionAPI): void => {
|
|
|
238
186
|
const { paths, config } = await resolveToolRunContext(context.cwd);
|
|
239
187
|
|
|
240
188
|
const run = await prepareVerifierRun({ paths, specId });
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
} finally {
|
|
271
|
-
maestroSessionState.clearVerifierCheckpointCommit();
|
|
272
|
-
}
|
|
189
|
+
await refreshMaestroStatus(context);
|
|
190
|
+
|
|
191
|
+
const request: SubagentDelegationRequest = {
|
|
192
|
+
requestId: randomUUID(),
|
|
193
|
+
ownerRunId: toolCallId,
|
|
194
|
+
nodeId: WORKFLOW_ROLES.VERIFIER,
|
|
195
|
+
// pi-subagents loads this name from agents/verifier.md through package.json.
|
|
196
|
+
agent: AGENTS.VERIFIER,
|
|
197
|
+
task: `Verify specId "${specId}" at candidate checkpoint "${run.candidateCommit}" in the current checkout "${run.repositoryRoot}". Read all applicable AGENTS.md files before working.`,
|
|
198
|
+
context: 'fresh',
|
|
199
|
+
cwd: run.repositoryRoot,
|
|
200
|
+
model: config.verifier.model,
|
|
201
|
+
thinking: config.verifier.thinking,
|
|
202
|
+
timeoutMs: config.verifier.timeoutMinutes * MILLISECONDS_PER_MINUTE,
|
|
203
|
+
result: { kind: 'text' },
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const response = await waitForDelegationResponse({
|
|
207
|
+
piEventsBus: pi.events,
|
|
208
|
+
request,
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
assertDelegationResponse(response);
|
|
212
|
+
const result = await readVerifierResult({ paths, specId, run });
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
content: [{ type: 'text', text: formatVerifierResult(result) }],
|
|
216
|
+
details: result,
|
|
217
|
+
};
|
|
273
218
|
},
|
|
274
219
|
});
|
|
275
220
|
};
|
|
@@ -15,7 +15,6 @@ import {
|
|
|
15
15
|
import { writeBuilderHandoff } from '#artifacts/builder-handoff/writeBuilderHandoff.ts';
|
|
16
16
|
import type { MaestroPaths } from '#MaestroPaths.ts';
|
|
17
17
|
import { pathExists } from '#utils/path-exists.ts';
|
|
18
|
-
import { assertBuilderProtocolUnchanged } from '#workflow/builder/assertBuilderProtocolUnchanged.ts';
|
|
19
18
|
import { readWorkflowState } from '#workflow/state/readWorkflowState.ts';
|
|
20
19
|
import {
|
|
21
20
|
WORKFLOW_EVENTS,
|
|
@@ -75,8 +74,6 @@ export const completeBuilderPass = async ({
|
|
|
75
74
|
specId,
|
|
76
75
|
handoff: draftHandoff,
|
|
77
76
|
}: CompleteBuilderPassInput): Promise<CompletedBuilderPass> => {
|
|
78
|
-
await assertBuilderProtocolUnchanged({ paths, specId });
|
|
79
|
-
|
|
80
77
|
const workflowPath = paths.getWorkflowPath(specId);
|
|
81
78
|
const handoffPath = paths.getBuilderHandoffPath(specId);
|
|
82
79
|
const currentState = await readWorkflowState(workflowPath);
|
|
@@ -11,7 +11,6 @@ import type {
|
|
|
11
11
|
} from '#artifacts/escalation/schema.ts';
|
|
12
12
|
import type { MaestroPaths } from '#MaestroPaths.ts';
|
|
13
13
|
import { pathExists } from '#utils/path-exists.ts';
|
|
14
|
-
import { assertBuilderProtocolUnchanged } from '#workflow/builder/assertBuilderProtocolUnchanged.ts';
|
|
15
14
|
import { readWorkflowState } from '#workflow/state/readWorkflowState.ts';
|
|
16
15
|
import {
|
|
17
16
|
WORKFLOW_EVENTS,
|
|
@@ -40,8 +39,6 @@ export const openBuilderEscalation = async ({
|
|
|
40
39
|
specId,
|
|
41
40
|
escalation,
|
|
42
41
|
}: OpenBuilderEscalationInput): Promise<OpenedBuilderEscalation> => {
|
|
43
|
-
await assertBuilderProtocolUnchanged({ paths, specId });
|
|
44
|
-
|
|
45
42
|
const workflowPath = paths.getWorkflowPath(specId);
|
|
46
43
|
const handoffPath = paths.getBuilderHandoffPath(specId);
|
|
47
44
|
const escalationsPath = paths.getEscalationsPath(specId);
|
|
@@ -3,12 +3,14 @@
|
|
|
3
3
|
* Used: When the owner resolves a findings-decision handoff.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { relative } from 'node:path';
|
|
6
7
|
import { assertVerifierHandoff } from '#artifacts/verifier-handoff/assertVerifierHandoff.ts';
|
|
7
8
|
import { readVerifierHandoff } from '#artifacts/verifier-handoff/readVerifierHandoff.ts';
|
|
8
9
|
import type {
|
|
9
10
|
VerifierFinding,
|
|
10
11
|
VerifierHandoff,
|
|
11
12
|
} from '#artifacts/verifier-handoff/schema.ts';
|
|
13
|
+
import { runGitCommand } from '#git/command.ts';
|
|
12
14
|
import { createWorkflowCheckpointCommit } from '#git/commits/createWorkflowCheckpointCommit.ts';
|
|
13
15
|
import { getHeadCommit } from '#git/repository/getHeadCommit.ts';
|
|
14
16
|
import { getRepositoryStatus } from '#git/repository/getRepositoryStatus.ts';
|
|
@@ -22,7 +24,6 @@ import {
|
|
|
22
24
|
} from '#workflow/state/schema.ts';
|
|
23
25
|
import { writeWorkflowState } from '#workflow/state/writeWorkflowState.ts';
|
|
24
26
|
import { transitionWorkflow } from '#workflow/transitions.ts';
|
|
25
|
-
import { hasProductChanges } from '#workflow/verifier/hasProductChanges.ts';
|
|
26
27
|
|
|
27
28
|
export const FINDING_DECISIONS = {
|
|
28
29
|
REJECT: 'reject',
|
|
@@ -182,6 +183,67 @@ const buildFindingWorkflow = ({
|
|
|
182
183
|
return { state: nextState, handoff: nextHandoff };
|
|
183
184
|
};
|
|
184
185
|
|
|
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
|
+
|
|
185
247
|
type CommitFindingWorkflowInput = {
|
|
186
248
|
paths: MaestroPaths;
|
|
187
249
|
specId: string;
|
|
@@ -199,18 +261,11 @@ const commitFindingWorkflow = async ({
|
|
|
199
261
|
const workflowPath = paths.getWorkflowPath(specId);
|
|
200
262
|
const handoffPath = paths.getVerifierHandoffPath(specId);
|
|
201
263
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
handoffPath,
|
|
208
|
-
})
|
|
209
|
-
) {
|
|
210
|
-
throw new Error(
|
|
211
|
-
'Finding resolution requires no changes outside its protocol files.',
|
|
212
|
-
);
|
|
213
|
-
}
|
|
264
|
+
await assertNoChangesOutsideFindingArtifacts({
|
|
265
|
+
repositoryRoot,
|
|
266
|
+
workflowPath,
|
|
267
|
+
handoffPath,
|
|
268
|
+
});
|
|
214
269
|
|
|
215
270
|
await writeJsonAtomically({ path: handoffPath, data: workflow.handoff });
|
|
216
271
|
await writeWorkflowState({
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Objective: Complete a verifier pass
|
|
2
|
+
* Objective: Complete a verifier pass with a validated handoff.
|
|
3
3
|
* Used: When the verifier submits its terminal handoff.
|
|
4
4
|
*/
|
|
5
5
|
|
|
@@ -18,16 +18,6 @@ import {
|
|
|
18
18
|
} from '#workflow/state/schema.ts';
|
|
19
19
|
import { writeWorkflowState } from '#workflow/state/writeWorkflowState.ts';
|
|
20
20
|
import { transitionWorkflow } from '#workflow/transitions.ts';
|
|
21
|
-
import { hasProductChanges } from '#workflow/verifier/hasProductChanges.ts';
|
|
22
|
-
|
|
23
|
-
export const VERIFIER_PASS_ERRORS = {
|
|
24
|
-
PRODUCT_FILES_MODIFIED: 'PRODUCT_FILES_MODIFIED',
|
|
25
|
-
} as const;
|
|
26
|
-
|
|
27
|
-
export const VERIFIER_PASS_MESSAGES = {
|
|
28
|
-
PRODUCT_FILES_MODIFIED:
|
|
29
|
-
'Product files differ from the candidate commit. Restore the candidate before submitting the verifier handoff.',
|
|
30
|
-
} as const;
|
|
31
21
|
|
|
32
22
|
export type CompletedVerifierPass = {
|
|
33
23
|
handoff: VerifierHandoff;
|
|
@@ -35,26 +25,17 @@ export type CompletedVerifierPass = {
|
|
|
35
25
|
repositoryRoot: string;
|
|
36
26
|
};
|
|
37
27
|
|
|
38
|
-
export type VerifierPassRejection = {
|
|
39
|
-
error: (typeof VERIFIER_PASS_ERRORS)[keyof typeof VERIFIER_PASS_ERRORS];
|
|
40
|
-
message: string;
|
|
41
|
-
};
|
|
42
|
-
|
|
43
28
|
type CompleteVerifierPassInput = {
|
|
44
29
|
paths: MaestroPaths;
|
|
45
30
|
specId: string;
|
|
46
|
-
candidateCommit: string;
|
|
47
31
|
handoff: unknown;
|
|
48
32
|
};
|
|
49
33
|
|
|
50
34
|
export const completeVerifierPass = async ({
|
|
51
35
|
paths,
|
|
52
36
|
specId,
|
|
53
|
-
candidateCommit,
|
|
54
37
|
handoff: draftHandoff,
|
|
55
|
-
}: CompleteVerifierPassInput): Promise<
|
|
56
|
-
CompletedVerifierPass | VerifierPassRejection
|
|
57
|
-
> => {
|
|
38
|
+
}: CompleteVerifierPassInput): Promise<CompletedVerifierPass> => {
|
|
58
39
|
const repositoryRoot = paths.getRepositoryRoot();
|
|
59
40
|
const workflowPath = paths.getWorkflowPath(specId);
|
|
60
41
|
const handoffPath = paths.getVerifierHandoffPath(specId);
|
|
@@ -76,20 +57,6 @@ export const completeVerifierPass = async ({
|
|
|
76
57
|
throw new Error('Verifier terminal handoff already exists.');
|
|
77
58
|
}
|
|
78
59
|
|
|
79
|
-
if (
|
|
80
|
-
await hasProductChanges({
|
|
81
|
-
repositoryRoot,
|
|
82
|
-
candidateCommit,
|
|
83
|
-
workflowPath,
|
|
84
|
-
handoffPath,
|
|
85
|
-
})
|
|
86
|
-
) {
|
|
87
|
-
return {
|
|
88
|
-
error: VERIFIER_PASS_ERRORS.PRODUCT_FILES_MODIFIED,
|
|
89
|
-
message: VERIFIER_PASS_MESSAGES.PRODUCT_FILES_MODIFIED,
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
|
|
93
60
|
const handoffInput = {
|
|
94
61
|
handoff: draftHandoff,
|
|
95
62
|
specId,
|
|
@@ -7,7 +7,6 @@ import { rm } from 'node:fs/promises';
|
|
|
7
7
|
import { createWorkflowCheckpointCommit } from '#git/commits/createWorkflowCheckpointCommit.ts';
|
|
8
8
|
import { getRepositoryStatus } from '#git/repository/getRepositoryStatus.ts';
|
|
9
9
|
import type { MaestroPaths } from '#MaestroPaths.ts';
|
|
10
|
-
import maestroSessionState from '#maestro/session/MaestroSessionState.ts';
|
|
11
10
|
import { pathExists } from '#utils/path-exists.ts';
|
|
12
11
|
import { readWorkflowState } from '#workflow/state/readWorkflowState.ts';
|
|
13
12
|
import { WORKFLOW_EVENTS, WORKFLOW_PHASES } from '#workflow/state/schema.ts';
|
|
@@ -84,8 +83,6 @@ export const prepareVerifierRun = async ({
|
|
|
84
83
|
expectedPaths,
|
|
85
84
|
});
|
|
86
85
|
|
|
87
|
-
maestroSessionState.setVerifierCheckpointCommit(checkpointCommit);
|
|
88
|
-
|
|
89
86
|
return {
|
|
90
87
|
specId,
|
|
91
88
|
repositoryRoot,
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Objective: Identify paths that resolve to the protected workflow spec.
|
|
3
|
-
* Used: Before child write and edit tools run.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { lstat, realpath } from 'node:fs/promises';
|
|
7
|
-
import { basename, dirname, resolve } from 'node:path';
|
|
8
|
-
import { isErrnoException } from '#utils/is-errno-exception.ts';
|
|
9
|
-
|
|
10
|
-
const resolveThroughExistingParent = async (path: string): Promise<string> => {
|
|
11
|
-
const unresolvedParts: string[] = [];
|
|
12
|
-
let currentPath = path;
|
|
13
|
-
|
|
14
|
-
while (true) {
|
|
15
|
-
try {
|
|
16
|
-
await lstat(currentPath);
|
|
17
|
-
|
|
18
|
-
const existingPath = await realpath(currentPath);
|
|
19
|
-
|
|
20
|
-
return resolve(existingPath, ...unresolvedParts);
|
|
21
|
-
} catch (error) {
|
|
22
|
-
if (!isErrnoException(error) || error.code !== 'ENOENT') {
|
|
23
|
-
throw error;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const parentPath = dirname(currentPath);
|
|
27
|
-
|
|
28
|
-
if (parentPath === currentPath) {
|
|
29
|
-
throw new Error(`Cannot resolve path "${path}".`);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
unresolvedParts.unshift(basename(currentPath));
|
|
33
|
-
currentPath = parentPath;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
type IsProtectedSpecPathInput = {
|
|
39
|
-
repositoryRoot: string;
|
|
40
|
-
specPath: string;
|
|
41
|
-
targetPath: string;
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
export const isProtectedSpecPath = async ({
|
|
45
|
-
repositoryRoot,
|
|
46
|
-
specPath,
|
|
47
|
-
targetPath,
|
|
48
|
-
}: IsProtectedSpecPathInput): Promise<boolean> => {
|
|
49
|
-
const pathWithoutPrefix = targetPath.startsWith('@')
|
|
50
|
-
? targetPath.slice(1)
|
|
51
|
-
: targetPath;
|
|
52
|
-
|
|
53
|
-
const resolvedTargetPath = resolve(repositoryRoot, pathWithoutPrefix);
|
|
54
|
-
|
|
55
|
-
const [resolvedSpecPath, resolvedTarget] = await Promise.all([
|
|
56
|
-
resolveThroughExistingParent(specPath),
|
|
57
|
-
resolveThroughExistingParent(resolvedTargetPath),
|
|
58
|
-
]);
|
|
59
|
-
|
|
60
|
-
return resolvedTarget === resolvedSpecPath;
|
|
61
|
-
};
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Objective: Calculate the SHA-256 digest of a file.
|
|
3
|
-
* Used: When a workflow needs to compare a file with an in-memory baseline.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { createHash } from 'node:crypto';
|
|
7
|
-
import { readFile } from 'node:fs/promises';
|
|
8
|
-
|
|
9
|
-
export const getFileSha256 = async (path: string): Promise<string> => {
|
|
10
|
-
const contents = await readFile(path);
|
|
11
|
-
|
|
12
|
-
return createHash('sha256').update(contents).digest('hex');
|
|
13
|
-
};
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Objective: Reject changes to spec.md after a builder session starts.
|
|
3
|
-
* Used: Before a builder tool writes a terminal workflow artifact.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { MaestroPaths } from '#MaestroPaths.ts';
|
|
7
|
-
import maestroSessionState from '#maestro/session/MaestroSessionState.ts';
|
|
8
|
-
import { getFileSha256 } from '#utils/getFileSha256.ts';
|
|
9
|
-
|
|
10
|
-
type AssertBuilderProtocolUnchangedInput = {
|
|
11
|
-
paths: MaestroPaths;
|
|
12
|
-
specId: string;
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
export async function assertBuilderProtocolUnchanged({
|
|
16
|
-
paths,
|
|
17
|
-
specId,
|
|
18
|
-
}: AssertBuilderProtocolUnchangedInput): Promise<void> {
|
|
19
|
-
const expectedSpecSha256 = maestroSessionState.getSpecSha256();
|
|
20
|
-
|
|
21
|
-
if (expectedSpecSha256 === null) {
|
|
22
|
-
throw new Error('Builder spec SHA-256 baseline is not initialized.');
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const specPath = paths.getSpecFilePath(specId);
|
|
26
|
-
const actualSpecSha256 = await getFileSha256(specPath);
|
|
27
|
-
|
|
28
|
-
if (actualSpecSha256 !== expectedSpecSha256) {
|
|
29
|
-
throw new Error(
|
|
30
|
-
`Builder changed spec.md after the run starts: "${specPath}".`,
|
|
31
|
-
);
|
|
32
|
-
}
|
|
33
|
-
}
|