@emiliosp/pi-maestro 0.11.0 → 0.12.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/ADR.md +54 -22
- package/CHANGELOG.md +42 -28
- package/MISSION.md +2 -4
- package/README.md +53 -24
- package/ROADMAP.md +64 -64
- package/TECH_STACK.md +3 -3
- package/agents/builder.md +53 -57
- package/agents/simplifier.md +79 -61
- package/agents/verifier.md +52 -64
- package/docs/configuration.md +1 -1
- package/docs/glossary.md +43 -40
- package/docs/subagent-integration.md +7 -8
- package/docs/workflow.md +135 -62
- package/extensions/maestro-subagent.ts +1 -0
- package/extensions/maestro.ts +9 -30
- package/package.json +38 -39
- package/src/MaestroPaths.ts +22 -35
- package/src/artifacts/builder-handoff/assertBuilderHandoff.ts +6 -15
- package/src/artifacts/builder-handoff/schema.ts +3 -6
- package/src/artifacts/simplifier-handoff/assertSimplifierHandoff.ts +3 -3
- package/src/artifacts/simplifier-handoff/schema.ts +1 -0
- package/src/artifacts/verifier-handoff/assertVerifierHandoff.ts +4 -10
- package/src/artifacts/verifier-handoff/schema.ts +3 -6
- package/src/artifacts/verifier-handoff/writeVerifierHandoff.ts +1 -3
- package/src/config/assertConfiguration.ts +3 -6
- package/src/config/defaults.ts +1 -5
- package/src/config/loadConfiguration.ts +4 -11
- package/src/config/schema.ts +1 -2
- package/src/ids/createSpecId.ts +1 -2
- package/src/ids/isValidSpecId.ts +1 -2
- package/src/maestro/checks/assertEnvironment.ts +19 -50
- package/src/maestro/instructions/getMaestroInstructions.ts +45 -26
- package/src/maestro/status/formatMaestroStatus.ts +4 -13
- package/src/maestro/status/refreshMaestroStatus.ts +3 -7
- package/src/specs/create.ts +14 -4
- package/src/specs/template.ts +10 -14
- package/src/tools/child/record-builder-handoff.ts +1 -0
- package/src/tools/child/record-simplifier-handoff.ts +4 -4
- package/src/tools/child/record-verifier-handoff.ts +2 -3
- package/src/tools/child/utils/resolveWorkflowContext.ts +1 -4
- package/src/tools/main/create-spec.ts +3 -2
- package/src/tools/main/mark-spec-ready.ts +2 -1
- package/src/tools/main/resolve-escalations.ts +1 -0
- package/src/tools/main/resolve-findings.ts +6 -20
- package/src/tools/main/run-builder.ts +37 -57
- package/src/tools/main/run-simplifier.ts +10 -20
- package/src/tools/main/run-verifier.ts +31 -55
- package/src/tools/utils/pi-subagent-delegation.ts +13 -22
- package/src/tools/utils/resolveToolRunContext.ts +2 -3
- package/src/utils/is-errno-exception.ts +1 -3
- package/src/utils/path-exists.ts +1 -0
- package/src/utils/path-strictly-within.ts +3 -6
- package/src/utils/path-within-or-equal.ts +1 -4
- package/src/utils/read-json.ts +1 -4
- package/src/utils/write-json.ts +1 -4
- package/src/workflow/builder/completeBuilderPass.ts +3 -2
- package/src/workflow/builder/prepareBuilderRun.ts +3 -8
- package/src/workflow/builder/resolveEscalations.ts +2 -6
- package/src/workflow/builder/utils/getWorkflowEventByBuilderHandoff.ts +1 -3
- package/src/workflow/simplifier/completeSimplifierPass.ts +2 -3
- package/src/workflow/spec/markSpecReady.ts +10 -4
- package/src/workflow/state/readWorkflowState.ts +2 -7
- package/src/workflow/state/schema.ts +5 -10
- package/src/workflow/state/writeWorkflowState.ts +1 -4
- package/src/workflow/transitions.ts +3 -11
- package/src/workflow/utils/assertSpecAcceptanceCriteria.ts +2 -1
- package/src/workflow/utils/getAgentRole.ts +1 -3
- package/src/workflow/verifier/completeVerifierPass.ts +1 -0
- package/src/workflow/verifier/resolveFindings.ts +9 -29
- package/src/workflow/verifier/utils/getWorkflowEventByVerifierHandoff.ts +5 -20
- package/templates/design.md +20 -0
- package/templates/proposal.md +23 -0
- package/templates/spec.md +21 -50
|
@@ -4,14 +4,16 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { randomUUID } from 'node:crypto';
|
|
7
|
+
|
|
7
8
|
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
8
9
|
import type { SubagentDelegationRequest } from 'pi-subagents/delegation';
|
|
9
10
|
import { Type } from 'typebox';
|
|
11
|
+
|
|
10
12
|
import { readSimplifierHandoff } from '#artifacts/simplifier-handoff/readSimplifierHandoff.ts';
|
|
11
13
|
import { AGENTS } from '#config/schema.ts';
|
|
12
14
|
import { SPEC_ID_PATTERN } from '#ids/isValidSpecId.ts';
|
|
13
|
-
import type { MaestroPaths } from '#MaestroPaths.ts';
|
|
14
15
|
import { refreshMaestroStatus } from '#maestro/status/refreshMaestroStatus.ts';
|
|
16
|
+
import type { MaestroPaths } from '#MaestroPaths.ts';
|
|
15
17
|
import {
|
|
16
18
|
assertDelegationResponse,
|
|
17
19
|
waitForDelegationResponse,
|
|
@@ -44,16 +46,11 @@ type GitReadinessInput = {
|
|
|
44
46
|
projectRoot: string;
|
|
45
47
|
};
|
|
46
48
|
|
|
47
|
-
const isGitReady = async ({
|
|
48
|
-
pi,
|
|
49
|
-
projectRoot,
|
|
50
|
-
}: GitReadinessInput): Promise<boolean> => {
|
|
49
|
+
const isGitReady = async ({ pi, projectRoot }: GitReadinessInput): Promise<boolean> => {
|
|
51
50
|
try {
|
|
52
|
-
const result = await pi.exec(
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
{ cwd: projectRoot },
|
|
56
|
-
);
|
|
51
|
+
const result = await pi.exec('git', ['rev-parse', '--is-inside-work-tree'], {
|
|
52
|
+
cwd: projectRoot,
|
|
53
|
+
});
|
|
57
54
|
|
|
58
55
|
return result.code === 0 && result.stdout.trim() === 'true';
|
|
59
56
|
} catch {
|
|
@@ -66,17 +63,12 @@ type ReadSimplifierResultInput = {
|
|
|
66
63
|
specId: string;
|
|
67
64
|
};
|
|
68
65
|
|
|
69
|
-
const readSimplifierResult = async ({
|
|
70
|
-
paths,
|
|
71
|
-
specId,
|
|
72
|
-
}: ReadSimplifierResultInput) => {
|
|
66
|
+
const readSimplifierResult = async ({ paths, specId }: ReadSimplifierResultInput) => {
|
|
73
67
|
try {
|
|
74
68
|
const state = await readWorkflowState(paths.getWorkflowPath(specId));
|
|
75
69
|
|
|
76
70
|
if (state.specId !== specId) {
|
|
77
|
-
throw new Error(
|
|
78
|
-
`Workflow spec ID mismatch: expected "${specId}", found "${state.specId}".`,
|
|
79
|
-
);
|
|
71
|
+
throw new Error(`Workflow spec ID mismatch: expected "${specId}", found "${state.specId}".`);
|
|
80
72
|
}
|
|
81
73
|
|
|
82
74
|
if (state.phase !== WORKFLOW_PHASES.CANDIDATE_READY) {
|
|
@@ -105,9 +97,7 @@ export const registerRunSimplifierTool = (pi: ExtensionAPI): void => {
|
|
|
105
97
|
description: RUN_SIMPLIFIER_TOOL.DESCRIPTION,
|
|
106
98
|
parameters: RunSimplifierToolParameters,
|
|
107
99
|
async execute(toolCallId, { specId }, _signal, _onUpdate, context) {
|
|
108
|
-
const { paths, config, projectRoot } = await resolveToolRunContext(
|
|
109
|
-
context.cwd,
|
|
110
|
-
);
|
|
100
|
+
const { paths, config, projectRoot } = await resolveToolRunContext(context.cwd);
|
|
111
101
|
|
|
112
102
|
const workflowPath = paths.getWorkflowPath(specId);
|
|
113
103
|
const state = await readWorkflowState(workflowPath);
|
|
@@ -4,15 +4,17 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { randomUUID } from 'node:crypto';
|
|
7
|
+
|
|
7
8
|
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
8
9
|
import type { SubagentDelegationRequest } from 'pi-subagents/delegation';
|
|
9
10
|
import { Type } from 'typebox';
|
|
11
|
+
|
|
10
12
|
import { readVerifierHandoff } from '#artifacts/verifier-handoff/readVerifierHandoff.ts';
|
|
11
13
|
import type { VerifierHandoff } from '#artifacts/verifier-handoff/schema.ts';
|
|
12
14
|
import { AGENTS } from '#config/schema.ts';
|
|
13
15
|
import { SPEC_ID_PATTERN } from '#ids/isValidSpecId.ts';
|
|
14
|
-
import type { MaestroPaths } from '#MaestroPaths.ts';
|
|
15
16
|
import { refreshMaestroStatus } from '#maestro/status/refreshMaestroStatus.ts';
|
|
17
|
+
import type { MaestroPaths } from '#MaestroPaths.ts';
|
|
16
18
|
import {
|
|
17
19
|
assertDelegationResponse,
|
|
18
20
|
waitForDelegationResponse,
|
|
@@ -20,7 +22,7 @@ import {
|
|
|
20
22
|
import { resolveToolRunContext } from '#tools/utils/resolveToolRunContext.ts';
|
|
21
23
|
import { WORKFLOW_ROLES } from '#workflow/roles.ts';
|
|
22
24
|
import { readWorkflowState } from '#workflow/state/readWorkflowState.ts';
|
|
23
|
-
import { WORKFLOW_PHASES
|
|
25
|
+
import { WORKFLOW_PHASES } from '#workflow/state/schema.ts';
|
|
24
26
|
import { prepareVerifierRun } from '#workflow/verifier/prepareVerifierRun.ts';
|
|
25
27
|
|
|
26
28
|
export const RUN_VERIFIER_TOOL = {
|
|
@@ -50,50 +52,6 @@ type VerifierRunResult = {
|
|
|
50
52
|
handoff: VerifierHandoff;
|
|
51
53
|
};
|
|
52
54
|
|
|
53
|
-
type ReadTerminalVerifierResultInput = {
|
|
54
|
-
paths: MaestroPaths;
|
|
55
|
-
specId: string;
|
|
56
|
-
state: WorkflowState;
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
const readTerminalVerifierResult = async ({
|
|
60
|
-
paths,
|
|
61
|
-
specId,
|
|
62
|
-
state,
|
|
63
|
-
}: ReadTerminalVerifierResultInput): Promise<VerifierRunResult> => {
|
|
64
|
-
if (
|
|
65
|
-
state.phase !== WORKFLOW_PHASES.SIMPLIFIER_RUNNING &&
|
|
66
|
-
state.phase !== WORKFLOW_PHASES.FINDING_DECISIONS
|
|
67
|
-
) {
|
|
68
|
-
throw new Error(
|
|
69
|
-
`Verifier returned with unexpected workflow phase "${state.phase}".`,
|
|
70
|
-
);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const handoff = await readVerifierHandoff({
|
|
74
|
-
path: await paths.getActiveVerifierHandoffPath(specId),
|
|
75
|
-
specId,
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
const expectedPhase =
|
|
79
|
-
handoff.findings.length > 0
|
|
80
|
-
? WORKFLOW_PHASES.FINDING_DECISIONS
|
|
81
|
-
: WORKFLOW_PHASES.SIMPLIFIER_RUNNING;
|
|
82
|
-
|
|
83
|
-
if (state.phase !== expectedPhase) {
|
|
84
|
-
throw new Error(
|
|
85
|
-
`Verifier handoff findings do not match workflow phase "${state.phase}".`,
|
|
86
|
-
);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return {
|
|
90
|
-
specId,
|
|
91
|
-
handoff,
|
|
92
|
-
outcome: state.phase,
|
|
93
|
-
phase: state.phase,
|
|
94
|
-
};
|
|
95
|
-
};
|
|
96
|
-
|
|
97
55
|
type ReadVerifierResultInput = {
|
|
98
56
|
paths: MaestroPaths;
|
|
99
57
|
specId: string;
|
|
@@ -107,22 +65,40 @@ const readVerifierResult = async ({
|
|
|
107
65
|
const state = await readWorkflowState(paths.getWorkflowPath(specId));
|
|
108
66
|
|
|
109
67
|
if (state.specId !== specId) {
|
|
110
|
-
throw new Error(
|
|
111
|
-
`Workflow spec ID mismatch: expected "${specId}", found "${state.specId}".`,
|
|
112
|
-
);
|
|
68
|
+
throw new Error(`Workflow spec ID mismatch: expected "${specId}", found "${state.specId}".`);
|
|
113
69
|
}
|
|
114
70
|
|
|
115
71
|
if (state.phase === WORKFLOW_PHASES.VERIFIER_RUNNING) {
|
|
116
|
-
throw new Error(
|
|
117
|
-
'The verifier returned without recording a valid terminal handoff.',
|
|
118
|
-
);
|
|
72
|
+
throw new Error('The verifier returned without recording a valid terminal handoff.');
|
|
119
73
|
}
|
|
120
74
|
|
|
121
|
-
|
|
122
|
-
|
|
75
|
+
if (
|
|
76
|
+
state.phase !== WORKFLOW_PHASES.SIMPLIFIER_RUNNING &&
|
|
77
|
+
state.phase !== WORKFLOW_PHASES.FINDING_DECISIONS
|
|
78
|
+
) {
|
|
79
|
+
throw new Error(`Verifier returned with unexpected workflow phase "${state.phase}".`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const handoff = await readVerifierHandoff({
|
|
83
|
+
path: await paths.getActiveVerifierHandoffPath(specId),
|
|
123
84
|
specId,
|
|
124
|
-
state,
|
|
125
85
|
});
|
|
86
|
+
|
|
87
|
+
const expectedPhase =
|
|
88
|
+
handoff.findings.length > 0
|
|
89
|
+
? WORKFLOW_PHASES.FINDING_DECISIONS
|
|
90
|
+
: WORKFLOW_PHASES.SIMPLIFIER_RUNNING;
|
|
91
|
+
|
|
92
|
+
if (state.phase !== expectedPhase) {
|
|
93
|
+
throw new Error(`Verifier handoff findings do not match workflow phase "${state.phase}".`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
specId,
|
|
98
|
+
handoff,
|
|
99
|
+
outcome: state.phase,
|
|
100
|
+
phase: state.phase,
|
|
101
|
+
};
|
|
126
102
|
} catch (error) {
|
|
127
103
|
const message = error instanceof Error ? error.message : String(error);
|
|
128
104
|
|
|
@@ -50,8 +50,7 @@ export const matchesDelegationRequest = ({
|
|
|
50
50
|
|
|
51
51
|
return (
|
|
52
52
|
response.requestId === request.requestId &&
|
|
53
|
-
(response.ownerRunId === undefined ||
|
|
54
|
-
response.ownerRunId === request.ownerRunId) &&
|
|
53
|
+
(response.ownerRunId === undefined || response.ownerRunId === request.ownerRunId) &&
|
|
55
54
|
(response.nodeId === undefined || response.nodeId === request.nodeId)
|
|
56
55
|
);
|
|
57
56
|
};
|
|
@@ -65,20 +64,16 @@ export const waitForDelegationResponse = async ({
|
|
|
65
64
|
piEventsBus,
|
|
66
65
|
request,
|
|
67
66
|
}: WaitForDelegationResponseInput): Promise<SubagentDelegationResponse> => {
|
|
68
|
-
const { promise, resolve } =
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
// JUSTIFICATION: matchesDelegationRequest accepts only the response for this pi-subagents request.
|
|
79
|
-
resolve(payload as SubagentDelegationResponse);
|
|
80
|
-
},
|
|
81
|
-
);
|
|
67
|
+
const { promise, resolve } = Promise.withResolvers<SubagentDelegationResponse>();
|
|
68
|
+
|
|
69
|
+
const unsubscribe = piEventsBus.on(SUBAGENT_DELEGATION_RESPONSE_EVENT, (payload) => {
|
|
70
|
+
if (!matchesDelegationRequest({ request, payload })) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// JUSTIFICATION: matchesDelegationRequest accepts only the response for this pi-subagents request.
|
|
75
|
+
resolve(payload as SubagentDelegationResponse);
|
|
76
|
+
});
|
|
82
77
|
|
|
83
78
|
try {
|
|
84
79
|
try {
|
|
@@ -97,9 +92,7 @@ export const waitForDelegationResponse = async ({
|
|
|
97
92
|
}
|
|
98
93
|
};
|
|
99
94
|
|
|
100
|
-
export function assertDelegationResponse(
|
|
101
|
-
response: SubagentDelegationResponse,
|
|
102
|
-
): void {
|
|
95
|
+
export function assertDelegationResponse(response: SubagentDelegationResponse): void {
|
|
103
96
|
if (response.status === DELEGATION_STATUSES.COMPLETED) {
|
|
104
97
|
return;
|
|
105
98
|
}
|
|
@@ -121,7 +114,5 @@ export function assertDelegationResponse(
|
|
|
121
114
|
throw new Error(`Delegation error: ${error}`);
|
|
122
115
|
}
|
|
123
116
|
|
|
124
|
-
throw new Error(
|
|
125
|
-
`Protocol error: unsupported final response status "${response.status}".`,
|
|
126
|
-
);
|
|
117
|
+
throw new Error(`Protocol error: unsupported final response status "${response.status}".`);
|
|
127
118
|
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { realpath } from 'node:fs/promises';
|
|
7
|
+
|
|
7
8
|
import { loadConfiguration } from '#config/loadConfiguration.ts';
|
|
8
9
|
import type { MaestroConfig } from '#config/schema.ts';
|
|
9
10
|
import { MaestroPaths } from '#MaestroPaths.ts';
|
|
@@ -14,9 +15,7 @@ export type ToolRunContext = {
|
|
|
14
15
|
paths: MaestroPaths;
|
|
15
16
|
};
|
|
16
17
|
|
|
17
|
-
export const resolveToolRunContext = async (
|
|
18
|
-
cwd: string,
|
|
19
|
-
): Promise<ToolRunContext> => {
|
|
18
|
+
export const resolveToolRunContext = async (cwd: string): Promise<ToolRunContext> => {
|
|
20
19
|
const projectRoot = await realpath(cwd);
|
|
21
20
|
const config = await loadConfiguration(projectRoot);
|
|
22
21
|
|
|
@@ -3,7 +3,5 @@
|
|
|
3
3
|
* Used: When filesystem operations handle specific error codes.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
export const isErrnoException = (
|
|
7
|
-
error: unknown,
|
|
8
|
-
): error is NodeJS.ErrnoException =>
|
|
6
|
+
export const isErrnoException = (error: unknown): error is NodeJS.ErrnoException =>
|
|
9
7
|
error instanceof Error && 'code' in error && typeof error.code === 'string';
|
package/src/utils/path-exists.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { relative } from 'node:path';
|
|
7
|
+
|
|
7
8
|
import { isPathWithinOrEqual } from '#utils/path-within-or-equal.ts';
|
|
8
9
|
|
|
9
10
|
type IsPathStrictlyWithinInput = {
|
|
@@ -11,9 +12,5 @@ type IsPathStrictlyWithinInput = {
|
|
|
11
12
|
path: string;
|
|
12
13
|
};
|
|
13
14
|
|
|
14
|
-
export const isPathStrictlyWithin = ({
|
|
15
|
-
parent,
|
|
16
|
-
path,
|
|
17
|
-
}: IsPathStrictlyWithinInput): boolean =>
|
|
18
|
-
relative(parent, path) !== '' &&
|
|
19
|
-
isPathWithinOrEqual({ parent, candidate: path });
|
|
15
|
+
export const isPathStrictlyWithin = ({ parent, path }: IsPathStrictlyWithinInput): boolean =>
|
|
16
|
+
relative(parent, path) !== '' && isPathWithinOrEqual({ parent, candidate: path });
|
|
@@ -10,10 +10,7 @@ type IsPathWithinOrEqualInput = {
|
|
|
10
10
|
candidate: string;
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
-
export const isPathWithinOrEqual = ({
|
|
14
|
-
parent,
|
|
15
|
-
candidate,
|
|
16
|
-
}: IsPathWithinOrEqualInput): boolean => {
|
|
13
|
+
export const isPathWithinOrEqual = ({ parent, candidate }: IsPathWithinOrEqualInput): boolean => {
|
|
17
14
|
const pathToCandidate = relative(parent, candidate);
|
|
18
15
|
|
|
19
16
|
return (
|
package/src/utils/read-json.ts
CHANGED
|
@@ -10,10 +10,7 @@ type ReadJsonFileInput = {
|
|
|
10
10
|
description: string;
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
-
export const readJsonFile = async ({
|
|
14
|
-
path,
|
|
15
|
-
description,
|
|
16
|
-
}: ReadJsonFileInput): Promise<unknown> => {
|
|
13
|
+
export const readJsonFile = async ({ path, description }: ReadJsonFileInput): Promise<unknown> => {
|
|
17
14
|
const content = await readFile(path, 'utf8');
|
|
18
15
|
|
|
19
16
|
try {
|
package/src/utils/write-json.ts
CHANGED
|
@@ -10,10 +10,7 @@ type WriteJsonInput = {
|
|
|
10
10
|
data: unknown;
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
-
export const writeJson = async ({
|
|
14
|
-
path,
|
|
15
|
-
data,
|
|
16
|
-
}: WriteJsonInput): Promise<void> => {
|
|
13
|
+
export const writeJson = async ({ path, data }: WriteJsonInput): Promise<void> => {
|
|
17
14
|
const content = JSON.stringify(data, null, 2);
|
|
18
15
|
|
|
19
16
|
if (content === undefined) {
|
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { mkdir, readFile } from 'node:fs/promises';
|
|
7
|
+
|
|
7
8
|
import { Value } from 'typebox/value';
|
|
9
|
+
|
|
8
10
|
import { assertBuilderHandoff } from '#artifacts/builder-handoff/assertBuilderHandoff.ts';
|
|
9
11
|
import {
|
|
10
12
|
BUILDER_HANDOFF_VERSION,
|
|
@@ -33,8 +35,7 @@ function assertBuilderHandoffSubmission(
|
|
|
33
35
|
): asserts input is BuilderHandoffSubmissionInput {
|
|
34
36
|
const [error] = Value.Errors(BuilderHandoffSubmissionSchema, input);
|
|
35
37
|
|
|
36
|
-
if (error !== undefined)
|
|
37
|
-
throw new Error(`Invalid builder submission: ${error.message}.`);
|
|
38
|
+
if (error !== undefined) throw new Error(`Invalid builder submission: ${error.message}.`);
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
type CompleteBuilderPassInput = {
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* Used: Before Maestro runs a builder pass.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import type { MaestroPaths } from '#MaestroPaths.ts';
|
|
7
6
|
import maestroSessionState from '#maestro/session/MaestroSessionState.ts';
|
|
7
|
+
import type { MaestroPaths } from '#MaestroPaths.ts';
|
|
8
8
|
import { pathExists } from '#utils/path-exists.ts';
|
|
9
9
|
import { readWorkflowState } from '#workflow/state/readWorkflowState.ts';
|
|
10
10
|
import { WORKFLOW_EVENTS, WORKFLOW_PHASES } from '#workflow/state/schema.ts';
|
|
@@ -21,16 +21,11 @@ type AssertBuilderRunBaseInput = {
|
|
|
21
21
|
specId: string;
|
|
22
22
|
};
|
|
23
23
|
|
|
24
|
-
async function assertBuilderRunBase({
|
|
25
|
-
paths,
|
|
26
|
-
specId,
|
|
27
|
-
}: AssertBuilderRunBaseInput): Promise<void> {
|
|
24
|
+
async function assertBuilderRunBase({ paths, specId }: AssertBuilderRunBaseInput): Promise<void> {
|
|
28
25
|
const state = await readWorkflowState(paths.getWorkflowPath(specId));
|
|
29
26
|
|
|
30
27
|
if (state.specId !== specId) {
|
|
31
|
-
throw new Error(
|
|
32
|
-
`Workflow spec ID mismatch: expected "${specId}", found "${state.specId}".`,
|
|
33
|
-
);
|
|
28
|
+
throw new Error(`Workflow spec ID mismatch: expected "${specId}", found "${state.specId}".`);
|
|
34
29
|
}
|
|
35
30
|
|
|
36
31
|
if (state.phase !== WORKFLOW_PHASES.READY_FOR_BUILDER) {
|
|
@@ -26,16 +26,12 @@ type ApplyEscalationDecisionsInput = {
|
|
|
26
26
|
decisions: EscalationDecision[];
|
|
27
27
|
};
|
|
28
28
|
|
|
29
|
-
const applyEscalationDecisions = ({
|
|
30
|
-
escalations,
|
|
31
|
-
decisions,
|
|
32
|
-
}: ApplyEscalationDecisionsInput) => {
|
|
29
|
+
const applyEscalationDecisions = ({ escalations, decisions }: ApplyEscalationDecisionsInput) => {
|
|
33
30
|
if (escalations.some(({ resolution }) => resolution !== null)) {
|
|
34
31
|
throw new Error('Current questions already contain an owner resolution.');
|
|
35
32
|
}
|
|
36
33
|
|
|
37
|
-
if (decisions.length === 0)
|
|
38
|
-
throw new Error('At least one escalation decision is required.');
|
|
34
|
+
if (decisions.length === 0) throw new Error('At least one escalation decision is required.');
|
|
39
35
|
|
|
40
36
|
const expectedIds = new Set(escalations.map(({ id }) => id));
|
|
41
37
|
const resolutions = new Map<string, EscalationResolution>();
|
|
@@ -9,9 +9,7 @@ import {
|
|
|
9
9
|
} from '#artifacts/builder-handoff/schema.ts';
|
|
10
10
|
import { WORKFLOW_EVENTS, type WorkflowEvent } from '#workflow/state/schema.ts';
|
|
11
11
|
|
|
12
|
-
export const getWorkflowEventByBuilderHandoff = ({
|
|
13
|
-
status,
|
|
14
|
-
}: BuilderHandoff): WorkflowEvent => {
|
|
12
|
+
export const getWorkflowEventByBuilderHandoff = ({ status }: BuilderHandoff): WorkflowEvent => {
|
|
15
13
|
if (status === BUILDER_HANDOFF_STATUSES.DONE) {
|
|
16
14
|
return WORKFLOW_EVENTS.BUILDER_DONE;
|
|
17
15
|
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { mkdir } from 'node:fs/promises';
|
|
7
7
|
import { dirname } from 'node:path';
|
|
8
|
+
|
|
8
9
|
import { assertSimplifierHandoff } from '#artifacts/simplifier-handoff/assertSimplifierHandoff.ts';
|
|
9
10
|
import type { MaestroPaths } from '#MaestroPaths.ts';
|
|
10
11
|
import { writeJson } from '#utils/write-json.ts';
|
|
@@ -28,9 +29,7 @@ export const completeSimplifierPass = async ({
|
|
|
28
29
|
const state = await readWorkflowState(workflowPath);
|
|
29
30
|
|
|
30
31
|
if (state.specId !== specId) {
|
|
31
|
-
throw new Error(
|
|
32
|
-
`Workflow spec ID mismatch: expected "${specId}", found "${state.specId}".`,
|
|
33
|
-
);
|
|
32
|
+
throw new Error(`Workflow spec ID mismatch: expected "${specId}", found "${state.specId}".`);
|
|
34
33
|
}
|
|
35
34
|
|
|
36
35
|
if (state.phase !== WORKFLOW_PHASES.SIMPLIFIER_RUNNING) {
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { readFile } from 'node:fs/promises';
|
|
7
|
+
|
|
7
8
|
import type { MaestroPaths } from '#MaestroPaths.ts';
|
|
8
9
|
import { pathExists } from '#utils/path-exists.ts';
|
|
9
10
|
import { readWorkflowState } from '#workflow/state/readWorkflowState.ts';
|
|
@@ -12,7 +13,8 @@ import { writeWorkflowState } from '#workflow/state/writeWorkflowState.ts';
|
|
|
12
13
|
import { transitionWorkflow } from '#workflow/transitions.ts';
|
|
13
14
|
|
|
14
15
|
function assertAcceptanceCriterionOrdering(markdown: string): void {
|
|
15
|
-
|
|
16
|
+
// Match standalone ACs (###), including regression checks, and ACs nested under requirements (####).
|
|
17
|
+
const criteria = [...markdown.matchAll(/^#{3,4} AC(\d+):/gm)];
|
|
16
18
|
|
|
17
19
|
if (criteria.some((match, index) => Number(match[1]) !== index + 1)) {
|
|
18
20
|
throw new Error('Fix acceptance criterion ordering in spec.md.');
|
|
@@ -46,13 +48,17 @@ export const markSpecReady = async ({
|
|
|
46
48
|
throw new Error(`Spec file is missing: ${specFilePath}.`);
|
|
47
49
|
}
|
|
48
50
|
|
|
51
|
+
const proposalFilePath = paths.getProposalFilePath(specId);
|
|
52
|
+
|
|
53
|
+
if (!(await pathExists(proposalFilePath))) {
|
|
54
|
+
throw new Error(`Proposal file is missing: ${proposalFilePath}.`);
|
|
55
|
+
}
|
|
56
|
+
|
|
49
57
|
const workflowPath = paths.getWorkflowPath(specId);
|
|
50
58
|
const current = await readWorkflowState(workflowPath);
|
|
51
59
|
|
|
52
60
|
if (current.specId !== specId) {
|
|
53
|
-
throw new Error(
|
|
54
|
-
`Workflow spec ID mismatch: expected "${specId}", found "${current.specId}".`,
|
|
55
|
-
);
|
|
61
|
+
throw new Error(`Workflow spec ID mismatch: expected "${specId}", found "${current.specId}".`);
|
|
56
62
|
}
|
|
57
63
|
|
|
58
64
|
const state = transitionWorkflow({
|
|
@@ -4,14 +4,9 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { readJsonFile } from '#utils/read-json.ts';
|
|
7
|
-
import {
|
|
8
|
-
assertWorkflowState,
|
|
9
|
-
type WorkflowState,
|
|
10
|
-
} from '#workflow/state/schema.ts';
|
|
7
|
+
import { assertWorkflowState, type WorkflowState } from '#workflow/state/schema.ts';
|
|
11
8
|
|
|
12
|
-
export const readWorkflowState = async (
|
|
13
|
-
path: string,
|
|
14
|
-
): Promise<WorkflowState> => {
|
|
9
|
+
export const readWorkflowState = async (path: string): Promise<WorkflowState> => {
|
|
15
10
|
const state = await readJsonFile({ path, description: 'Workflow state' });
|
|
16
11
|
assertWorkflowState(state);
|
|
17
12
|
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { type Static, Type } from 'typebox';
|
|
7
7
|
import { Value } from 'typebox/value';
|
|
8
|
+
|
|
8
9
|
import { isValidSpecId, SPEC_ID_PATTERN } from '#ids/isValidSpecId.ts';
|
|
9
10
|
|
|
10
11
|
export const WORKFLOW_STATE_VERSION = '1.0.0';
|
|
@@ -58,17 +59,13 @@ export const WorkflowStateSchema = Type.Object(
|
|
|
58
59
|
{ additionalProperties: false },
|
|
59
60
|
);
|
|
60
61
|
|
|
61
|
-
export type WorkflowPhase =
|
|
62
|
-
(typeof WORKFLOW_PHASES)[keyof typeof WORKFLOW_PHASES];
|
|
62
|
+
export type WorkflowPhase = (typeof WORKFLOW_PHASES)[keyof typeof WORKFLOW_PHASES];
|
|
63
63
|
|
|
64
|
-
export type WorkflowEvent =
|
|
65
|
-
(typeof WORKFLOW_EVENTS)[keyof typeof WORKFLOW_EVENTS];
|
|
64
|
+
export type WorkflowEvent = (typeof WORKFLOW_EVENTS)[keyof typeof WORKFLOW_EVENTS];
|
|
66
65
|
|
|
67
66
|
export type WorkflowState = Static<typeof WorkflowStateSchema>;
|
|
68
67
|
|
|
69
|
-
function assertWorkflowStateSchema(
|
|
70
|
-
input: unknown,
|
|
71
|
-
): asserts input is WorkflowState {
|
|
68
|
+
function assertWorkflowStateSchema(input: unknown): asserts input is WorkflowState {
|
|
72
69
|
if (typeof input !== 'object' || input === null || Array.isArray(input)) {
|
|
73
70
|
throw new Error('Workflow state must be a JSON object.');
|
|
74
71
|
}
|
|
@@ -80,9 +77,7 @@ function assertWorkflowStateSchema(
|
|
|
80
77
|
}
|
|
81
78
|
}
|
|
82
79
|
|
|
83
|
-
export function assertWorkflowState(
|
|
84
|
-
input: unknown,
|
|
85
|
-
): asserts input is WorkflowState {
|
|
80
|
+
export function assertWorkflowState(input: unknown): asserts input is WorkflowState {
|
|
86
81
|
assertWorkflowStateSchema(input);
|
|
87
82
|
|
|
88
83
|
if (!isValidSpecId(input.specId)) {
|
|
@@ -4,10 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { writeJson } from '#utils/write-json.ts';
|
|
7
|
-
import {
|
|
8
|
-
assertWorkflowState,
|
|
9
|
-
type WorkflowState,
|
|
10
|
-
} from '#workflow/state/schema.ts';
|
|
7
|
+
import { assertWorkflowState, type WorkflowState } from '#workflow/state/schema.ts';
|
|
11
8
|
|
|
12
9
|
type WriteWorkflowStateInput = {
|
|
13
10
|
path: string;
|
|
@@ -35,10 +35,7 @@ export const WORKFLOW_TRANSITIONS: Record<WorkflowEvent, WorkflowTransition> = {
|
|
|
35
35
|
to: WORKFLOW_PHASES.READY_FOR_BUILDER,
|
|
36
36
|
},
|
|
37
37
|
[WORKFLOW_EVENTS.OWNER_REQUEST_SPEC_REVISION]: {
|
|
38
|
-
from: [
|
|
39
|
-
WORKFLOW_PHASES.ESCALATION_DECISIONS,
|
|
40
|
-
WORKFLOW_PHASES.FINDING_DECISIONS,
|
|
41
|
-
],
|
|
38
|
+
from: [WORKFLOW_PHASES.ESCALATION_DECISIONS, WORKFLOW_PHASES.FINDING_DECISIONS],
|
|
42
39
|
to: WORKFLOW_PHASES.DRAFTING_SPEC,
|
|
43
40
|
},
|
|
44
41
|
[WORKFLOW_EVENTS.BUILDER_DONE]: {
|
|
@@ -80,10 +77,7 @@ type TransitionWorkflowInput = {
|
|
|
80
77
|
event: WorkflowEvent;
|
|
81
78
|
};
|
|
82
79
|
|
|
83
|
-
export const transitionWorkflow = ({
|
|
84
|
-
state,
|
|
85
|
-
event,
|
|
86
|
-
}: TransitionWorkflowInput): WorkflowState => {
|
|
80
|
+
export const transitionWorkflow = ({ state, event }: TransitionWorkflowInput): WorkflowState => {
|
|
87
81
|
assertWorkflowState(state);
|
|
88
82
|
const transition = WORKFLOW_TRANSITIONS[event];
|
|
89
83
|
|
|
@@ -92,9 +86,7 @@ export const transitionWorkflow = ({
|
|
|
92
86
|
}
|
|
93
87
|
|
|
94
88
|
if (!transition.from.includes(state.phase)) {
|
|
95
|
-
throw new Error(
|
|
96
|
-
`Workflow event "${event}" is not allowed from phase "${state.phase}".`,
|
|
97
|
-
);
|
|
89
|
+
throw new Error(`Workflow event "${event}" is not allowed from phase "${state.phase}".`);
|
|
98
90
|
}
|
|
99
91
|
|
|
100
92
|
return {
|
|
@@ -12,7 +12,8 @@ export function assertSpecAcceptanceCriteria({
|
|
|
12
12
|
markdown,
|
|
13
13
|
criterionIds,
|
|
14
14
|
}: AssertSpecAcceptanceCriteriaInput): void {
|
|
15
|
-
|
|
15
|
+
// Match standalone ACs (###), including regression checks, and ACs nested under requirements (####).
|
|
16
|
+
const missingIds = [...markdown.matchAll(/^#{3,4} AC(\d+):/gm)]
|
|
16
17
|
.map((match) => `AC${match[1]}`)
|
|
17
18
|
.filter((id) => !criterionIds.has(id));
|
|
18
19
|
|
|
@@ -18,9 +18,7 @@ const VERIFIER_PHASES: readonly WorkflowState['phase'][] = [
|
|
|
18
18
|
WORKFLOW_PHASES.FINDING_DECISIONS,
|
|
19
19
|
];
|
|
20
20
|
|
|
21
|
-
export const getAgentRole = (
|
|
22
|
-
phase: WorkflowState['phase'],
|
|
23
|
-
): WorkflowRole | null => {
|
|
21
|
+
export const getAgentRole = (phase: WorkflowState['phase']): WorkflowRole | null => {
|
|
24
22
|
if (BUILDER_PHASES.includes(phase)) {
|
|
25
23
|
return WORKFLOW_ROLES.BUILDER;
|
|
26
24
|
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { mkdir, readFile } from 'node:fs/promises';
|
|
7
|
+
|
|
7
8
|
import { assertVerifierHandoff } from '#artifacts/verifier-handoff/assertVerifierHandoff.ts';
|
|
8
9
|
import type { VerifierHandoff } from '#artifacts/verifier-handoff/schema.ts';
|
|
9
10
|
import { writeVerifierHandoff } from '#artifacts/verifier-handoff/writeVerifierHandoff.ts';
|