@emiliosp/pi-maestro 0.10.1 → 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 +94 -18
- package/CHANGELOG.md +87 -33
- package/MISSION.md +4 -4
- package/README.md +66 -22
- package/ROADMAP.md +141 -112
- package/TECH_STACK.md +12 -11
- package/agents/builder.md +53 -57
- package/agents/simplifier.md +232 -0
- package/agents/verifier.md +52 -79
- package/docs/configuration.md +27 -16
- package/docs/glossary.md +47 -39
- package/docs/subagent-integration.md +22 -13
- package/docs/workflow.md +207 -103
- package/extensions/maestro-subagent.ts +4 -1
- package/extensions/maestro.ts +15 -26
- package/package.json +38 -39
- package/src/MaestroPaths.ts +26 -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 +56 -0
- package/src/artifacts/simplifier-handoff/readSimplifierHandoff.ts +29 -0
- package/src/artifacts/simplifier-handoff/schema.ts +27 -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 +4 -7
- package/src/config/defaults.ts +8 -5
- package/src/config/loadConfiguration.ts +5 -11
- package/src/config/schema.ts +4 -2
- package/src/ids/createSpecId.ts +1 -2
- package/src/ids/isValidSpecId.ts +1 -2
- package/src/maestro/checks/assertEnvironment.ts +21 -48
- package/src/maestro/instructions/getMaestroInstructions.ts +59 -30
- package/src/maestro/status/formatMaestroStatus.ts +7 -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 +73 -0
- 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 +7 -21
- package/src/tools/main/run-builder.ts +37 -57
- package/src/tools/main/run-simplifier.ts +172 -0
- package/src/tools/main/run-verifier.ts +34 -58
- 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/roles.ts +2 -1
- package/src/workflow/simplifier/completeSimplifierPass.ts +56 -0
- package/src/workflow/spec/markSpecReady.ts +10 -4
- package/src/workflow/state/readWorkflowState.ts +2 -7
- package/src/workflow/state/schema.ts +9 -10
- package/src/workflow/state/writeWorkflowState.ts +1 -4
- package/src/workflow/transitions.ts +12 -12
- package/src/workflow/utils/assertSpecAcceptanceCriteria.ts +2 -1
- package/src/workflow/utils/getAgentRole.ts +6 -4
- 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
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
7
7
|
import { Type } from 'typebox';
|
|
8
|
+
|
|
8
9
|
import { BuilderAcceptanceCriterionSchema } from '#artifacts/builder-handoff/schema.ts';
|
|
9
10
|
import {
|
|
10
11
|
VERIFIER_HANDOFF_VERSION,
|
|
@@ -57,9 +58,7 @@ export const registerRecordVerifierHandoffTool = (pi: ExtensionAPI): void => {
|
|
|
57
58
|
specId,
|
|
58
59
|
});
|
|
59
60
|
|
|
60
|
-
const currentState = await readWorkflowState(
|
|
61
|
-
paths.getWorkflowPath(specId),
|
|
62
|
-
);
|
|
61
|
+
const currentState = await readWorkflowState(paths.getWorkflowPath(specId));
|
|
63
62
|
|
|
64
63
|
const handoff = {
|
|
65
64
|
version: VERIFIER_HANDOFF_VERSION,
|
|
@@ -11,10 +11,7 @@ export type ResolveWorkflowContextInput = {
|
|
|
11
11
|
specId: string;
|
|
12
12
|
};
|
|
13
13
|
|
|
14
|
-
export const resolveWorkflowContext = async ({
|
|
15
|
-
cwd,
|
|
16
|
-
specId,
|
|
17
|
-
}: ResolveWorkflowContextInput) => {
|
|
14
|
+
export const resolveWorkflowContext = async ({ cwd, specId }: ResolveWorkflowContextInput) => {
|
|
18
15
|
if (!isValidSpecId(specId)) {
|
|
19
16
|
throw new Error(`Invalid spec ID: "${specId}".`);
|
|
20
17
|
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
7
7
|
import { Type } from 'typebox';
|
|
8
|
+
|
|
8
9
|
import maestroSessionState from '#maestro/session/MaestroSessionState.ts';
|
|
9
10
|
import { createSpec } from '#specs/create.ts';
|
|
10
11
|
import { resolveToolRunContext } from '#tools/utils/resolveToolRunContext.ts';
|
|
@@ -13,7 +14,7 @@ export const CREATE_SPEC_TOOL = {
|
|
|
13
14
|
NAME: 'maestro_create_spec',
|
|
14
15
|
LABEL: 'Create Spec',
|
|
15
16
|
DESCRIPTION:
|
|
16
|
-
'Create
|
|
17
|
+
'Create proposal.md and spec.md in drafting-spec from the bundled templates. Add design.md only when significant technical decisions are needed. Review all present documents with the owner before marking the specification ready.',
|
|
17
18
|
} as const;
|
|
18
19
|
|
|
19
20
|
const CreateSpecToolParameters = Type.Object(
|
|
@@ -44,7 +45,7 @@ export const registerCreateSpecTool = (pi: ExtensionAPI): void => {
|
|
|
44
45
|
content: [
|
|
45
46
|
{
|
|
46
47
|
type: 'text',
|
|
47
|
-
text: `Spec ${created.specId} created in drafting-spec phase. Review ${created.specFilePath} with the owner
|
|
48
|
+
text: `Spec ${created.specId} created in drafting-spec phase. Review ${created.proposalFilePath} and ${created.specFilePath} with the owner. Add ${created.designFilePath} only when significant technical decisions are needed; include it in the same approval.`,
|
|
48
49
|
},
|
|
49
50
|
],
|
|
50
51
|
details: created,
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
7
7
|
import { Type } from 'typebox';
|
|
8
|
+
|
|
8
9
|
import { SPEC_ID_PATTERN } from '#ids/isValidSpecId.ts';
|
|
9
10
|
import maestroSessionState from '#maestro/session/MaestroSessionState.ts';
|
|
10
11
|
import { resolveToolRunContext } from '#tools/utils/resolveToolRunContext.ts';
|
|
@@ -14,7 +15,7 @@ export const MARK_SPEC_READY_TOOL = {
|
|
|
14
15
|
NAME: 'maestro_mark_spec_ready',
|
|
15
16
|
LABEL: 'Mark Spec Ready',
|
|
16
17
|
DESCRIPTION:
|
|
17
|
-
'Approve
|
|
18
|
+
'Approve proposal.md and spec.md together with design.md if present, only from drafting-spec after the owner replies GREEN FLAG. Freeze all present documents and move to ready-for-builder. Only the owner reply GREEN FLAG authorizes approval. After this transition succeeds, call maestro_run_builder in the foreground.',
|
|
18
19
|
} as const;
|
|
19
20
|
|
|
20
21
|
const MarkSpecReadyToolParameters = Type.Object(
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { StringEnum } from '@earendil-works/pi-ai';
|
|
7
7
|
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
8
8
|
import { Type } from 'typebox';
|
|
9
|
+
|
|
9
10
|
import { FINDING_DECISIONS } from '#artifacts/verifier-handoff/schema.ts';
|
|
10
11
|
import { SPEC_ID_PATTERN } from '#ids/isValidSpecId.ts';
|
|
11
12
|
import { resolveToolRunContext } from '#tools/utils/resolveToolRunContext.ts';
|
|
@@ -86,9 +87,7 @@ const formatResolution = (details: FindingResolutionDetails): string => {
|
|
|
86
87
|
const summary = `Recorded rejections for ${rejected}. Findings requiring code fixes: ${requiringFix}.`;
|
|
87
88
|
|
|
88
89
|
if (details.phase === WORKFLOW_PHASES.DRAFTING_SPEC) {
|
|
89
|
-
const requiringRevision = formatFindingIds(
|
|
90
|
-
details.findingsRequiringSpecRevisionIds,
|
|
91
|
-
);
|
|
90
|
+
const requiringRevision = formatFindingIds(details.findingsRequiringSpecRevisionIds);
|
|
92
91
|
|
|
93
92
|
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.`;
|
|
94
93
|
}
|
|
@@ -97,7 +96,7 @@ const formatResolution = (details: FindingResolutionDetails): string => {
|
|
|
97
96
|
return `${summary} The workflow is ready-for-builder. Run the builder separately.`;
|
|
98
97
|
}
|
|
99
98
|
|
|
100
|
-
return `${summary} The workflow is
|
|
99
|
+
return `${summary} The workflow is simplifier-running. Call maestro_run_simplifier next with this specId, without another approval or verifier pass.`;
|
|
101
100
|
};
|
|
102
101
|
|
|
103
102
|
export const registerResolveFindingsTool = (pi: ExtensionAPI): void => {
|
|
@@ -106,13 +105,7 @@ export const registerResolveFindingsTool = (pi: ExtensionAPI): void => {
|
|
|
106
105
|
label: RESOLVE_FINDINGS_TOOL.LABEL,
|
|
107
106
|
description: RESOLVE_FINDINGS_TOOL.DESCRIPTION,
|
|
108
107
|
parameters: ResolveFindingsToolParameters,
|
|
109
|
-
async execute(
|
|
110
|
-
_toolCallId,
|
|
111
|
-
{ specId, decisions },
|
|
112
|
-
_signal,
|
|
113
|
-
_onUpdate,
|
|
114
|
-
context,
|
|
115
|
-
) {
|
|
108
|
+
async execute(_toolCallId, { specId, decisions }, _signal, _onUpdate, context) {
|
|
116
109
|
const { paths } = await resolveToolRunContext(context.cwd);
|
|
117
110
|
|
|
118
111
|
const resolved = await resolveFindings({
|
|
@@ -126,20 +119,13 @@ export const registerResolveFindingsTool = (pi: ExtensionAPI): void => {
|
|
|
126
119
|
phase: resolved.state.phase,
|
|
127
120
|
projectRoot: resolved.projectRoot,
|
|
128
121
|
rejectedFindingIds: resolved.findings
|
|
129
|
-
.filter(
|
|
130
|
-
({ decision }) => decision?.decision === FINDING_DECISIONS.REJECT,
|
|
131
|
-
)
|
|
122
|
+
.filter(({ decision }) => decision?.decision === FINDING_DECISIONS.REJECT)
|
|
132
123
|
.map(({ id }) => id),
|
|
133
124
|
findingsRequiringSpecRevisionIds: resolved.findings
|
|
134
|
-
.filter(
|
|
135
|
-
({ decision }) =>
|
|
136
|
-
decision?.decision === FINDING_DECISIONS.REVISE_SPEC,
|
|
137
|
-
)
|
|
125
|
+
.filter(({ decision }) => decision?.decision === FINDING_DECISIONS.REVISE_SPEC)
|
|
138
126
|
.map(({ id }) => id),
|
|
139
127
|
findingsRequiringFixIds: resolved.findings
|
|
140
|
-
.filter(
|
|
141
|
-
({ decision }) => decision?.decision === FINDING_DECISIONS.FIX_CODE,
|
|
142
|
-
)
|
|
128
|
+
.filter(({ decision }) => decision?.decision === FINDING_DECISIONS.FIX_CODE)
|
|
143
129
|
.map(({ id }) => id),
|
|
144
130
|
};
|
|
145
131
|
|
|
@@ -4,9 +4,11 @@
|
|
|
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 { readBuilderHandoff } from '#artifacts/builder-handoff/readBuilderHandoff.ts';
|
|
11
13
|
import {
|
|
12
14
|
BUILDER_HANDOFF_STATUSES,
|
|
@@ -14,8 +16,8 @@ import {
|
|
|
14
16
|
} from '#artifacts/builder-handoff/schema.ts';
|
|
15
17
|
import { AGENTS } from '#config/schema.ts';
|
|
16
18
|
import { SPEC_ID_PATTERN } from '#ids/isValidSpecId.ts';
|
|
17
|
-
import type { MaestroPaths } from '#MaestroPaths.ts';
|
|
18
19
|
import { refreshMaestroStatus } from '#maestro/status/refreshMaestroStatus.ts';
|
|
20
|
+
import type { MaestroPaths } from '#MaestroPaths.ts';
|
|
19
21
|
import {
|
|
20
22
|
assertDelegationResponse,
|
|
21
23
|
waitForDelegationResponse,
|
|
@@ -24,7 +26,7 @@ import { resolveToolRunContext } from '#tools/utils/resolveToolRunContext.ts';
|
|
|
24
26
|
import { prepareBuilderRun } from '#workflow/builder/prepareBuilderRun.ts';
|
|
25
27
|
import { WORKFLOW_ROLES } from '#workflow/roles.ts';
|
|
26
28
|
import { readWorkflowState } from '#workflow/state/readWorkflowState.ts';
|
|
27
|
-
import { WORKFLOW_PHASES
|
|
29
|
+
import { WORKFLOW_PHASES } from '#workflow/state/schema.ts';
|
|
28
30
|
|
|
29
31
|
export const RUN_BUILDER_TOOL = {
|
|
30
32
|
NAME: 'maestro_run_builder',
|
|
@@ -69,76 +71,54 @@ const readBuilderResult = async ({
|
|
|
69
71
|
const state = await readWorkflowState(paths.getWorkflowPath(specId));
|
|
70
72
|
|
|
71
73
|
if (state.specId !== specId) {
|
|
72
|
-
throw new Error(
|
|
73
|
-
`Workflow spec ID mismatch: expected "${specId}", found "${state.specId}".`,
|
|
74
|
-
);
|
|
74
|
+
throw new Error(`Workflow spec ID mismatch: expected "${specId}", found "${state.specId}".`);
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
};
|
|
77
|
+
if (
|
|
78
|
+
state.phase !== WORKFLOW_PHASES.READY_FOR_VERIFIER &&
|
|
79
|
+
state.phase !== WORKFLOW_PHASES.ESCALATION_DECISIONS
|
|
80
|
+
) {
|
|
81
|
+
throw new Error(`Builder returned with unexpected workflow phase "${state.phase}".`);
|
|
82
|
+
}
|
|
84
83
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
};
|
|
84
|
+
const handoff = await readBuilderHandoff({
|
|
85
|
+
path: await paths.getActiveBuilderHandoffPath(specId),
|
|
86
|
+
specId,
|
|
87
|
+
});
|
|
90
88
|
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
);
|
|
103
|
-
}
|
|
89
|
+
const expectedStatus =
|
|
90
|
+
state.phase === WORKFLOW_PHASES.READY_FOR_VERIFIER
|
|
91
|
+
? BUILDER_HANDOFF_STATUSES.DONE
|
|
92
|
+
: BUILDER_HANDOFF_STATUSES.ESCALATION;
|
|
104
93
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
});
|
|
94
|
+
if (handoff.status !== expectedStatus) {
|
|
95
|
+
throw new Error(`Builder handoff status does not match workflow phase "${state.phase}".`);
|
|
96
|
+
}
|
|
109
97
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
98
|
+
if (state.phase === WORKFLOW_PHASES.READY_FOR_VERIFIER) {
|
|
99
|
+
return {
|
|
100
|
+
outcome: BUILDER_HANDOFF_STATUSES.DONE,
|
|
101
|
+
specId,
|
|
102
|
+
phase: state.phase,
|
|
103
|
+
handoff,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
114
106
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
);
|
|
119
|
-
}
|
|
107
|
+
if (handoff.escalations.some(({ resolution }) => resolution !== null)) {
|
|
108
|
+
throw new Error('Current builder escalations already contain an owner resolution.');
|
|
109
|
+
}
|
|
120
110
|
|
|
121
|
-
if (state.phase === WORKFLOW_PHASES.READY_FOR_VERIFIER) {
|
|
122
111
|
return {
|
|
123
|
-
outcome: BUILDER_HANDOFF_STATUSES.
|
|
112
|
+
outcome: BUILDER_HANDOFF_STATUSES.ESCALATION,
|
|
124
113
|
specId,
|
|
125
114
|
phase: state.phase,
|
|
126
115
|
handoff,
|
|
127
116
|
};
|
|
128
|
-
}
|
|
117
|
+
} catch (error) {
|
|
118
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
129
119
|
|
|
130
|
-
|
|
131
|
-
throw new Error(
|
|
132
|
-
'Current builder escalations already contain an owner resolution.',
|
|
133
|
-
);
|
|
120
|
+
throw new Error(`Builder protocol error: ${message}.`, { cause: error });
|
|
134
121
|
}
|
|
135
|
-
|
|
136
|
-
return {
|
|
137
|
-
outcome: BUILDER_HANDOFF_STATUSES.ESCALATION,
|
|
138
|
-
specId,
|
|
139
|
-
phase: state.phase,
|
|
140
|
-
handoff,
|
|
141
|
-
};
|
|
142
122
|
};
|
|
143
123
|
|
|
144
124
|
const formatBuilderResult = (result: BuilderRunResult): string => {
|
|
@@ -0,0 +1,172 @@
|
|
|
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
|
+
|
|
8
|
+
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
9
|
+
import type { SubagentDelegationRequest } from 'pi-subagents/delegation';
|
|
10
|
+
import { Type } from 'typebox';
|
|
11
|
+
|
|
12
|
+
import { readSimplifierHandoff } from '#artifacts/simplifier-handoff/readSimplifierHandoff.ts';
|
|
13
|
+
import { AGENTS } from '#config/schema.ts';
|
|
14
|
+
import { SPEC_ID_PATTERN } from '#ids/isValidSpecId.ts';
|
|
15
|
+
import { refreshMaestroStatus } from '#maestro/status/refreshMaestroStatus.ts';
|
|
16
|
+
import type { MaestroPaths } from '#MaestroPaths.ts';
|
|
17
|
+
import {
|
|
18
|
+
assertDelegationResponse,
|
|
19
|
+
waitForDelegationResponse,
|
|
20
|
+
} from '#tools/utils/pi-subagent-delegation.ts';
|
|
21
|
+
import { resolveToolRunContext } from '#tools/utils/resolveToolRunContext.ts';
|
|
22
|
+
import { WORKFLOW_ROLES } from '#workflow/roles.ts';
|
|
23
|
+
import { readWorkflowState } from '#workflow/state/readWorkflowState.ts';
|
|
24
|
+
import { WORKFLOW_EVENTS, WORKFLOW_PHASES } from '#workflow/state/schema.ts';
|
|
25
|
+
import { writeWorkflowState } from '#workflow/state/writeWorkflowState.ts';
|
|
26
|
+
import { transitionWorkflow } from '#workflow/transitions.ts';
|
|
27
|
+
|
|
28
|
+
export const RUN_SIMPLIFIER_TOOL = {
|
|
29
|
+
NAME: 'maestro_run_simplifier',
|
|
30
|
+
LABEL: 'Run Simplifier',
|
|
31
|
+
DESCRIPTION:
|
|
32
|
+
'Run the simplifier in the foreground from simplifier-running. If Git readiness cannot be confirmed, warn and complete without delegation.',
|
|
33
|
+
} as const;
|
|
34
|
+
|
|
35
|
+
const RunSimplifierToolParameters = Type.Object(
|
|
36
|
+
{ specId: Type.String({ pattern: SPEC_ID_PATTERN.source }) },
|
|
37
|
+
{ additionalProperties: false },
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const MILLISECONDS_PER_MINUTE = 60_000;
|
|
41
|
+
|
|
42
|
+
const GIT_SKIP_WARNING = 'Simplifier skipped: Git repository unavailable.';
|
|
43
|
+
|
|
44
|
+
type GitReadinessInput = {
|
|
45
|
+
pi: ExtensionAPI;
|
|
46
|
+
projectRoot: string;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const isGitReady = async ({ pi, projectRoot }: GitReadinessInput): Promise<boolean> => {
|
|
50
|
+
try {
|
|
51
|
+
const result = await pi.exec('git', ['rev-parse', '--is-inside-work-tree'], {
|
|
52
|
+
cwd: projectRoot,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
return result.code === 0 && result.stdout.trim() === 'true';
|
|
56
|
+
} catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
type ReadSimplifierResultInput = {
|
|
62
|
+
paths: MaestroPaths;
|
|
63
|
+
specId: string;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const readSimplifierResult = async ({ paths, specId }: ReadSimplifierResultInput) => {
|
|
67
|
+
try {
|
|
68
|
+
const state = await readWorkflowState(paths.getWorkflowPath(specId));
|
|
69
|
+
|
|
70
|
+
if (state.specId !== specId) {
|
|
71
|
+
throw new Error(`Workflow spec ID mismatch: expected "${specId}", found "${state.specId}".`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (state.phase !== WORKFLOW_PHASES.CANDIDATE_READY) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
`The simplifier returned without recording a valid terminal handoff. Workflow phase is "${state.phase}".`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const handoff = await readSimplifierHandoff({
|
|
81
|
+
path: paths.getSimplifierHandoffPath(specId),
|
|
82
|
+
specId,
|
|
83
|
+
projectRoot: paths.getProjectRoot(),
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
return { specId, phase: state.phase, outcome: state.phase, handoff };
|
|
87
|
+
} catch (error) {
|
|
88
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
89
|
+
throw new Error(`Simplifier protocol error: ${message}`, { cause: error });
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export const registerRunSimplifierTool = (pi: ExtensionAPI): void => {
|
|
94
|
+
pi.registerTool({
|
|
95
|
+
name: RUN_SIMPLIFIER_TOOL.NAME,
|
|
96
|
+
label: RUN_SIMPLIFIER_TOOL.LABEL,
|
|
97
|
+
description: RUN_SIMPLIFIER_TOOL.DESCRIPTION,
|
|
98
|
+
parameters: RunSimplifierToolParameters,
|
|
99
|
+
async execute(toolCallId, { specId }, _signal, _onUpdate, context) {
|
|
100
|
+
const { paths, config, projectRoot } = await resolveToolRunContext(context.cwd);
|
|
101
|
+
|
|
102
|
+
const workflowPath = paths.getWorkflowPath(specId);
|
|
103
|
+
const state = await readWorkflowState(workflowPath);
|
|
104
|
+
|
|
105
|
+
if (state.specId !== specId) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
`Workflow spec ID mismatch: expected "${specId}", found "${state.specId}".`,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (state.phase !== WORKFLOW_PHASES.SIMPLIFIER_RUNNING) {
|
|
112
|
+
throw new Error(
|
|
113
|
+
`Simplifier run requires simplifier-running state, found "${state.phase}".`,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
await refreshMaestroStatus(context);
|
|
118
|
+
|
|
119
|
+
if (!(await isGitReady({ pi, projectRoot }))) {
|
|
120
|
+
const nextState = transitionWorkflow({
|
|
121
|
+
state,
|
|
122
|
+
event: WORKFLOW_EVENTS.SIMPLIFIER_SKIPPED,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
await writeWorkflowState({ path: workflowPath, state: nextState });
|
|
126
|
+
context.ui.notify(GIT_SKIP_WARNING, 'warning');
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
content: [{ type: 'text', text: GIT_SKIP_WARNING }],
|
|
130
|
+
details: {
|
|
131
|
+
specId,
|
|
132
|
+
phase: nextState.phase,
|
|
133
|
+
skipped: true,
|
|
134
|
+
warning: GIT_SKIP_WARNING,
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const request: SubagentDelegationRequest = {
|
|
140
|
+
requestId: randomUUID(),
|
|
141
|
+
ownerRunId: toolCallId,
|
|
142
|
+
nodeId: WORKFLOW_ROLES.SIMPLIFIER,
|
|
143
|
+
agent: AGENTS.SIMPLIFIER,
|
|
144
|
+
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.`,
|
|
145
|
+
context: 'fresh',
|
|
146
|
+
cwd: projectRoot,
|
|
147
|
+
model: config.simplifier.model,
|
|
148
|
+
thinking: config.simplifier.thinking,
|
|
149
|
+
timeoutMs: config.simplifier.timeoutMinutes * MILLISECONDS_PER_MINUTE,
|
|
150
|
+
result: { kind: 'text' },
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const response = await waitForDelegationResponse({
|
|
154
|
+
piEventsBus: pi.events,
|
|
155
|
+
request,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
assertDelegationResponse(response);
|
|
159
|
+
const result = await readSimplifierResult({ paths, specId });
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
content: [
|
|
163
|
+
{
|
|
164
|
+
type: 'text',
|
|
165
|
+
text: `Simplifier completed spec ${specId}. The workflow is candidate-ready. Report the saved simplifier summary and notes as its report, not verifier evidence.`,
|
|
166
|
+
},
|
|
167
|
+
],
|
|
168
|
+
details: result,
|
|
169
|
+
};
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
};
|
|
@@ -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 = {
|
|
@@ -40,7 +42,7 @@ const RunVerifierToolParameters = Type.Object(
|
|
|
40
42
|
const MILLISECONDS_PER_MINUTE = 60_000;
|
|
41
43
|
|
|
42
44
|
type VerifierTerminalPhase =
|
|
43
|
-
| typeof WORKFLOW_PHASES.
|
|
45
|
+
| typeof WORKFLOW_PHASES.SIMPLIFIER_RUNNING
|
|
44
46
|
| typeof WORKFLOW_PHASES.FINDING_DECISIONS;
|
|
45
47
|
|
|
46
48
|
type VerifierRunResult = {
|
|
@@ -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.CANDIDATE_READY &&
|
|
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.CANDIDATE_READY;
|
|
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
|
|
|
@@ -131,8 +107,8 @@ const readVerifierResult = async ({
|
|
|
131
107
|
};
|
|
132
108
|
|
|
133
109
|
const formatVerifierResult = (result: VerifierRunResult): string => {
|
|
134
|
-
if (result.outcome === WORKFLOW_PHASES.
|
|
135
|
-
return `Verifier completed spec ${result.specId}. The workflow is
|
|
110
|
+
if (result.outcome === WORKFLOW_PHASES.SIMPLIFIER_RUNNING) {
|
|
111
|
+
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.`;
|
|
136
112
|
}
|
|
137
113
|
|
|
138
114
|
return `Verifier found ${result.handoff.findings.length} finding(s) for spec ${result.specId}. The workflow is waiting for owner decisions.`;
|
|
@@ -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
|
}
|