@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 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
 
@@ -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 checks product restoration, writes the handoff, changes the phase, and commits only its two protocol files.
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 never changes product code, it independently regenerates every probe and breakage from the candidate.
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. The spec and its prototypes are immutable during a builder or verifier pass.
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 changes the phase to `ready-for-builder`.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@emiliosp/pi-maestro",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "A spec-driven multiagent development workflow for Pi.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -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. Use existing facts before asking the owner for missing information.
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 the shared in-memory activation state for one Maestro session.
3
- * Used: By the main and subagent extensions in the foreground Pi runtime.
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 or a main tool result, and before an owner turn.
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 maestroSessionState from '#maestro/session/MaestroSessionState.ts';
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 ProductChangesResult = {
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
- if (result.outcome === WORKFLOW_PHASES.FINDINGS_DECISION) {
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
- try {
243
- const request: SubagentDelegationRequest = {
244
- requestId: randomUUID(),
245
- ownerRunId: toolCallId,
246
- nodeId: WORKFLOW_ROLES.VERIFIER,
247
- // pi-subagents loads this name from agents/verifier.md through package.json.
248
- agent: AGENTS.VERIFIER,
249
- task: `Verify specId "${specId}" at candidate checkpoint "${run.candidateCommit}" in the current checkout "${run.repositoryRoot}". Read all applicable AGENTS.md files before working.`,
250
- context: 'fresh',
251
- cwd: run.repositoryRoot,
252
- model: config.verifier.model,
253
- thinking: config.verifier.thinking,
254
- timeoutMs: config.verifier.timeoutMinutes * MILLISECONDS_PER_MINUTE,
255
- result: { kind: 'text' },
256
- };
257
-
258
- const response = await waitForDelegationResponse({
259
- piEventsBus: pi.events,
260
- request,
261
- });
262
-
263
- assertDelegationResponse(response);
264
- const result = await readVerifierResult({ paths, specId, run });
265
-
266
- return {
267
- content: [{ type: 'text', text: formatVerifierResult(result) }],
268
- details: result,
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);
@@ -103,8 +103,6 @@ export const prepareBuilderRun = async ({
103
103
  expectedPaths,
104
104
  });
105
105
 
106
- await maestroSessionState.setSpecSha256(paths.getSpecFilePath(specId));
107
-
108
106
  return {
109
107
  specId,
110
108
  revision: nextState.revision,
@@ -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
- if (
203
- await hasProductChanges({
204
- repositoryRoot,
205
- candidateCommit: await getHeadCommit(repositoryRoot),
206
- workflowPath,
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 without allowing candidate changes.
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
- };