@emiliosp/pi-maestro 0.5.1 → 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 -33
  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
@@ -1,8 +1,9 @@
1
1
  /**
2
- * Objective: Build validated paths for one Maestro workflow in the current checkout.
3
- * Used: Whenever Maestro reads or writes repository workflow artifacts.
2
+ * Objective: Build validated paths for one Maestro workflow in the current project.
3
+ * Used: Whenever Maestro reads or writes project workflow artifacts.
4
4
  */
5
5
 
6
+ import { readdir } from 'node:fs/promises';
6
7
  import { isAbsolute, relative, resolve } from 'node:path';
7
8
  import type { MaestroConfig } from '#config/schema.ts';
8
9
  import { isValidSpecId } from '#ids/isValidSpecId.ts';
@@ -10,12 +11,17 @@ import { isPathStrictlyWithin } from '#utils/path-strictly-within.ts';
10
11
 
11
12
  const PATHS = {
12
13
  SPEC_FILE: 'spec.md',
13
- WORKFLOW: 'workflow.json',
14
- HANDOFFS: 'handoffs',
15
- BUILDER_HANDOFF: 'handoffs/builder.json',
16
- VERIFIER_HANDOFF: 'handoffs/verifier.json',
17
- ESCALATIONS: 'handoffs/escalations',
18
- PROTOTYPES: 'prototypes',
14
+ WORKFLOW_FILE: 'workflow.json',
15
+ HANDOFFS_PATH: 'handoffs',
16
+ BUILDER_HANDOFFS_PATH: 'handoffs/builder',
17
+ VERIFIER_HANDOFFS_PATH: 'handoffs/verifier',
18
+ ESCALATIONS_PATH: 'handoffs/escalations',
19
+ PROTOTYPES_PATH: 'prototypes',
20
+ } as const;
21
+
22
+ const AGENTS_PREFIX = {
23
+ BUILDER: 'B',
24
+ VERIFIER: 'V',
19
25
  } as const;
20
26
 
21
27
  function assertSpecId(specId: string): void {
@@ -26,9 +32,9 @@ function assertSpecId(specId: string): void {
26
32
  }
27
33
  }
28
34
 
29
- function assertEscalationNumber(escalationNumber: number): void {
30
- if (!Number.isSafeInteger(escalationNumber) || escalationNumber < 1) {
31
- throw new Error('Escalation number must be a positive integer.');
35
+ function assertArtifactNumber(artifactNumber: number): void {
36
+ if (!Number.isSafeInteger(artifactNumber) || artifactNumber < 1) {
37
+ throw new Error('Artifact number must be a positive integer.');
32
38
  }
33
39
  }
34
40
 
@@ -45,7 +51,7 @@ function assertSafeRelativePrototypePath(value: string): void {
45
51
  }
46
52
 
47
53
  export type MaestroPathsInput = {
48
- repositoryRoot: string;
54
+ projectRoot: string;
49
55
  config: MaestroConfig;
50
56
  };
51
57
 
@@ -54,34 +60,39 @@ type EscalationPathInput = {
54
60
  escalationNumber: number;
55
61
  };
56
62
 
63
+ type HandoffPathInput = {
64
+ specId: string;
65
+ handoffPassNumber: number;
66
+ };
67
+
57
68
  type PrototypePathInput = {
58
69
  specId: string;
59
70
  relativePath: string;
60
71
  };
61
72
 
62
73
  export class MaestroPaths {
63
- private readonly repositoryRoot: string;
74
+ private readonly projectRoot: string;
64
75
  private readonly specDirectory: string;
65
76
 
66
77
  private readonly specDirectoryFromRoot: string;
67
78
 
68
- public constructor({ repositoryRoot, config }: MaestroPathsInput) {
79
+ public constructor({ projectRoot, config }: MaestroPathsInput) {
69
80
  if (
70
81
  !isPathStrictlyWithin({
71
- parent: repositoryRoot,
72
- candidate: config.specDirectory,
82
+ parent: projectRoot,
83
+ path: config.specDirectory,
73
84
  })
74
85
  ) {
75
- throw new Error('Generated Maestro path leaves the Git root.');
86
+ throw new Error('Generated Maestro path leaves the project root.');
76
87
  }
77
88
 
78
- this.repositoryRoot = repositoryRoot;
89
+ this.projectRoot = projectRoot;
79
90
  this.specDirectory = config.specDirectory;
80
- this.specDirectoryFromRoot = relative(repositoryRoot, config.specDirectory);
91
+ this.specDirectoryFromRoot = relative(projectRoot, config.specDirectory);
81
92
  }
82
93
 
83
- public getRepositoryRoot(): string {
84
- return this.repositoryRoot;
94
+ public getProjectRoot(): string {
95
+ return this.projectRoot;
85
96
  }
86
97
 
87
98
  public getSpecDirectory(): string {
@@ -91,14 +102,14 @@ export class MaestroPaths {
91
102
  public getSpecPath(specId: string): string {
92
103
  assertSpecId(specId);
93
104
 
94
- return resolve(this.repositoryRoot, this.specDirectoryFromRoot, specId);
105
+ return resolve(this.projectRoot, this.specDirectoryFromRoot, specId);
95
106
  }
96
107
 
97
108
  public getSpecFilePath(specId: string): string {
98
109
  assertSpecId(specId);
99
110
 
100
111
  return resolve(
101
- this.repositoryRoot,
112
+ this.projectRoot,
102
113
  this.specDirectoryFromRoot,
103
114
  specId,
104
115
  PATHS.SPEC_FILE,
@@ -109,10 +120,10 @@ export class MaestroPaths {
109
120
  assertSpecId(specId);
110
121
 
111
122
  return resolve(
112
- this.repositoryRoot,
123
+ this.projectRoot,
113
124
  this.specDirectoryFromRoot,
114
125
  specId,
115
- PATHS.WORKFLOW,
126
+ PATHS.WORKFLOW_FILE,
116
127
  );
117
128
  }
118
129
 
@@ -120,43 +131,96 @@ export class MaestroPaths {
120
131
  assertSpecId(specId);
121
132
 
122
133
  return resolve(
123
- this.repositoryRoot,
134
+ this.projectRoot,
124
135
  this.specDirectoryFromRoot,
125
136
  specId,
126
- PATHS.HANDOFFS,
137
+ PATHS.HANDOFFS_PATH,
127
138
  );
128
139
  }
129
140
 
130
- public getBuilderHandoffPath(specId: string): string {
131
- assertSpecId(specId);
141
+ public getBuilderHandoffsPath(specId: string): string {
142
+ return resolve(this.getSpecPath(specId), PATHS.BUILDER_HANDOFFS_PATH);
143
+ }
144
+
145
+ public getVerifierHandoffsPath(specId: string): string {
146
+ return resolve(this.getSpecPath(specId), PATHS.VERIFIER_HANDOFFS_PATH);
147
+ }
148
+
149
+ public getBuilderHandoffPath({
150
+ specId,
151
+ handoffPassNumber,
152
+ }: HandoffPathInput): string {
153
+ assertArtifactNumber(handoffPassNumber);
132
154
 
133
155
  return resolve(
134
- this.repositoryRoot,
135
- this.specDirectoryFromRoot,
136
- specId,
137
- PATHS.BUILDER_HANDOFF,
156
+ this.getBuilderHandoffsPath(specId),
157
+ `${AGENTS_PREFIX.BUILDER}${handoffPassNumber}.json`,
138
158
  );
139
159
  }
140
160
 
141
- public getVerifierHandoffPath(specId: string): string {
142
- assertSpecId(specId);
161
+ public getVerifierHandoffPath({
162
+ specId,
163
+ handoffPassNumber,
164
+ }: HandoffPathInput): string {
165
+ assertArtifactNumber(handoffPassNumber);
143
166
 
144
167
  return resolve(
145
- this.repositoryRoot,
146
- this.specDirectoryFromRoot,
147
- specId,
148
- PATHS.VERIFIER_HANDOFF,
168
+ this.getVerifierHandoffsPath(specId),
169
+ `${AGENTS_PREFIX.VERIFIER}${handoffPassNumber}.json`,
149
170
  );
150
171
  }
151
172
 
173
+ public async getActiveBuilderHandoffPath(specId: string): Promise<string> {
174
+ const handoffPassNumber = (
175
+ await readdir(this.getBuilderHandoffsPath(specId))
176
+ ).length;
177
+
178
+ if (handoffPassNumber === 0) throw new Error('Builder handoff is missing.');
179
+
180
+ return this.getBuilderHandoffPath({ specId, handoffPassNumber });
181
+ }
182
+
183
+ public async getActiveVerifierHandoffPath(specId: string): Promise<string> {
184
+ const handoffPassNumber = (
185
+ await readdir(this.getVerifierHandoffsPath(specId))
186
+ ).length;
187
+
188
+ if (handoffPassNumber === 0)
189
+ throw new Error('Verifier handoff is missing.');
190
+
191
+ return this.getVerifierHandoffPath({ specId, handoffPassNumber });
192
+ }
193
+
194
+ public async getNextBuilderHandoffPath(specId: string): Promise<string> {
195
+ const handoffPassNumber = (
196
+ await readdir(this.getBuilderHandoffsPath(specId))
197
+ ).length;
198
+
199
+ return this.getBuilderHandoffPath({
200
+ specId,
201
+ handoffPassNumber: handoffPassNumber + 1,
202
+ });
203
+ }
204
+
205
+ public async getNextVerifierHandoffPath(specId: string): Promise<string> {
206
+ const handoffPassNumber = (
207
+ await readdir(this.getVerifierHandoffsPath(specId))
208
+ ).length;
209
+
210
+ return this.getVerifierHandoffPath({
211
+ specId,
212
+ handoffPassNumber: handoffPassNumber + 1,
213
+ });
214
+ }
215
+
152
216
  public getEscalationsPath(specId: string): string {
153
217
  assertSpecId(specId);
154
218
 
155
219
  return resolve(
156
- this.repositoryRoot,
220
+ this.projectRoot,
157
221
  this.specDirectoryFromRoot,
158
222
  specId,
159
- PATHS.ESCALATIONS,
223
+ PATHS.ESCALATIONS_PATH,
160
224
  );
161
225
  }
162
226
 
@@ -164,14 +228,14 @@ export class MaestroPaths {
164
228
  specId,
165
229
  escalationNumber,
166
230
  }: EscalationPathInput): string {
167
- assertEscalationNumber(escalationNumber);
231
+ assertArtifactNumber(escalationNumber);
168
232
  assertSpecId(specId);
169
233
 
170
234
  return resolve(
171
- this.repositoryRoot,
235
+ this.projectRoot,
172
236
  this.specDirectoryFromRoot,
173
237
  specId,
174
- `${PATHS.ESCALATIONS}/E${escalationNumber}.json`,
238
+ `${PATHS.ESCALATIONS_PATH}/E${escalationNumber}.json`,
175
239
  );
176
240
  }
177
241
 
@@ -179,10 +243,10 @@ export class MaestroPaths {
179
243
  assertSpecId(specId);
180
244
 
181
245
  return resolve(
182
- this.repositoryRoot,
246
+ this.projectRoot,
183
247
  this.specDirectoryFromRoot,
184
248
  specId,
185
- PATHS.PROTOTYPES,
249
+ PATHS.PROTOTYPES_PATH,
186
250
  );
187
251
  }
188
252
 
@@ -195,7 +259,7 @@ export class MaestroPaths {
195
259
  const prototypesPath = this.getPrototypesPath(specId);
196
260
  const target = resolve(prototypesPath, relativePath);
197
261
 
198
- if (!isPathStrictlyWithin({ parent: prototypesPath, candidate: target })) {
262
+ if (!isPathStrictlyWithin({ parent: prototypesPath, path: target })) {
199
263
  throw new Error(
200
264
  'Prototype path must stay inside the prototypes directory.',
201
265
  );
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Objective: Check builder handoff content, spec ID, and revision.
2
+ * Objective: Check builder handoff content and spec ID.
3
3
  * Used: When Maestro handles builder handoff artifacts.
4
4
  */
5
5
 
@@ -48,14 +48,13 @@ function assertBuilderHandoffSchema(
48
48
  type AssertBuilderHandoffInput<Handoff = unknown> = {
49
49
  handoff: Handoff;
50
50
  specId: string;
51
- revision: number;
52
51
  };
53
52
 
54
53
  export function assertBuilderHandoff(
55
54
  input: AssertBuilderHandoffInput,
56
55
  ): asserts input is AssertBuilderHandoffInput<BuilderHandoff> {
57
56
  assertBuilderHandoffSchema(input.handoff);
58
- const { handoff, specId, revision } = input;
57
+ const { handoff, specId } = input;
59
58
 
60
59
  if (!isValidSpecId(handoff.specId)) {
61
60
  throw new Error(`Invalid builder handoff spec ID: "${handoff.specId}".`);
@@ -85,21 +84,9 @@ export function assertBuilderHandoff(
85
84
  throw new Error(`Invalid expected builder handoff spec ID: "${specId}".`);
86
85
  }
87
86
 
88
- if (!Number.isSafeInteger(revision) || revision < 1) {
89
- throw new Error(
90
- 'Expected builder handoff revision must be a positive integer.',
91
- );
92
- }
93
-
94
87
  if (handoff.specId !== specId) {
95
88
  throw new Error(
96
89
  `Builder handoff spec ID mismatch: expected "${specId}", found "${handoff.specId}".`,
97
90
  );
98
91
  }
99
-
100
- if (handoff.revision !== revision) {
101
- throw new Error(
102
- `Builder handoff revision mismatch: expected ${revision}, found ${handoff.revision}.`,
103
- );
104
- }
105
92
  }
@@ -10,18 +10,15 @@ import { readJsonFile } from '#utils/read-json.ts';
10
10
  type ReadBuilderHandoffInput = {
11
11
  path: string;
12
12
  specId: string;
13
- revision: number;
14
13
  };
15
14
 
16
15
  export const readBuilderHandoff = async ({
17
16
  path,
18
17
  specId,
19
- revision,
20
18
  }: ReadBuilderHandoffInput): Promise<BuilderHandoff> => {
21
19
  const input = {
22
20
  handoff: await readJsonFile({ path, description: 'Builder handoff' }),
23
21
  specId,
24
- revision,
25
22
  };
26
23
 
27
24
  assertBuilderHandoff(input);
@@ -51,7 +51,7 @@ const BuilderHandoffContentFields = {
51
51
  const BuilderHandoffFields = {
52
52
  version: Type.Literal(BUILDER_HANDOFF_VERSION),
53
53
  specId: Type.String({ pattern: SPEC_ID_PATTERN.source }),
54
- revision: Type.Integer({ minimum: 1 }),
54
+
55
55
  ...BuilderHandoffContentFields,
56
56
  };
57
57
 
@@ -4,21 +4,19 @@
4
4
  */
5
5
 
6
6
  import { assertBuilderHandoff } from '#artifacts/builder-handoff/assertBuilderHandoff.ts';
7
- import { writeJsonAtomically } from '#utils/write-json-atomically.ts';
7
+ import { writeJson } from '#utils/write-json.ts';
8
8
 
9
9
  type WriteBuilderHandoffInput = {
10
10
  path: string;
11
11
  handoff: unknown;
12
12
  specId: string;
13
- revision: number;
14
13
  };
15
14
 
16
15
  export const writeBuilderHandoff = async ({
17
16
  path,
18
17
  handoff,
19
18
  specId,
20
- revision,
21
19
  }: WriteBuilderHandoffInput): Promise<void> => {
22
- assertBuilderHandoff({ handoff, specId, revision });
23
- await writeJsonAtomically({ path, data: handoff });
20
+ assertBuilderHandoff({ handoff, specId });
21
+ await writeJson({ path, data: handoff });
24
22
  };
@@ -11,19 +11,17 @@ import {
11
11
  type Escalation,
12
12
  type NewEscalation,
13
13
  } from '#artifacts/escalation/schema.ts';
14
- import { writeJsonAtomically } from '#utils/write-json-atomically.ts';
14
+ import { writeJson } from '#utils/write-json.ts';
15
15
 
16
16
  type CreateEscalationInput = {
17
17
  directory: string;
18
18
  specId: string;
19
- revision: number;
20
19
  escalation: NewEscalation;
21
20
  };
22
21
 
23
22
  export const createEscalation = async ({
24
23
  directory,
25
24
  specId,
26
- revision,
27
25
  escalation,
28
26
  }: CreateEscalationInput): Promise<{
29
27
  path: string;
@@ -32,21 +30,23 @@ export const createEscalation = async ({
32
30
  const id = await getNextEscalationId({
33
31
  directory,
34
32
  specId,
35
- currentRevision: revision,
36
33
  });
37
34
 
38
35
  const newEscalation = {
39
- ...escalation,
36
+ question: escalation.question,
37
+ context: escalation.context,
38
+ options: escalation.options,
39
+ recommendation: escalation.recommendation,
40
+ notes: escalation.notes,
40
41
  version: ESCALATION_VERSION,
41
42
  specId,
42
- revision,
43
43
  id,
44
44
  resolution: null,
45
45
  };
46
46
 
47
47
  assertEscalation(newEscalation);
48
48
  const path = join(directory, `${id}.json`);
49
- await writeJsonAtomically({ path, data: newEscalation });
49
+ await writeJson({ path, data: newEscalation });
50
50
 
51
51
  return { path, escalation: newEscalation };
52
52
  };
@@ -8,18 +8,15 @@ import { readEscalationHistory } from '#artifacts/escalation/readEscalationHisto
8
8
  type GetNextEscalationIdInput = {
9
9
  directory: string;
10
10
  specId: string;
11
- currentRevision: number;
12
11
  };
13
12
 
14
13
  export const getNextEscalationId = async ({
15
14
  directory,
16
15
  specId,
17
- currentRevision,
18
16
  }: GetNextEscalationIdInput): Promise<string> => {
19
17
  const history = await readEscalationHistory({
20
18
  directory,
21
19
  specId,
22
- currentRevision,
23
20
  });
24
21
 
25
22
  return `E${history.length + 1}`;
@@ -10,41 +10,31 @@ import { readJsonFile } from '#utils/read-json.ts';
10
10
  type AssertWorkflowEscalationInput = {
11
11
  escalation: Escalation;
12
12
  specId: string;
13
- currentRevision: number;
14
13
  };
15
14
 
16
15
  function assertWorkflowEscalation({
17
16
  escalation,
18
17
  specId,
19
- currentRevision,
20
18
  }: AssertWorkflowEscalationInput): void {
21
19
  if (escalation.specId !== specId) {
22
20
  throw new Error(
23
21
  `Escalation spec ID mismatch: expected "${specId}", found "${escalation.specId}".`,
24
22
  );
25
23
  }
26
-
27
- if (escalation.revision > currentRevision) {
28
- throw new Error(
29
- `Escalation revision ${escalation.revision} is newer than workflow revision ${currentRevision}.`,
30
- );
31
- }
32
24
  }
33
25
 
34
26
  type ReadEscalationInput = {
35
27
  path: string;
36
28
  specId: string;
37
- currentRevision: number;
38
29
  };
39
30
 
40
31
  export const readEscalation = async ({
41
32
  path,
42
33
  specId,
43
- currentRevision,
44
34
  }: ReadEscalationInput): Promise<Escalation> => {
45
35
  const escalation = await readJsonFile({ path, description: 'Escalation' });
46
36
  assertEscalation(escalation);
47
- assertWorkflowEscalation({ escalation, specId, currentRevision });
37
+ assertWorkflowEscalation({ escalation, specId });
48
38
 
49
39
  return escalation;
50
40
  };
@@ -13,13 +13,11 @@ const ESCALATION_FILE_PATTERN = /^E([1-9]\d*)\.json$/;
13
13
  type ReadEscalationHistoryInput = {
14
14
  directory: string;
15
15
  specId: string;
16
- currentRevision: number;
17
16
  };
18
17
 
19
18
  export const readEscalationHistory = async ({
20
19
  directory,
21
20
  specId,
22
- currentRevision,
23
21
  }: ReadEscalationHistoryInput): Promise<Escalation[]> => {
24
22
  const files = await readdir(directory);
25
23
 
@@ -37,7 +35,6 @@ export const readEscalationHistory = async ({
37
35
  const escalation = await readEscalation({
38
36
  path: join(directory, file),
39
37
  specId,
40
- currentRevision,
41
38
  });
42
39
 
43
40
  escalations.push(escalation);
@@ -9,45 +9,35 @@ import type {
9
9
  Escalation,
10
10
  EscalationResolution,
11
11
  } from '#artifacts/escalation/schema.ts';
12
- import { writeJsonAtomically } from '#utils/write-json-atomically.ts';
12
+ import { writeJson } from '#utils/write-json.ts';
13
13
 
14
14
  type ResolveEscalationInput = {
15
15
  path: string;
16
16
  specId: string;
17
- revision: number;
18
17
  resolution: EscalationResolution;
19
18
  };
20
19
 
21
20
  export const resolveEscalation = async ({
22
21
  path,
23
22
  specId,
24
- revision,
25
23
  resolution,
26
24
  }: ResolveEscalationInput): Promise<Escalation> => {
27
25
  const current = await readEscalation({
28
26
  path,
29
27
  specId,
30
- currentRevision: revision,
31
28
  });
32
29
 
33
30
  if (current.resolution !== null) {
34
31
  throw new Error(`Escalation "${current.id}" is already resolved.`);
35
32
  }
36
33
 
37
- if (revision <= current.revision) {
38
- throw new Error(
39
- `Escalation resolution revision must be greater than ${current.revision}.`,
40
- );
41
- }
42
-
43
34
  const resolvedEscalation = {
44
35
  ...current,
45
- revision,
46
36
  resolution,
47
37
  };
48
38
 
49
39
  assertEscalation(resolvedEscalation);
50
- await writeJsonAtomically({ path, data: resolvedEscalation });
40
+ await writeJson({ path, data: resolvedEscalation });
51
41
 
52
42
  return resolvedEscalation;
53
43
  };
@@ -56,7 +56,6 @@ export const EscalationSchema = Type.Object(
56
56
  {
57
57
  version: Type.Literal(ESCALATION_VERSION),
58
58
  specId: Type.String({ pattern: SPEC_ID_PATTERN.source }),
59
- revision: Type.Integer({ minimum: 1 }),
60
59
  id: Type.String({ pattern: ESCALATION_ID_PATTERN.source }),
61
60
  ...NewEscalationFields,
62
61
  recommendation: Type.Union([EscalationRecommendationSchema, Type.Null()]),
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Objective: Check verifier handoff content, spec ID, and revision.
2
+ * Objective: Check verifier handoff content and spec ID.
3
3
  * Used: When Maestro handles verifier handoff artifacts.
4
4
  */
5
5
 
@@ -28,14 +28,13 @@ function assertVerifierHandoffSchema(
28
28
  type AssertVerifierHandoffInput<Handoff = unknown> = {
29
29
  handoff: Handoff;
30
30
  specId: string;
31
- revision: number;
32
31
  };
33
32
 
34
33
  export function assertVerifierHandoff(
35
34
  input: AssertVerifierHandoffInput,
36
35
  ): asserts input is AssertVerifierHandoffInput<VerifierHandoff> {
37
36
  assertVerifierHandoffSchema(input.handoff);
38
- const { handoff, specId, revision } = input;
37
+ const { handoff, specId } = input;
39
38
 
40
39
  if (!isValidSpecId(handoff.specId)) {
41
40
  throw new Error(`Invalid verifier handoff spec ID: "${handoff.specId}".`);
@@ -95,10 +94,4 @@ export function assertVerifierHandoff(
95
94
  `Verifier handoff spec ID mismatch: expected "${specId}", found "${handoff.specId}".`,
96
95
  );
97
96
  }
98
-
99
- if (handoff.revision !== revision) {
100
- throw new Error(
101
- `Verifier handoff revision mismatch: expected ${revision}, found ${handoff.revision}.`,
102
- );
103
- }
104
97
  }
@@ -10,18 +10,15 @@ import { readJsonFile } from '#utils/read-json.ts';
10
10
  type ReadVerifierHandoffInput = {
11
11
  path: string;
12
12
  specId: string;
13
- revision: number;
14
13
  };
15
14
 
16
15
  export const readVerifierHandoff = async ({
17
16
  path,
18
17
  specId,
19
- revision,
20
18
  }: ReadVerifierHandoffInput): Promise<VerifierHandoff> => {
21
19
  const input = {
22
20
  handoff: await readJsonFile({ path, description: 'Verifier handoff' }),
23
21
  specId,
24
- revision,
25
22
  };
26
23
 
27
24
  assertVerifierHandoff(input);
@@ -32,10 +32,25 @@ const FindingEvidenceSchema = Type.Object(
32
32
  { additionalProperties: false },
33
33
  );
34
34
 
35
- const FindingRejectionSchema = Type.Object(
36
- { reason: Type.String({ minLength: 1 }) },
37
- { additionalProperties: false },
38
- );
35
+ export const FINDING_DECISIONS = {
36
+ REJECT: 'reject',
37
+ FIX_CODE: 'fix-code',
38
+ } as const;
39
+
40
+ const FindingDecisionSchema = Type.Union([
41
+ Type.Null(),
42
+ Type.Object(
43
+ {
44
+ decision: Type.Literal(FINDING_DECISIONS.REJECT),
45
+ reason: Type.String({ minLength: 1 }),
46
+ },
47
+ { additionalProperties: false },
48
+ ),
49
+ Type.Object(
50
+ { decision: Type.Literal(FINDING_DECISIONS.FIX_CODE) },
51
+ { additionalProperties: false },
52
+ ),
53
+ ]);
39
54
 
40
55
  export const VerifierFindingSchema = Type.Object(
41
56
  {
@@ -48,7 +63,7 @@ export const VerifierFindingSchema = Type.Object(
48
63
  confidence: Type.Number({ minimum: 0, maximum: 1 }),
49
64
  summary: Type.String({ minLength: 1 }),
50
65
  evidence: Type.Array(FindingEvidenceSchema, { minItems: 1 }),
51
- rejection: Type.Union([FindingRejectionSchema, Type.Null()]),
66
+ decision: FindingDecisionSchema,
52
67
  },
53
68
  { additionalProperties: false },
54
69
  );
@@ -57,7 +72,6 @@ export const VerifierHandoffSchema = Type.Object(
57
72
  {
58
73
  version: Type.Literal(VERIFIER_HANDOFF_VERSION),
59
74
  specId: Type.String({ pattern: SPEC_ID_PATTERN.source }),
60
- revision: Type.Integer({ minimum: 1 }),
61
75
  summary: Type.String({ minLength: 1 }),
62
76
  acceptanceCriteria: Type.Array(BuilderAcceptanceCriterionSchema),
63
77
  findings: Type.Array(VerifierFindingSchema),