@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.
- package/README.md +38 -41
- package/agents/builder.md +16 -18
- package/agents/verifier.md +24 -21
- package/docs/configuration.md +16 -24
- package/docs/subagent-integration.md +18 -40
- package/docs/workflow.md +106 -169
- package/package.json +1 -2
- package/src/MaestroPaths.ts +111 -47
- package/src/artifacts/builder-handoff/assertBuilderHandoff.ts +2 -15
- package/src/artifacts/builder-handoff/readBuilderHandoff.ts +0 -3
- package/src/artifacts/builder-handoff/schema.ts +1 -1
- package/src/artifacts/builder-handoff/writeBuilderHandoff.ts +3 -5
- package/src/artifacts/escalation/createEscalation.ts +7 -7
- package/src/artifacts/escalation/getNextEscalationId.ts +0 -3
- package/src/artifacts/escalation/readEscalation.ts +1 -11
- package/src/artifacts/escalation/readEscalationHistory.ts +0 -3
- package/src/artifacts/escalation/resolveEscalation.ts +2 -12
- package/src/artifacts/escalation/schema.ts +0 -1
- package/src/artifacts/verifier-handoff/assertVerifierHandoff.ts +2 -9
- package/src/artifacts/verifier-handoff/readVerifierHandoff.ts +0 -3
- package/src/artifacts/verifier-handoff/schema.ts +20 -6
- package/src/artifacts/verifier-handoff/writeVerifierHandoff.ts +7 -7
- package/src/config/loadConfiguration.ts +18 -18
- package/src/maestro/checks/assertEnvironment.ts +13 -17
- package/src/maestro/instructions/getMaestroInstructions.ts +33 -24
- package/src/specs/create.ts +0 -2
- package/src/tools/child/open-escalation.ts +3 -4
- package/src/tools/child/record-builder-handoff.ts +4 -4
- package/src/tools/child/record-verifier-handoff.ts +13 -26
- package/src/tools/child/utils/resolveWorkflowContext.ts +3 -3
- package/src/tools/main/mark-spec-ready.ts +2 -2
- package/src/tools/main/resolve-escalation.ts +2 -4
- package/src/tools/main/resolve-findings.ts +17 -15
- package/src/tools/main/run-builder.ts +10 -38
- package/src/tools/main/run-verifier.ts +6 -33
- package/src/tools/utils/resolveToolRunContext.ts +8 -8
- package/src/utils/path-strictly-within.ts +4 -4
- package/src/utils/write-json.ts +24 -0
- package/src/workflow/builder/completeBuilderPass.ts +6 -15
- package/src/workflow/builder/prepareBuilderRun.ts +3 -34
- package/src/workflow/escalation/openBuilderEscalation.ts +2 -10
- package/src/workflow/escalation/resolveBuilderEscalation.ts +9 -14
- package/src/workflow/findings/resolveFindings.ts +25 -135
- package/src/workflow/spec/markSpecReady.ts +0 -1
- package/src/workflow/state/readWorkflowState.ts +2 -2
- package/src/workflow/state/schema.ts +0 -1
- package/src/workflow/state/writeWorkflowState.ts +7 -63
- package/src/workflow/transitions.ts +2 -2
- package/src/workflow/verifier/completeVerifierPass.ts +8 -14
- package/src/workflow/verifier/prepareVerifierRun.ts +4 -41
- package/src/artifacts/verifier-handoff/rejectVerifierFinding.ts +0 -54
- package/src/git/command.ts +0 -172
- package/src/git/commits/createCommit.ts +0 -76
- package/src/git/commits/createWorkflowCheckpointCommit.ts +0 -24
- package/src/git/commits/findCommitByMessage.ts +0 -39
- package/src/git/commits/getStagedPaths.ts +0 -23
- package/src/git/history/getParentCommit.ts +0 -25
- package/src/git/repository/assertRepositoryTrusted.ts +0 -16
- package/src/git/repository/findRepositoryRoot.ts +0 -19
- package/src/git/repository/getCurrentBranch.ts +0 -18
- package/src/git/repository/getHeadCommit.ts +0 -18
- package/src/git/repository/getRepositoryStatus.ts +0 -74
- package/src/git/utils/hasGitExitCode.ts +0 -19
- package/src/utils/write-atomically.ts +0 -41
- package/src/utils/write-json-atomically.ts +0 -28
package/src/MaestroPaths.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Objective: Build validated paths for one Maestro workflow in the current
|
|
3
|
-
* Used: Whenever Maestro reads or writes
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
30
|
-
if (!Number.isSafeInteger(
|
|
31
|
-
throw new Error('
|
|
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
|
-
|
|
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
|
|
74
|
+
private readonly projectRoot: string;
|
|
64
75
|
private readonly specDirectory: string;
|
|
65
76
|
|
|
66
77
|
private readonly specDirectoryFromRoot: string;
|
|
67
78
|
|
|
68
|
-
public constructor({
|
|
79
|
+
public constructor({ projectRoot, config }: MaestroPathsInput) {
|
|
69
80
|
if (
|
|
70
81
|
!isPathStrictlyWithin({
|
|
71
|
-
parent:
|
|
72
|
-
|
|
82
|
+
parent: projectRoot,
|
|
83
|
+
path: config.specDirectory,
|
|
73
84
|
})
|
|
74
85
|
) {
|
|
75
|
-
throw new Error('Generated Maestro path leaves the
|
|
86
|
+
throw new Error('Generated Maestro path leaves the project root.');
|
|
76
87
|
}
|
|
77
88
|
|
|
78
|
-
this.
|
|
89
|
+
this.projectRoot = projectRoot;
|
|
79
90
|
this.specDirectory = config.specDirectory;
|
|
80
|
-
this.specDirectoryFromRoot = relative(
|
|
91
|
+
this.specDirectoryFromRoot = relative(projectRoot, config.specDirectory);
|
|
81
92
|
}
|
|
82
93
|
|
|
83
|
-
public
|
|
84
|
-
return this.
|
|
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.
|
|
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.
|
|
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.
|
|
123
|
+
this.projectRoot,
|
|
113
124
|
this.specDirectoryFromRoot,
|
|
114
125
|
specId,
|
|
115
|
-
PATHS.
|
|
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.
|
|
134
|
+
this.projectRoot,
|
|
124
135
|
this.specDirectoryFromRoot,
|
|
125
136
|
specId,
|
|
126
|
-
PATHS.
|
|
137
|
+
PATHS.HANDOFFS_PATH,
|
|
127
138
|
);
|
|
128
139
|
}
|
|
129
140
|
|
|
130
|
-
public
|
|
131
|
-
|
|
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.
|
|
135
|
-
|
|
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(
|
|
142
|
-
|
|
161
|
+
public getVerifierHandoffPath({
|
|
162
|
+
specId,
|
|
163
|
+
handoffPassNumber,
|
|
164
|
+
}: HandoffPathInput): string {
|
|
165
|
+
assertArtifactNumber(handoffPassNumber);
|
|
143
166
|
|
|
144
167
|
return resolve(
|
|
145
|
-
this.
|
|
146
|
-
|
|
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.
|
|
220
|
+
this.projectRoot,
|
|
157
221
|
this.specDirectoryFromRoot,
|
|
158
222
|
specId,
|
|
159
|
-
PATHS.
|
|
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
|
-
|
|
231
|
+
assertArtifactNumber(escalationNumber);
|
|
168
232
|
assertSpecId(specId);
|
|
169
233
|
|
|
170
234
|
return resolve(
|
|
171
|
-
this.
|
|
235
|
+
this.projectRoot,
|
|
172
236
|
this.specDirectoryFromRoot,
|
|
173
237
|
specId,
|
|
174
|
-
`${PATHS.
|
|
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.
|
|
246
|
+
this.projectRoot,
|
|
183
247
|
this.specDirectoryFromRoot,
|
|
184
248
|
specId,
|
|
185
|
-
PATHS.
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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
|
|
23
|
-
await
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|
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
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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),
|