@emiliosp/pi-maestro 0.3.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 +1 -1
- package/agents/verifier.md +1 -2
- package/docs/workflow.md +3 -3
- package/extensions/maestro-subagent.ts +1 -72
- package/package.json +1 -1
- package/src/maestro/instructions/getMaestroInstructions.ts +8 -1
- 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
package/README.md
CHANGED
|
@@ -77,7 +77,7 @@ Do not change product code yourself while the workflow runs. Maestro can run tem
|
|
|
77
77
|
|
|
78
78
|
The workflow is complete when it reaches `candidate-ready`. No final tool call is needed. Changes after completion are outside the Maestro review.
|
|
79
79
|
|
|
80
|
-
If an escalation or finding requires a contract change, edit and approve `spec.md`, then call `maestro_mark_spec_ready`. The workflow returns to `ready-for-builder` on the same branch. A technical builder failure stops the workflow and requires owner follow-up.
|
|
80
|
+
If an escalation or finding requires a contract change, edit and approve `spec.md`, then call `maestro_mark_spec_ready`. The workflow returns to `ready-for-builder` on the same branch. Commit the revised `spec.md`, its prototypes, and `workflow.json` before asking Maestro to run the builder again. The checkout must be clean. A technical builder failure stops the workflow and requires owner follow-up.
|
|
81
81
|
|
|
82
82
|
Builder and verifier runs stay in the foreground. Pi waits for each run before you continue the conversation. Maestro shows the current phase in Pi's status. Use pi-subagents FleetView or `/subagents-fleet` to inspect live activity and the transcript.
|
|
83
83
|
|
package/agents/verifier.md
CHANGED
|
@@ -106,8 +106,7 @@ All files outside the tool-owned `workflow.json` and `handoffs/verifier.json` mu
|
|
|
106
106
|
This includes the spec, prototypes, builder handoff, and escalation files. Findings do not relax this requirement.
|
|
107
107
|
|
|
108
108
|
Call `maestro_record_verifier_handoff` with `specId`, `summary`, every criterion result, `findings`, and `notes`.
|
|
109
|
-
The tool
|
|
110
|
-
If it returns `PRODUCT_FILES_MODIFIED`, inspect the remaining differences, restore your changes, and retry. The tool does not restore files.
|
|
109
|
+
The tool writes the handoff, changes the phase, and commits only its two protocol files.
|
|
111
110
|
If validation rejects the payload without writing it, correct the payload without changing the facts.
|
|
112
111
|
If the tool already wrote an artifact or changed phase before an error, do not resubmit or commit manually.
|
|
113
112
|
Do not delete artifacts, change workflow state, or bypass the tool to force completion.
|
package/docs/workflow.md
CHANGED
|
@@ -64,7 +64,7 @@ It reads the active spec and available artifacts. Historical artifacts provide c
|
|
|
64
64
|
|
|
65
65
|
Before the verifier starts, Maestro commits a `verifier-running` checkpoint. That checkpoint is the candidate commit.
|
|
66
66
|
|
|
67
|
-
The verifier
|
|
67
|
+
The verifier does not repair product code. It independently regenerates every probe and breakage from the candidate and restores all temporary changes before its handoff. Maestro relies on verifier instructions for restoration, not a comparison with the candidate commit.
|
|
68
68
|
|
|
69
69
|
## Main flow
|
|
70
70
|
|
|
@@ -146,7 +146,7 @@ For the initial spec:
|
|
|
146
146
|
|
|
147
147
|
During `drafting-spec`, Maestro can use any available tool to edit the active `spec.md` with the owner. Maestro can also create and update visual prototypes in that spec's `prototypes/` directory with any available tool. During `escalation-decision` or `findings-decision`, the same permission applies to owner-directed contract revisions of the spec and its prototypes. This permission does not apply to other workflow artifacts or product files. Maestro cannot edit the spec or its prototypes in other phases.
|
|
148
148
|
|
|
149
|
-
The committed `spec.md` represents the approved contract for the builder and verifier.
|
|
149
|
+
The committed `spec.md` represents the approved contract for the builder and verifier. Agents must not change the spec or its prototypes during a builder or verifier pass.
|
|
150
150
|
|
|
151
151
|
During spec preparation, Maestro can run tests and checks to understand the repository. This also applies when you request a spec revision in `escalation-decision` or `findings-decision`, but not in other phases.
|
|
152
152
|
|
|
@@ -217,7 +217,7 @@ A spec revision is allowed only from these blocked phases:
|
|
|
217
217
|
- `escalation-decision`
|
|
218
218
|
- `findings-decision`
|
|
219
219
|
|
|
220
|
-
The owner edits and approves the spec, then Maestro
|
|
220
|
+
The owner edits and approves the spec, then Maestro calls `maestro_mark_spec_ready` to change the phase to `ready-for-builder`. The owner must commit the revised `spec.md`, its prototypes, and `workflow.json` before Maestro starts the builder again. The checkout must be clean.
|
|
221
221
|
|
|
222
222
|
The previous escalation or finding becomes inactive. Its artifact remains in the branch as historical context. Builder and verifier decide whether historical artifacts apply to the current spec.
|
|
223
223
|
|
|
@@ -3,75 +3,10 @@
|
|
|
3
3
|
* Used: When Pi starts a Maestro subagent session.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import type {
|
|
7
|
-
ExtensionAPI,
|
|
8
|
-
ToolCallEvent,
|
|
9
|
-
ToolCallEventResult,
|
|
10
|
-
} from '@earendil-works/pi-coding-agent';
|
|
11
|
-
import { isToolCallEventType } from '@earendil-works/pi-coding-agent';
|
|
12
|
-
import maestroSessionState from '#maestro/session/MaestroSessionState.ts';
|
|
6
|
+
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
13
7
|
import { registerOpenEscalationTool } from '#tools/child/open-escalation.ts';
|
|
14
8
|
import { registerRecordBuilderHandoffTool } from '#tools/child/record-builder-handoff.ts';
|
|
15
9
|
import { registerRecordVerifierHandoffTool } from '#tools/child/record-verifier-handoff.ts';
|
|
16
|
-
import { isProtectedSpecPath } from '#tools/child/utils/isProtectedSpecPath.ts';
|
|
17
|
-
import { resolveWorkflowContext } from '#tools/child/utils/resolveWorkflowContext.ts';
|
|
18
|
-
|
|
19
|
-
const getWriteOrEditPath = (event: ToolCallEvent): string | undefined => {
|
|
20
|
-
if (isToolCallEventType('write', event)) {
|
|
21
|
-
return event.input.path;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
if (isToolCallEventType('edit', event)) {
|
|
25
|
-
return event.input.path;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
return undefined;
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
type ProtectSpecPathInput = {
|
|
32
|
-
cwd: string;
|
|
33
|
-
event: ToolCallEvent;
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
const protectSpecPath = async ({
|
|
37
|
-
cwd,
|
|
38
|
-
event,
|
|
39
|
-
}: ProtectSpecPathInput): Promise<ToolCallEventResult | undefined> => {
|
|
40
|
-
const targetPath = getWriteOrEditPath(event);
|
|
41
|
-
|
|
42
|
-
if (targetPath === undefined) {
|
|
43
|
-
return undefined;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const specId = maestroSessionState.getActiveSpecId();
|
|
47
|
-
|
|
48
|
-
if (specId === null) {
|
|
49
|
-
return undefined;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const { paths, repositoryRoot } = await resolveWorkflowContext({
|
|
53
|
-
cwd,
|
|
54
|
-
specId,
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
const specPath = paths.getSpecFilePath(specId);
|
|
58
|
-
|
|
59
|
-
if (
|
|
60
|
-
await isProtectedSpecPath({
|
|
61
|
-
repositoryRoot,
|
|
62
|
-
specPath,
|
|
63
|
-
targetPath,
|
|
64
|
-
})
|
|
65
|
-
) {
|
|
66
|
-
return {
|
|
67
|
-
block: true,
|
|
68
|
-
reason:
|
|
69
|
-
'The owner-approved spec.md cannot be changed directly during a subagent session.',
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return undefined;
|
|
74
|
-
};
|
|
75
10
|
|
|
76
11
|
export default (pi: ExtensionAPI): void => {
|
|
77
12
|
// pi-subagents selects active tools from each agent's tools allowlist after registration.
|
|
@@ -79,10 +14,4 @@ export default (pi: ExtensionAPI): void => {
|
|
|
79
14
|
registerOpenEscalationTool(pi);
|
|
80
15
|
registerRecordBuilderHandoffTool(pi);
|
|
81
16
|
registerRecordVerifierHandoffTool(pi);
|
|
82
|
-
|
|
83
|
-
// Pi fires this before a tool runs. A handler can block the call.
|
|
84
|
-
// Block direct child writes and edits to the owner-approved spec.md.
|
|
85
|
-
pi.on('tool_call', (event, context) =>
|
|
86
|
-
protectSpecPath({ cwd: context.cwd, event }),
|
|
87
|
-
);
|
|
88
17
|
};
|
package/package.json
CHANGED
|
@@ -26,7 +26,7 @@ Wait for each tool result before taking the next workflow action. Do not launch
|
|
|
26
26
|
### Prepare and approve the spec
|
|
27
27
|
|
|
28
28
|
1. Call maestro_create_spec with the change title. Use the generated spec.md structure from the package's templates/spec.md.
|
|
29
|
-
2. Read the repository and applicable AGENTS.md files.
|
|
29
|
+
2. Read the repository and applicable AGENTS.md files. Investigate the affected behavior before asking the owner for missing information.
|
|
30
30
|
3. Ask one focused question at a time. Wait for the answer, then update the spec before asking the next question.
|
|
31
31
|
4. Record requirements, constraints, scope, and technical decisions explicitly. Do not invent requirements or silently resolve owner decisions.
|
|
32
32
|
5. Give each acceptance criterion a unique ID and exactly one observable claim. Specify its probe, expected result, and safe temporary breakage.
|
|
@@ -35,6 +35,13 @@ Wait for each tool result before taking the next workflow action. Do not launch
|
|
|
35
35
|
8. Request explicit approval. Only after approval, call maestro_mark_spec_ready with the active specId.
|
|
36
36
|
9. Ask the owner to commit spec.md, its prototypes, and workflow.json. Do not start the builder until the checkout is clean.
|
|
37
37
|
|
|
38
|
+
During spec preparation and revisions, investigate each technical decision before presenting options or recommending an answer. Do not wait for the owner to request code analysis.
|
|
39
|
+
Trace the relevant code and data flow across affected components, including transformations that limit the available data.
|
|
40
|
+
Use repository evidence to explain each option's feasibility, required changes, scope, and effects on existing behavior.
|
|
41
|
+
Cite the relevant files. Distinguish confirmed facts from assumptions and state what you could not verify, including deployed state.
|
|
42
|
+
Do not ask the owner questions that repository inspection can answer. Keep requirement choices and technical decisions with the owner.
|
|
43
|
+
Record the supporting evidence and unresolved limits with the decision in spec.md. Follow the check and experiment permissions below.
|
|
44
|
+
|
|
38
45
|
### Spec edits, checks, and experiments
|
|
39
46
|
|
|
40
47
|
Edit the active spec.md and its prototypes/ directory only in drafting-spec or during owner-directed contract revisions in decision phases.
|
|
@@ -1,15 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Objective: Manage
|
|
3
|
-
* Used: By the main and
|
|
2
|
+
* Objective: Manage activation and spec selection for one Maestro owner session.
|
|
3
|
+
* Used: By the main extension and owner tools, never by child sessions.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { getFileSha256 } from '#utils/getFileSha256.ts';
|
|
7
|
-
|
|
8
6
|
class MaestroSessionState {
|
|
9
7
|
private active = false;
|
|
10
8
|
private activeSpecId: string | null = null;
|
|
11
|
-
private specSha256: string | null = null;
|
|
12
|
-
private verifierCheckpointCommit: string | null = null;
|
|
13
9
|
|
|
14
10
|
public isActive = (): boolean => this.active;
|
|
15
11
|
|
|
@@ -25,35 +21,12 @@ class MaestroSessionState {
|
|
|
25
21
|
this.activeSpecId = specId;
|
|
26
22
|
};
|
|
27
23
|
|
|
28
|
-
public getSpecSha256 = (): string | null => this.specSha256;
|
|
29
|
-
|
|
30
|
-
public setSpecSha256 = async (specPath: string): Promise<void> => {
|
|
31
|
-
if (this.activeSpecId === null) {
|
|
32
|
-
throw new Error('Cannot set spec SHA-256 without an active spec.');
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
this.specSha256 = await getFileSha256(specPath);
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
public getVerifierCheckpointCommit = (): string | null =>
|
|
39
|
-
this.verifierCheckpointCommit;
|
|
40
|
-
|
|
41
|
-
public setVerifierCheckpointCommit = (commit: string): void => {
|
|
42
|
-
this.verifierCheckpointCommit = commit;
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
public clearVerifierCheckpointCommit = (): void => {
|
|
46
|
-
this.verifierCheckpointCommit = null;
|
|
47
|
-
};
|
|
48
|
-
|
|
49
24
|
public activate = (): void => {
|
|
50
25
|
this.active = true;
|
|
51
26
|
};
|
|
52
27
|
|
|
53
28
|
public clearActiveSpecId = (): void => {
|
|
54
29
|
this.activeSpecId = null;
|
|
55
|
-
this.specSha256 = null;
|
|
56
|
-
this.clearVerifierCheckpointCommit();
|
|
57
30
|
};
|
|
58
31
|
|
|
59
32
|
public deactivate = (): void => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Objective: Refresh Pi status from the live Maestro session and its current workflow.
|
|
3
|
-
* Used: After activation
|
|
3
|
+
* Used: After activation, before child delegation, after main tool results, and before owner turns.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import type { ExtensionContext } from '@earendil-works/pi-coding-agent';
|
|
@@ -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
|
-
}
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Objective: Check whether product files differ from a commit.
|
|
3
|
-
* Used: Before verifier handoffs and owner finding resolutions.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { relative } from 'node:path';
|
|
7
|
-
import { runGitCommand } from '#git/command.ts';
|
|
8
|
-
import { getRepositoryStatus } from '#git/repository/getRepositoryStatus.ts';
|
|
9
|
-
|
|
10
|
-
type HasProductChangesInput = {
|
|
11
|
-
repositoryRoot: string;
|
|
12
|
-
candidateCommit: string;
|
|
13
|
-
workflowPath: string;
|
|
14
|
-
handoffPath: string;
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
export const hasProductChanges = async ({
|
|
18
|
-
repositoryRoot,
|
|
19
|
-
candidateCommit,
|
|
20
|
-
workflowPath,
|
|
21
|
-
handoffPath,
|
|
22
|
-
}: HasProductChangesInput): Promise<boolean> => {
|
|
23
|
-
/*
|
|
24
|
-
* Compare both the checkout and index with the candidate. The second diff
|
|
25
|
-
* catches a product file that the verifier staged and then restored only on
|
|
26
|
-
* disk.
|
|
27
|
-
*/
|
|
28
|
-
const [diff, stagedDiff, status] = await Promise.all([
|
|
29
|
-
runGitCommand({
|
|
30
|
-
arguments: [
|
|
31
|
-
'diff',
|
|
32
|
-
'--no-renames',
|
|
33
|
-
'--name-only',
|
|
34
|
-
'-z',
|
|
35
|
-
candidateCommit,
|
|
36
|
-
'--',
|
|
37
|
-
],
|
|
38
|
-
cwd: repositoryRoot,
|
|
39
|
-
}),
|
|
40
|
-
runGitCommand({
|
|
41
|
-
arguments: [
|
|
42
|
-
'diff',
|
|
43
|
-
'--cached',
|
|
44
|
-
'--no-renames',
|
|
45
|
-
'--name-only',
|
|
46
|
-
'-z',
|
|
47
|
-
candidateCommit,
|
|
48
|
-
'--',
|
|
49
|
-
],
|
|
50
|
-
cwd: repositoryRoot,
|
|
51
|
-
}),
|
|
52
|
-
getRepositoryStatus(repositoryRoot),
|
|
53
|
-
]);
|
|
54
|
-
|
|
55
|
-
const allowedPaths = new Set([
|
|
56
|
-
relative(repositoryRoot, workflowPath),
|
|
57
|
-
relative(repositoryRoot, handoffPath),
|
|
58
|
-
]);
|
|
59
|
-
|
|
60
|
-
const changedTrackedPaths = [diff.stdout, stagedDiff.stdout]
|
|
61
|
-
.flatMap((output) => output.split('\0'))
|
|
62
|
-
.filter((path) => path.length > 0);
|
|
63
|
-
|
|
64
|
-
return (
|
|
65
|
-
changedTrackedPaths.some((path) => !allowedPaths.has(path)) ||
|
|
66
|
-
status.untracked.some((path) => !allowedPaths.has(path))
|
|
67
|
-
);
|
|
68
|
-
};
|