@emiliosp/pi-maestro 0.10.0 → 0.11.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 +53 -9
- package/CHANGELOG.md +79 -36
- package/MISSION.md +4 -2
- package/README.md +31 -15
- package/ROADMAP.md +150 -61
- package/TECH_STACK.md +12 -11
- package/agents/simplifier.md +214 -0
- package/agents/verifier.md +1 -16
- package/docs/configuration.md +27 -16
- package/docs/glossary.md +41 -36
- package/docs/subagent-integration.md +23 -13
- package/docs/workflow.md +103 -71
- package/extensions/maestro-subagent.ts +3 -1
- package/extensions/maestro.ts +10 -0
- package/package.json +1 -1
- package/src/MaestroPaths.ts +8 -32
- package/src/artifacts/builder-handoff/assertBuilderHandoff.ts +23 -29
- package/src/artifacts/simplifier-handoff/assertSimplifierHandoff.ts +56 -0
- package/src/artifacts/simplifier-handoff/readSimplifierHandoff.ts +29 -0
- package/src/artifacts/simplifier-handoff/schema.ts +26 -0
- package/src/artifacts/verifier-handoff/assertVerifierHandoff.ts +14 -18
- package/src/config/assertConfiguration.ts +1 -1
- package/src/config/defaults.ts +7 -0
- package/src/config/loadConfiguration.ts +21 -50
- package/src/config/schema.ts +3 -0
- package/src/maestro/checks/assertEnvironment.ts +7 -3
- package/src/maestro/instructions/getMaestroInstructions.ts +27 -9
- package/src/maestro/status/formatMaestroStatus.ts +3 -0
- package/src/specs/create.ts +7 -24
- package/src/tools/child/record-simplifier-handoff.ts +73 -0
- package/src/tools/main/resolve-findings.ts +4 -3
- package/src/tools/main/run-builder.ts +38 -48
- package/src/tools/main/run-simplifier.ts +182 -0
- package/src/tools/main/run-verifier.ts +14 -25
- package/src/utils/path-within-or-equal.ts +3 -4
- package/src/workflow/builder/resolveEscalations.ts +43 -27
- package/src/workflow/roles.ts +2 -1
- package/src/workflow/simplifier/completeSimplifierPass.ts +57 -0
- package/src/workflow/state/schema.ts +4 -0
- package/src/workflow/transitions.ts +13 -6
- package/src/workflow/utils/getAgentRole.ts +5 -1
- package/src/workflow/verifier/resolveFindings.ts +53 -87
- package/templates/spec.md +9 -5
package/src/specs/create.ts
CHANGED
|
@@ -7,8 +7,6 @@ import { mkdir, writeFile } from 'node:fs/promises';
|
|
|
7
7
|
import { createSpecId } from '#ids/createSpecId.ts';
|
|
8
8
|
import type { MaestroPaths } from '#MaestroPaths.ts';
|
|
9
9
|
import { loadSpecTemplate, renderSpecTemplate } from '#specs/template.ts';
|
|
10
|
-
import { isErrnoException } from '#utils/is-errno-exception.ts';
|
|
11
|
-
import { pathExists } from '#utils/path-exists.ts';
|
|
12
10
|
import {
|
|
13
11
|
assertWorkflowState,
|
|
14
12
|
WORKFLOW_PHASES,
|
|
@@ -49,10 +47,6 @@ export const createSpec = async ({
|
|
|
49
47
|
const workflowPath = paths.getWorkflowPath(specId);
|
|
50
48
|
const prototypesPath = paths.getPrototypesPath(specId);
|
|
51
49
|
|
|
52
|
-
if (await pathExists(specPath)) {
|
|
53
|
-
throw new Error(`Spec directory already exists: ${specPath}.`);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
50
|
const template = await loadSpecTemplate();
|
|
57
51
|
const spec = renderSpecTemplate({ template, specId, title });
|
|
58
52
|
|
|
@@ -63,12 +57,11 @@ export const createSpec = async ({
|
|
|
63
57
|
} as const;
|
|
64
58
|
|
|
65
59
|
assertWorkflowState(state);
|
|
66
|
-
|
|
60
|
+
|
|
61
|
+
await mkdir(paths.getSpecDirectory(), { recursive: true });
|
|
62
|
+
await mkdir(specPath);
|
|
67
63
|
|
|
68
64
|
try {
|
|
69
|
-
await mkdir(paths.getSpecDirectory(), { recursive: true });
|
|
70
|
-
await mkdir(specPath);
|
|
71
|
-
created = true;
|
|
72
65
|
await mkdir(prototypesPath, { recursive: true });
|
|
73
66
|
await writeFile(specFilePath, spec, { encoding: 'utf8', flag: 'wx' });
|
|
74
67
|
await writeWorkflowState({
|
|
@@ -76,20 +69,10 @@ export const createSpec = async ({
|
|
|
76
69
|
state,
|
|
77
70
|
});
|
|
78
71
|
} catch (error) {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if (created) {
|
|
86
|
-
throw new Error(
|
|
87
|
-
`Spec creation failed after creating ${specPath}. Remove this directory before retrying.`,
|
|
88
|
-
{ cause: error },
|
|
89
|
-
);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
throw error;
|
|
72
|
+
throw new Error(
|
|
73
|
+
`Spec creation failed after creating ${specPath}. Remove this directory before retrying.`,
|
|
74
|
+
{ cause: error },
|
|
75
|
+
);
|
|
93
76
|
}
|
|
94
77
|
|
|
95
78
|
return {
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Objective: Register the simplifier's fixed result recording tool.
|
|
3
|
+
* Used: When a child simplifier completes its refactoring pass.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
7
|
+
import { type Static, Type } from 'typebox';
|
|
8
|
+
import { Value } from 'typebox/value';
|
|
9
|
+
import {
|
|
10
|
+
SIMPLIFIER_HANDOFF_VERSION,
|
|
11
|
+
SimplifierSubmissionFields,
|
|
12
|
+
} from '#artifacts/simplifier-handoff/schema.ts';
|
|
13
|
+
import { resolveWorkflowContext } from '#tools/child/utils/resolveWorkflowContext.ts';
|
|
14
|
+
import { completeSimplifierPass } from '#workflow/simplifier/completeSimplifierPass.ts';
|
|
15
|
+
|
|
16
|
+
export const SIMPLIFIER_HANDOFF_TOOL = {
|
|
17
|
+
NAME: 'maestro_record_simplifier_handoff',
|
|
18
|
+
LABEL: 'Record Simplifier Handoff',
|
|
19
|
+
DESCRIPTION:
|
|
20
|
+
'Save the simplifier result at handoffs/simplifier/S.json and complete the workflow. Stop after success.',
|
|
21
|
+
} as const;
|
|
22
|
+
|
|
23
|
+
const SimplifierHandoffToolParameters = Type.Object(
|
|
24
|
+
SimplifierSubmissionFields,
|
|
25
|
+
{ additionalProperties: false },
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
function assertSimplifierSubmission(
|
|
29
|
+
input: unknown,
|
|
30
|
+
): asserts input is Static<typeof SimplifierHandoffToolParameters> {
|
|
31
|
+
const [error] = Value.Errors(SimplifierHandoffToolParameters, input);
|
|
32
|
+
|
|
33
|
+
if (error !== undefined) {
|
|
34
|
+
throw new Error(`Invalid simplifier submission: ${error.message}.`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const registerRecordSimplifierHandoffTool = (pi: ExtensionAPI): void => {
|
|
39
|
+
pi.registerTool({
|
|
40
|
+
name: SIMPLIFIER_HANDOFF_TOOL.NAME,
|
|
41
|
+
label: SIMPLIFIER_HANDOFF_TOOL.LABEL,
|
|
42
|
+
description: SIMPLIFIER_HANDOFF_TOOL.DESCRIPTION,
|
|
43
|
+
parameters: SimplifierHandoffToolParameters,
|
|
44
|
+
async execute(_toolCallId, params, _signal, _onUpdate, context) {
|
|
45
|
+
assertSimplifierSubmission(params);
|
|
46
|
+
|
|
47
|
+
const { paths } = await resolveWorkflowContext({
|
|
48
|
+
cwd: context.cwd,
|
|
49
|
+
specId: params.specId,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const completed = await completeSimplifierPass({
|
|
53
|
+
paths,
|
|
54
|
+
specId: params.specId,
|
|
55
|
+
handoff: { ...params, version: SIMPLIFIER_HANDOFF_VERSION },
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
content: [
|
|
60
|
+
{
|
|
61
|
+
type: 'text',
|
|
62
|
+
text: 'Simplifier handoff and candidate-ready phase saved. Stop now.',
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
details: {
|
|
66
|
+
specId: params.specId,
|
|
67
|
+
handoffPath: completed.handoffPath,
|
|
68
|
+
phase: completed.state.phase,
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
};
|
|
@@ -83,20 +83,21 @@ const formatFindingIds = (findingIds: string[]): string =>
|
|
|
83
83
|
const formatResolution = (details: FindingResolutionDetails): string => {
|
|
84
84
|
const rejected = formatFindingIds(details.rejectedFindingIds);
|
|
85
85
|
const requiringFix = formatFindingIds(details.findingsRequiringFixIds);
|
|
86
|
+
const summary = `Recorded rejections for ${rejected}. Findings requiring code fixes: ${requiringFix}.`;
|
|
86
87
|
|
|
87
88
|
if (details.phase === WORKFLOW_PHASES.DRAFTING_SPEC) {
|
|
88
89
|
const requiringRevision = formatFindingIds(
|
|
89
90
|
details.findingsRequiringSpecRevisionIds,
|
|
90
91
|
);
|
|
91
92
|
|
|
92
|
-
return
|
|
93
|
+
return `${summary} Findings requiring spec revision: ${requiringRevision}. The workflow is drafting-spec. Revise the same spec with the owner and obtain renewed GREEN FLAG. Then call maestro_mark_spec_ready separately before running the builder.`;
|
|
93
94
|
}
|
|
94
95
|
|
|
95
96
|
if (details.phase === WORKFLOW_PHASES.READY_FOR_BUILDER) {
|
|
96
|
-
return
|
|
97
|
+
return `${summary} The workflow is ready-for-builder. Run the builder separately.`;
|
|
97
98
|
}
|
|
98
99
|
|
|
99
|
-
return
|
|
100
|
+
return `${summary} The workflow is simplifier-running. Call maestro_run_simplifier next with this specId, without another approval or verifier pass.`;
|
|
100
101
|
};
|
|
101
102
|
|
|
102
103
|
export const registerResolveFindingsTool = (pi: ExtensionAPI): void => {
|
|
@@ -74,7 +74,7 @@ const readBuilderResult = async ({
|
|
|
74
74
|
);
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
return await
|
|
77
|
+
return await readTerminalBuilderResult({ paths, specId, state });
|
|
78
78
|
} catch (error) {
|
|
79
79
|
const message = error instanceof Error ? error.message : String(error);
|
|
80
80
|
|
|
@@ -82,73 +82,63 @@ const readBuilderResult = async ({
|
|
|
82
82
|
}
|
|
83
83
|
};
|
|
84
84
|
|
|
85
|
-
|
|
86
|
-
paths,
|
|
87
|
-
specId,
|
|
88
|
-
}: ReadBuilderResultInput): Promise<BuilderRunResult> => {
|
|
89
|
-
const handoff = await readBuilderHandoff({
|
|
90
|
-
path: await paths.getActiveBuilderHandoffPath(specId),
|
|
91
|
-
specId,
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
if (handoff.status !== BUILDER_HANDOFF_STATUSES.DONE) {
|
|
95
|
-
throw new Error(
|
|
96
|
-
`Builder handoff status does not match workflow phase "${WORKFLOW_PHASES.READY_FOR_VERIFIER}".`,
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
return {
|
|
101
|
-
outcome: BUILDER_HANDOFF_STATUSES.DONE,
|
|
102
|
-
specId,
|
|
103
|
-
phase: WORKFLOW_PHASES.READY_FOR_VERIFIER,
|
|
104
|
-
handoff,
|
|
105
|
-
};
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
type BuildRunResultInput = {
|
|
85
|
+
type ReadTerminalBuilderResultInput = {
|
|
109
86
|
paths: MaestroPaths;
|
|
110
87
|
specId: string;
|
|
111
88
|
state: WorkflowState;
|
|
112
89
|
};
|
|
113
90
|
|
|
114
|
-
const
|
|
91
|
+
const readTerminalBuilderResult = async ({
|
|
115
92
|
paths,
|
|
116
93
|
specId,
|
|
117
94
|
state,
|
|
118
|
-
}:
|
|
119
|
-
if (
|
|
120
|
-
|
|
95
|
+
}: ReadTerminalBuilderResultInput): Promise<BuilderRunResult> => {
|
|
96
|
+
if (
|
|
97
|
+
state.phase !== WORKFLOW_PHASES.READY_FOR_VERIFIER &&
|
|
98
|
+
state.phase !== WORKFLOW_PHASES.ESCALATION_DECISIONS
|
|
99
|
+
) {
|
|
100
|
+
throw new Error(
|
|
101
|
+
`Builder returned with unexpected workflow phase "${state.phase}".`,
|
|
102
|
+
);
|
|
121
103
|
}
|
|
122
104
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
});
|
|
105
|
+
const handoff = await readBuilderHandoff({
|
|
106
|
+
path: await paths.getActiveBuilderHandoffPath(specId),
|
|
107
|
+
specId,
|
|
108
|
+
});
|
|
128
109
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}
|
|
110
|
+
const expectedStatus =
|
|
111
|
+
state.phase === WORKFLOW_PHASES.READY_FOR_VERIFIER
|
|
112
|
+
? BUILDER_HANDOFF_STATUSES.DONE
|
|
113
|
+
: BUILDER_HANDOFF_STATUSES.ESCALATION;
|
|
134
114
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
115
|
+
if (handoff.status !== expectedStatus) {
|
|
116
|
+
throw new Error(
|
|
117
|
+
`Builder handoff status does not match workflow phase "${state.phase}".`,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
140
120
|
|
|
121
|
+
if (state.phase === WORKFLOW_PHASES.READY_FOR_VERIFIER) {
|
|
141
122
|
return {
|
|
142
|
-
outcome: BUILDER_HANDOFF_STATUSES.
|
|
123
|
+
outcome: BUILDER_HANDOFF_STATUSES.DONE,
|
|
143
124
|
specId,
|
|
144
125
|
phase: state.phase,
|
|
145
126
|
handoff,
|
|
146
127
|
};
|
|
147
128
|
}
|
|
148
129
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
130
|
+
if (handoff.escalations.some(({ resolution }) => resolution !== null)) {
|
|
131
|
+
throw new Error(
|
|
132
|
+
'Current builder escalations already contain an owner resolution.',
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
outcome: BUILDER_HANDOFF_STATUSES.ESCALATION,
|
|
138
|
+
specId,
|
|
139
|
+
phase: state.phase,
|
|
140
|
+
handoff,
|
|
141
|
+
};
|
|
152
142
|
};
|
|
153
143
|
|
|
154
144
|
const formatBuilderResult = (result: BuilderRunResult): string => {
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Objective: Run the simplifier in Git projects or explicitly skip unavailable Git.
|
|
3
|
+
* Used: After verification succeeds or the owner rejects all findings.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { randomUUID } from 'node:crypto';
|
|
7
|
+
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
8
|
+
import type { SubagentDelegationRequest } from 'pi-subagents/delegation';
|
|
9
|
+
import { Type } from 'typebox';
|
|
10
|
+
import { readSimplifierHandoff } from '#artifacts/simplifier-handoff/readSimplifierHandoff.ts';
|
|
11
|
+
import { AGENTS } from '#config/schema.ts';
|
|
12
|
+
import { SPEC_ID_PATTERN } from '#ids/isValidSpecId.ts';
|
|
13
|
+
import type { MaestroPaths } from '#MaestroPaths.ts';
|
|
14
|
+
import { refreshMaestroStatus } from '#maestro/status/refreshMaestroStatus.ts';
|
|
15
|
+
import {
|
|
16
|
+
assertDelegationResponse,
|
|
17
|
+
waitForDelegationResponse,
|
|
18
|
+
} from '#tools/utils/pi-subagent-delegation.ts';
|
|
19
|
+
import { resolveToolRunContext } from '#tools/utils/resolveToolRunContext.ts';
|
|
20
|
+
import { WORKFLOW_ROLES } from '#workflow/roles.ts';
|
|
21
|
+
import { readWorkflowState } from '#workflow/state/readWorkflowState.ts';
|
|
22
|
+
import { WORKFLOW_EVENTS, WORKFLOW_PHASES } from '#workflow/state/schema.ts';
|
|
23
|
+
import { writeWorkflowState } from '#workflow/state/writeWorkflowState.ts';
|
|
24
|
+
import { transitionWorkflow } from '#workflow/transitions.ts';
|
|
25
|
+
|
|
26
|
+
export const RUN_SIMPLIFIER_TOOL = {
|
|
27
|
+
NAME: 'maestro_run_simplifier',
|
|
28
|
+
LABEL: 'Run Simplifier',
|
|
29
|
+
DESCRIPTION:
|
|
30
|
+
'Run the simplifier in the foreground from simplifier-running. If Git readiness cannot be confirmed, warn and complete without delegation.',
|
|
31
|
+
} as const;
|
|
32
|
+
|
|
33
|
+
const RunSimplifierToolParameters = Type.Object(
|
|
34
|
+
{ specId: Type.String({ pattern: SPEC_ID_PATTERN.source }) },
|
|
35
|
+
{ additionalProperties: false },
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const MILLISECONDS_PER_MINUTE = 60_000;
|
|
39
|
+
|
|
40
|
+
const GIT_SKIP_WARNING = 'Simplifier skipped: Git repository unavailable.';
|
|
41
|
+
|
|
42
|
+
type GitReadinessInput = {
|
|
43
|
+
pi: ExtensionAPI;
|
|
44
|
+
projectRoot: string;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const isGitReady = async ({
|
|
48
|
+
pi,
|
|
49
|
+
projectRoot,
|
|
50
|
+
}: GitReadinessInput): Promise<boolean> => {
|
|
51
|
+
try {
|
|
52
|
+
const result = await pi.exec(
|
|
53
|
+
'git',
|
|
54
|
+
['rev-parse', '--is-inside-work-tree'],
|
|
55
|
+
{ cwd: projectRoot },
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
return result.code === 0 && result.stdout.trim() === 'true';
|
|
59
|
+
} catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
type ReadSimplifierResultInput = {
|
|
65
|
+
paths: MaestroPaths;
|
|
66
|
+
specId: string;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const readSimplifierResult = async ({
|
|
70
|
+
paths,
|
|
71
|
+
specId,
|
|
72
|
+
}: ReadSimplifierResultInput) => {
|
|
73
|
+
try {
|
|
74
|
+
const state = await readWorkflowState(paths.getWorkflowPath(specId));
|
|
75
|
+
|
|
76
|
+
if (state.specId !== specId) {
|
|
77
|
+
throw new Error(
|
|
78
|
+
`Workflow spec ID mismatch: expected "${specId}", found "${state.specId}".`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (state.phase !== WORKFLOW_PHASES.CANDIDATE_READY) {
|
|
83
|
+
throw new Error(
|
|
84
|
+
`The simplifier returned without recording a valid terminal handoff. Workflow phase is "${state.phase}".`,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const handoff = await readSimplifierHandoff({
|
|
89
|
+
path: paths.getSimplifierHandoffPath(specId),
|
|
90
|
+
specId,
|
|
91
|
+
projectRoot: paths.getProjectRoot(),
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
return { specId, phase: state.phase, outcome: state.phase, handoff };
|
|
95
|
+
} catch (error) {
|
|
96
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
97
|
+
throw new Error(`Simplifier protocol error: ${message}`, { cause: error });
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export const registerRunSimplifierTool = (pi: ExtensionAPI): void => {
|
|
102
|
+
pi.registerTool({
|
|
103
|
+
name: RUN_SIMPLIFIER_TOOL.NAME,
|
|
104
|
+
label: RUN_SIMPLIFIER_TOOL.LABEL,
|
|
105
|
+
description: RUN_SIMPLIFIER_TOOL.DESCRIPTION,
|
|
106
|
+
parameters: RunSimplifierToolParameters,
|
|
107
|
+
async execute(toolCallId, { specId }, _signal, _onUpdate, context) {
|
|
108
|
+
const { paths, config, projectRoot } = await resolveToolRunContext(
|
|
109
|
+
context.cwd,
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
const workflowPath = paths.getWorkflowPath(specId);
|
|
113
|
+
const state = await readWorkflowState(workflowPath);
|
|
114
|
+
|
|
115
|
+
if (state.specId !== specId) {
|
|
116
|
+
throw new Error(
|
|
117
|
+
`Workflow spec ID mismatch: expected "${specId}", found "${state.specId}".`,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (state.phase !== WORKFLOW_PHASES.SIMPLIFIER_RUNNING) {
|
|
122
|
+
throw new Error(
|
|
123
|
+
`Simplifier run requires simplifier-running state, found "${state.phase}".`,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
await refreshMaestroStatus(context);
|
|
128
|
+
|
|
129
|
+
if (!(await isGitReady({ pi, projectRoot }))) {
|
|
130
|
+
const nextState = transitionWorkflow({
|
|
131
|
+
state,
|
|
132
|
+
event: WORKFLOW_EVENTS.SIMPLIFIER_SKIPPED,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
await writeWorkflowState({ path: workflowPath, state: nextState });
|
|
136
|
+
context.ui.notify(GIT_SKIP_WARNING, 'warning');
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
content: [{ type: 'text', text: GIT_SKIP_WARNING }],
|
|
140
|
+
details: {
|
|
141
|
+
specId,
|
|
142
|
+
phase: nextState.phase,
|
|
143
|
+
skipped: true,
|
|
144
|
+
warning: GIT_SKIP_WARNING,
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const request: SubagentDelegationRequest = {
|
|
150
|
+
requestId: randomUUID(),
|
|
151
|
+
ownerRunId: toolCallId,
|
|
152
|
+
nodeId: WORKFLOW_ROLES.SIMPLIFIER,
|
|
153
|
+
agent: AGENTS.SIMPLIFIER,
|
|
154
|
+
task: `Simplify accepted code for specId "${specId}" in the project directory "${projectRoot}". Discover all current staged, unstaged, and untracked code changes with Git, including pre-existing changes.`,
|
|
155
|
+
context: 'fresh',
|
|
156
|
+
cwd: projectRoot,
|
|
157
|
+
model: config.simplifier.model,
|
|
158
|
+
thinking: config.simplifier.thinking,
|
|
159
|
+
timeoutMs: config.simplifier.timeoutMinutes * MILLISECONDS_PER_MINUTE,
|
|
160
|
+
result: { kind: 'text' },
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const response = await waitForDelegationResponse({
|
|
164
|
+
piEventsBus: pi.events,
|
|
165
|
+
request,
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
assertDelegationResponse(response);
|
|
169
|
+
const result = await readSimplifierResult({ paths, specId });
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
content: [
|
|
173
|
+
{
|
|
174
|
+
type: 'text',
|
|
175
|
+
text: `Simplifier completed spec ${specId}. The workflow is candidate-ready. Report the saved simplifier summary and notes as its report, not verifier evidence.`,
|
|
176
|
+
},
|
|
177
|
+
],
|
|
178
|
+
details: result,
|
|
179
|
+
};
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
};
|
|
@@ -39,22 +39,17 @@ const RunVerifierToolParameters = Type.Object(
|
|
|
39
39
|
|
|
40
40
|
const MILLISECONDS_PER_MINUTE = 60_000;
|
|
41
41
|
|
|
42
|
-
type
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
phase: typeof WORKFLOW_PHASES.CANDIDATE_READY;
|
|
46
|
-
handoff: VerifierHandoff;
|
|
47
|
-
};
|
|
42
|
+
type VerifierTerminalPhase =
|
|
43
|
+
| typeof WORKFLOW_PHASES.SIMPLIFIER_RUNNING
|
|
44
|
+
| typeof WORKFLOW_PHASES.FINDING_DECISIONS;
|
|
48
45
|
|
|
49
|
-
type
|
|
50
|
-
outcome:
|
|
46
|
+
type VerifierRunResult = {
|
|
47
|
+
outcome: VerifierTerminalPhase;
|
|
51
48
|
specId: string;
|
|
52
|
-
phase:
|
|
49
|
+
phase: VerifierTerminalPhase;
|
|
53
50
|
handoff: VerifierHandoff;
|
|
54
51
|
};
|
|
55
52
|
|
|
56
|
-
type VerifierRunResult = CandidateReadyResult | FindingsResult;
|
|
57
|
-
|
|
58
53
|
type ReadTerminalVerifierResultInput = {
|
|
59
54
|
paths: MaestroPaths;
|
|
60
55
|
specId: string;
|
|
@@ -65,11 +60,9 @@ const readTerminalVerifierResult = async ({
|
|
|
65
60
|
paths,
|
|
66
61
|
specId,
|
|
67
62
|
state,
|
|
68
|
-
}: ReadTerminalVerifierResultInput): Promise<
|
|
69
|
-
CandidateReadyResult | FindingsResult
|
|
70
|
-
> => {
|
|
63
|
+
}: ReadTerminalVerifierResultInput): Promise<VerifierRunResult> => {
|
|
71
64
|
if (
|
|
72
|
-
state.phase !== WORKFLOW_PHASES.
|
|
65
|
+
state.phase !== WORKFLOW_PHASES.SIMPLIFIER_RUNNING &&
|
|
73
66
|
state.phase !== WORKFLOW_PHASES.FINDING_DECISIONS
|
|
74
67
|
) {
|
|
75
68
|
throw new Error(
|
|
@@ -85,7 +78,7 @@ const readTerminalVerifierResult = async ({
|
|
|
85
78
|
const expectedPhase =
|
|
86
79
|
handoff.findings.length > 0
|
|
87
80
|
? WORKFLOW_PHASES.FINDING_DECISIONS
|
|
88
|
-
: WORKFLOW_PHASES.
|
|
81
|
+
: WORKFLOW_PHASES.SIMPLIFIER_RUNNING;
|
|
89
82
|
|
|
90
83
|
if (state.phase !== expectedPhase) {
|
|
91
84
|
throw new Error(
|
|
@@ -93,16 +86,12 @@ const readTerminalVerifierResult = async ({
|
|
|
93
86
|
);
|
|
94
87
|
}
|
|
95
88
|
|
|
96
|
-
|
|
89
|
+
return {
|
|
97
90
|
specId,
|
|
98
91
|
handoff,
|
|
92
|
+
outcome: state.phase,
|
|
93
|
+
phase: state.phase,
|
|
99
94
|
};
|
|
100
|
-
|
|
101
|
-
if (state.phase === WORKFLOW_PHASES.CANDIDATE_READY) {
|
|
102
|
-
return { ...details, outcome: state.phase, phase: state.phase };
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
return { ...details, outcome: state.phase, phase: state.phase };
|
|
106
95
|
};
|
|
107
96
|
|
|
108
97
|
type ReadVerifierResultInput = {
|
|
@@ -142,8 +131,8 @@ const readVerifierResult = async ({
|
|
|
142
131
|
};
|
|
143
132
|
|
|
144
133
|
const formatVerifierResult = (result: VerifierRunResult): string => {
|
|
145
|
-
if (result.outcome === WORKFLOW_PHASES.
|
|
146
|
-
return `Verifier completed spec ${result.specId}. The workflow is
|
|
134
|
+
if (result.outcome === WORKFLOW_PHASES.SIMPLIFIER_RUNNING) {
|
|
135
|
+
return `Verifier completed spec ${result.specId}. The workflow is simplifier-running. Call maestro_run_simplifier next with this specId, without another approval or verifier pass.`;
|
|
147
136
|
}
|
|
148
137
|
|
|
149
138
|
return `Verifier found ${result.handoff.findings.length} finding(s) for spec ${result.specId}. The workflow is waiting for owner decisions.`;
|
|
@@ -17,9 +17,8 @@ export const isPathWithinOrEqual = ({
|
|
|
17
17
|
const pathToCandidate = relative(parent, candidate);
|
|
18
18
|
|
|
19
19
|
return (
|
|
20
|
-
pathToCandidate
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
!isAbsolute(pathToCandidate))
|
|
20
|
+
!pathToCandidate.startsWith(`..${sep}`) &&
|
|
21
|
+
pathToCandidate !== '..' &&
|
|
22
|
+
!isAbsolute(pathToCandidate)
|
|
24
23
|
);
|
|
25
24
|
};
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { readBuilderHandoff } from '#artifacts/builder-handoff/readBuilderHandoff.ts';
|
|
7
7
|
import {
|
|
8
8
|
BUILDER_HANDOFF_STATUSES,
|
|
9
|
+
type Escalation,
|
|
9
10
|
type EscalationResolution,
|
|
10
11
|
} from '#artifacts/builder-handoff/schema.ts';
|
|
11
12
|
import { writeBuilderHandoff } from '#artifacts/builder-handoff/writeBuilderHandoff.ts';
|
|
@@ -20,6 +21,45 @@ export type EscalationDecision = EscalationResolution & {
|
|
|
20
21
|
escalationId: string;
|
|
21
22
|
};
|
|
22
23
|
|
|
24
|
+
type ApplyEscalationDecisionsInput = {
|
|
25
|
+
escalations: Escalation[];
|
|
26
|
+
decisions: EscalationDecision[];
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const applyEscalationDecisions = ({
|
|
30
|
+
escalations,
|
|
31
|
+
decisions,
|
|
32
|
+
}: ApplyEscalationDecisionsInput) => {
|
|
33
|
+
if (escalations.some(({ resolution }) => resolution !== null)) {
|
|
34
|
+
throw new Error('Current questions already contain an owner resolution.');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (decisions.length === 0)
|
|
38
|
+
throw new Error('At least one escalation decision is required.');
|
|
39
|
+
|
|
40
|
+
const expectedIds = new Set(escalations.map(({ id }) => id));
|
|
41
|
+
const resolutions = new Map<string, EscalationResolution>();
|
|
42
|
+
|
|
43
|
+
for (const { escalationId, ...resolution } of decisions) {
|
|
44
|
+
if (!expectedIds.has(escalationId))
|
|
45
|
+
throw new Error(`Unknown escalation ID: "${escalationId}".`);
|
|
46
|
+
|
|
47
|
+
if (resolutions.has(escalationId))
|
|
48
|
+
throw new Error(`Duplicate decision for escalation "${escalationId}".`);
|
|
49
|
+
|
|
50
|
+
resolutions.set(escalationId, resolution);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return escalations.map((escalation) => {
|
|
54
|
+
const resolution = resolutions.get(escalation.id);
|
|
55
|
+
|
|
56
|
+
if (resolution === undefined)
|
|
57
|
+
throw new Error(`Missing decision for escalation "${escalation.id}".`);
|
|
58
|
+
|
|
59
|
+
return { ...escalation, resolution };
|
|
60
|
+
});
|
|
61
|
+
};
|
|
62
|
+
|
|
23
63
|
type ResolveEscalationsInput = {
|
|
24
64
|
paths: MaestroPaths;
|
|
25
65
|
specId: string;
|
|
@@ -51,33 +91,9 @@ export const resolveEscalations = async ({
|
|
|
51
91
|
throw new Error('Escalation resolution requires an escalation handoff.');
|
|
52
92
|
}
|
|
53
93
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (decisions.length === 0)
|
|
59
|
-
throw new Error('At least one escalation decision is required.');
|
|
60
|
-
|
|
61
|
-
const expectedIds = new Set(handoff.escalations.map(({ id }) => id));
|
|
62
|
-
const resolutions = new Map<string, EscalationResolution>();
|
|
63
|
-
|
|
64
|
-
for (const { escalationId, ...resolution } of decisions) {
|
|
65
|
-
if (!expectedIds.has(escalationId))
|
|
66
|
-
throw new Error(`Unknown escalation ID: "${escalationId}".`);
|
|
67
|
-
|
|
68
|
-
if (resolutions.has(escalationId))
|
|
69
|
-
throw new Error(`Duplicate decision for escalation "${escalationId}".`);
|
|
70
|
-
|
|
71
|
-
resolutions.set(escalationId, resolution);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const escalations = handoff.escalations.map((escalation) => {
|
|
75
|
-
const resolution = resolutions.get(escalation.id);
|
|
76
|
-
|
|
77
|
-
if (resolution === undefined)
|
|
78
|
-
throw new Error(`Missing decision for escalation "${escalation.id}".`);
|
|
79
|
-
|
|
80
|
-
return { ...escalation, resolution };
|
|
94
|
+
const escalations = applyEscalationDecisions({
|
|
95
|
+
escalations: handoff.escalations,
|
|
96
|
+
decisions,
|
|
81
97
|
});
|
|
82
98
|
|
|
83
99
|
const nextHandoff = { ...handoff, escalations };
|
package/src/workflow/roles.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Objective: Define the roles used by the Maestro workflow.
|
|
3
|
-
* Used: When workflow code identifies
|
|
3
|
+
* Used: When workflow code identifies agent responsibilities.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
export const WORKFLOW_ROLES = {
|
|
7
7
|
BUILDER: 'builder',
|
|
8
8
|
VERIFIER: 'verifier',
|
|
9
|
+
SIMPLIFIER: 'simplifier',
|
|
9
10
|
} as const;
|
|
10
11
|
|
|
11
12
|
export type WorkflowRole = (typeof WORKFLOW_ROLES)[keyof typeof WORKFLOW_ROLES];
|