@emiliosp/pi-maestro 0.5.2 → 0.6.0

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