@emiliosp/pi-maestro 0.1.1 → 0.2.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 +5 -5
- package/agents/verifier.md +1 -1
- package/docs/configuration.md +7 -18
- package/docs/subagent-integration.md +15 -14
- package/docs/workflow.md +64 -83
- package/extensions/maestro.ts +12 -12
- package/package.json +6 -5
- package/src/config/loadConfiguration.ts +11 -76
- package/src/maestro/instructions/getMaestroInstructions.ts +2 -2
- package/src/maestro/session/MaestroSessionState.ts +13 -0
- package/src/maestro/status/refreshMaestroStatus.ts +2 -2
- package/src/tools/child/record-verifier-handoff.ts +11 -7
- package/src/tools/child/utils/resolveWorkflowContext.ts +2 -2
- package/src/tools/main/create-spec.ts +2 -2
- package/src/tools/main/mark-spec-ready.ts +4 -4
- package/src/tools/main/resolve-escalation.ts +4 -4
- package/src/tools/main/resolve-findings.ts +3 -3
- package/src/tools/main/{launch-builder.ts → run-builder.ts} +26 -26
- package/src/tools/main/{launch-verifier.ts → run-verifier.ts} +67 -62
- package/src/tools/utils/pi-subagent-delegation.ts +1 -1
- package/src/tools/utils/{resolveToolLaunchContext.ts → resolveToolRunContext.ts} +3 -3
- package/src/workflow/builder/assertBuilderProtocolUnchanged.ts +3 -1
- package/src/workflow/builder/{prepareBuilderLauncher.ts → prepareBuilderRun.ts} +14 -14
- package/src/workflow/state/schema.ts +2 -3
- package/src/workflow/transitions.ts +2 -6
- package/src/workflow/verifier/{prepareVerifierLaunch.ts → prepareVerifierRun.ts} +11 -23
|
@@ -3,15 +3,13 @@
|
|
|
3
3
|
* Used: When Maestro initializes for a repository.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { readFile, realpath } from 'node:fs/promises';
|
|
7
|
+
import { isAbsolute, join, resolve } from 'node:path';
|
|
8
8
|
import { assertConfiguration } from '#config/assertConfiguration.ts';
|
|
9
9
|
import { CONFIG_FILE_PATH, DEFAULT_CONFIG } from '#config/defaults.ts';
|
|
10
10
|
import type { MaestroConfig, PartialMaestroConfig } from '#config/schema.ts';
|
|
11
|
-
import { isErrnoException } from '#utils/is-errno-exception.ts';
|
|
12
11
|
import { pathExists } from '#utils/path-exists.ts';
|
|
13
12
|
import { isPathStrictlyWithin } from '#utils/path-strictly-within.ts';
|
|
14
|
-
import { isPathWithinOrEqual } from '#utils/path-within-or-equal.ts';
|
|
15
13
|
|
|
16
14
|
const resolveConfiguration = (input: PartialMaestroConfig): MaestroConfig => {
|
|
17
15
|
const resolved: MaestroConfig = {
|
|
@@ -35,43 +33,6 @@ const resolveConfiguration = (input: PartialMaestroConfig): MaestroConfig => {
|
|
|
35
33
|
return resolved;
|
|
36
34
|
};
|
|
37
35
|
|
|
38
|
-
type ExistingAncestor = {
|
|
39
|
-
path: string;
|
|
40
|
-
realPath: string;
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
const findExistingAncestor = async (
|
|
44
|
-
path: string,
|
|
45
|
-
): Promise<ExistingAncestor> => {
|
|
46
|
-
let anchestor = path;
|
|
47
|
-
|
|
48
|
-
while (true) {
|
|
49
|
-
try {
|
|
50
|
-
// lstat instead of access, because access follows symlinks
|
|
51
|
-
await lstat(anchestor);
|
|
52
|
-
} catch (error) {
|
|
53
|
-
if (!isErrnoException(error) || error.code !== 'ENOENT') {
|
|
54
|
-
throw error;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const parent = dirname(anchestor);
|
|
58
|
-
|
|
59
|
-
if (parent === anchestor) {
|
|
60
|
-
throw new Error(`Cannot resolve an existing ancestor for "${path}".`);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
anchestor = parent;
|
|
64
|
-
continue;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
try {
|
|
68
|
-
return { path: anchestor, realPath: await realpath(anchestor) };
|
|
69
|
-
} catch {
|
|
70
|
-
throw new Error(`Cannot resolve symlink "${anchestor}".`);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
};
|
|
74
|
-
|
|
75
36
|
type AssertSafeDirectoryInput = {
|
|
76
37
|
value: string;
|
|
77
38
|
name: string;
|
|
@@ -96,11 +57,11 @@ type ResolveSafeDirectoryInput = {
|
|
|
96
57
|
name: string;
|
|
97
58
|
};
|
|
98
59
|
|
|
99
|
-
const resolveSafeDirectory =
|
|
60
|
+
const resolveSafeDirectory = ({
|
|
100
61
|
repositoryRoot,
|
|
101
62
|
directory,
|
|
102
63
|
name,
|
|
103
|
-
}: ResolveSafeDirectoryInput):
|
|
64
|
+
}: ResolveSafeDirectoryInput): string => {
|
|
104
65
|
assertSafeDirectoryInput({ value: directory, name });
|
|
105
66
|
|
|
106
67
|
const requestedDirectory = resolve(repositoryRoot, directory);
|
|
@@ -118,31 +79,7 @@ const resolveSafeDirectory = async ({
|
|
|
118
79
|
throw new Error(`${name} must stay inside the Git root.`);
|
|
119
80
|
}
|
|
120
81
|
|
|
121
|
-
|
|
122
|
-
const ancestor = await findExistingAncestor(requestedDirectory);
|
|
123
|
-
|
|
124
|
-
if (
|
|
125
|
-
!isPathWithinOrEqual({
|
|
126
|
-
parent: repositoryRoot,
|
|
127
|
-
candidate: ancestor.realPath,
|
|
128
|
-
})
|
|
129
|
-
) {
|
|
130
|
-
throw new Error(`${name} resolves outside the Git root through a symlink.`);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const unresolvedSuffix = relative(ancestor.path, requestedDirectory);
|
|
134
|
-
const resolvedDirectory = resolve(ancestor.realPath, unresolvedSuffix);
|
|
135
|
-
|
|
136
|
-
if (
|
|
137
|
-
!isPathStrictlyWithin({
|
|
138
|
-
parent: repositoryRoot,
|
|
139
|
-
candidate: resolvedDirectory,
|
|
140
|
-
})
|
|
141
|
-
) {
|
|
142
|
-
throw new Error(`${name} resolves outside the Git root through a symlink.`);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
return resolvedDirectory;
|
|
82
|
+
return requestedDirectory;
|
|
146
83
|
};
|
|
147
84
|
|
|
148
85
|
type ResolveDirectoriesInput = {
|
|
@@ -150,13 +87,11 @@ type ResolveDirectoriesInput = {
|
|
|
150
87
|
config: MaestroConfig;
|
|
151
88
|
};
|
|
152
89
|
|
|
153
|
-
const resolveDirectories =
|
|
154
|
-
repositoryRoot
|
|
90
|
+
const resolveDirectories = ({
|
|
91
|
+
repositoryRoot,
|
|
155
92
|
config,
|
|
156
|
-
}: ResolveDirectoriesInput):
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
const specDirectory = await resolveSafeDirectory({
|
|
93
|
+
}: ResolveDirectoriesInput): MaestroConfig => {
|
|
94
|
+
const specDirectory = resolveSafeDirectory({
|
|
160
95
|
repositoryRoot,
|
|
161
96
|
directory: config.specDirectory,
|
|
162
97
|
name: 'specDirectory',
|
|
@@ -186,7 +121,7 @@ export const loadConfiguration = async (
|
|
|
186
121
|
const targetPath = join(repositoryRoot, CONFIG_FILE_PATH);
|
|
187
122
|
|
|
188
123
|
if (!(await pathExists(targetPath))) {
|
|
189
|
-
return
|
|
124
|
+
return resolveDirectories({ repositoryRoot, config: DEFAULT_CONFIG });
|
|
190
125
|
}
|
|
191
126
|
|
|
192
127
|
const parsed = await readConfigurationFile(targetPath);
|
|
@@ -194,5 +129,5 @@ export const loadConfiguration = async (
|
|
|
194
129
|
assertConfiguration(parsed);
|
|
195
130
|
const config = resolveConfiguration(parsed);
|
|
196
131
|
|
|
197
|
-
return
|
|
132
|
+
return resolveDirectories({ repositoryRoot, config });
|
|
198
133
|
};
|
|
@@ -15,11 +15,11 @@ A builder escalation is an owner decision checkpoint, not only a technical failu
|
|
|
15
15
|
|
|
16
16
|
If the approved contract must change from escalation-decision or findings-decision, the owner revises and approves the same specId, then calls maestro_mark_spec_ready. If a builder reports failed for a technical reason, Maestro reports the error and stops the workflow. The owner is responsible for the follow-up; there is no retry or spec revision from builder-failed.
|
|
17
17
|
|
|
18
|
-
Use the deterministic maestro_* tools for workflow mutations, Git operations, artifact changes, and agent
|
|
18
|
+
Use the deterministic maestro_* tools for workflow mutations, Git operations, artifact changes, and agent runs. Do not perform these mutations through generic tools.
|
|
19
19
|
|
|
20
20
|
While Maestro mode is active, use generic tools to inspect and discuss the repository, but do not edit normal product files. Builder and verifier work through their dedicated handoff tools and do not communicate with the owner directly. Do not treat their conclusions as owner decisions.
|
|
21
21
|
|
|
22
|
-
The verifier uses the
|
|
22
|
+
The verifier uses the verifier-running checkpoint itself as its fixed candidate and must restore product changes before its handoff. The verifier handoff tool owns the protocol commit. The verifier handoff and finding resolution operations own the checks required to reach candidate-ready.
|
|
23
23
|
|
|
24
24
|
The workflow ends at candidate-ready. No final tool call, checkpoint, or owner commit is required. You own the final summary. Use inspection tools to read the existing artifacts and Git information. Summarize the changes, verification results, rejected findings with their reasons, and applicable builder notes. Include the current branch and final HEAD after the protocol commit as the Pull Request facts. Do not present owner rejections as passed verification.
|
|
25
25
|
|
|
@@ -9,6 +9,7 @@ class MaestroSessionState {
|
|
|
9
9
|
private active = false;
|
|
10
10
|
private activeSpecId: string | null = null;
|
|
11
11
|
private specSha256: string | null = null;
|
|
12
|
+
private verifierCheckpointCommit: string | null = null;
|
|
12
13
|
|
|
13
14
|
public isActive = (): boolean => this.active;
|
|
14
15
|
|
|
@@ -34,6 +35,17 @@ class MaestroSessionState {
|
|
|
34
35
|
this.specSha256 = await getFileSha256(specPath);
|
|
35
36
|
};
|
|
36
37
|
|
|
38
|
+
public getVerifierCheckpointCommit = (): string | null =>
|
|
39
|
+
this.verifierCheckpointCommit;
|
|
40
|
+
|
|
41
|
+
public setVerifierCheckpointCommit = (commit: string): void => {
|
|
42
|
+
this.verifierCheckpointCommit = commit;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
public clearVerifierCheckpointCommit = (): void => {
|
|
46
|
+
this.verifierCheckpointCommit = null;
|
|
47
|
+
};
|
|
48
|
+
|
|
37
49
|
public activate = (): void => {
|
|
38
50
|
this.active = true;
|
|
39
51
|
};
|
|
@@ -41,6 +53,7 @@ class MaestroSessionState {
|
|
|
41
53
|
public clearActiveSpecId = (): void => {
|
|
42
54
|
this.activeSpecId = null;
|
|
43
55
|
this.specSha256 = null;
|
|
56
|
+
this.clearVerifierCheckpointCommit();
|
|
44
57
|
};
|
|
45
58
|
|
|
46
59
|
public deactivate = (): void => {
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import type { ExtensionContext } from '@earendil-works/pi-coding-agent';
|
|
7
7
|
import maestroSessionState from '#maestro/session/MaestroSessionState.ts';
|
|
8
8
|
import { formatMaestroStatus } from '#maestro/status/formatMaestroStatus.ts';
|
|
9
|
-
import {
|
|
9
|
+
import { resolveToolRunContext } from '#tools/utils/resolveToolRunContext.ts';
|
|
10
10
|
import { readWorkflowState } from '#workflow/state/readWorkflowState.ts';
|
|
11
11
|
|
|
12
12
|
export const MAESTRO_STATUS_KEY = 'maestro';
|
|
@@ -32,7 +32,7 @@ export const refreshMaestroStatus = async (
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
try {
|
|
35
|
-
const { paths } = await
|
|
35
|
+
const { paths } = await resolveToolRunContext(context.cwd);
|
|
36
36
|
|
|
37
37
|
const state = await readWorkflowState(paths.getWorkflowPath(specId));
|
|
38
38
|
|
|
@@ -11,10 +11,9 @@ import {
|
|
|
11
11
|
VerifierFindingSchema,
|
|
12
12
|
} from '#artifacts/verifier-handoff/schema.ts';
|
|
13
13
|
import { createWorkflowCheckpointCommit } from '#git/commits/createWorkflowCheckpointCommit.ts';
|
|
14
|
-
import { getParentCommit } from '#git/history/getParentCommit.ts';
|
|
15
|
-
import { getHeadCommit } from '#git/repository/getHeadCommit.ts';
|
|
16
14
|
import { getRepositoryStatus } from '#git/repository/getRepositoryStatus.ts';
|
|
17
15
|
import { SPEC_ID_PATTERN } from '#ids/isValidSpecId.ts';
|
|
16
|
+
import maestroSessionState from '#maestro/session/MaestroSessionState.ts';
|
|
18
17
|
import { resolveWorkflowContext } from '#tools/child/utils/resolveWorkflowContext.ts';
|
|
19
18
|
import { readWorkflowState } from '#workflow/state/readWorkflowState.ts';
|
|
20
19
|
import {
|
|
@@ -55,6 +54,14 @@ const isVerifierPassRejection = (
|
|
|
55
54
|
result: VerifierPassResult,
|
|
56
55
|
): result is VerifierPassRejection => 'error' in result;
|
|
57
56
|
|
|
57
|
+
function assertVerifierCheckpointCommit(
|
|
58
|
+
commit: string | null,
|
|
59
|
+
): asserts commit is string {
|
|
60
|
+
if (commit === null) {
|
|
61
|
+
throw new Error('Verifier handoff requires a live verifier checkpoint.');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
58
65
|
export const registerRecordVerifierHandoffTool = (pi: ExtensionAPI): void => {
|
|
59
66
|
pi.registerTool({
|
|
60
67
|
name: VERIFIER_HANDOFF_TOOL.NAME,
|
|
@@ -73,12 +80,9 @@ export const registerRecordVerifierHandoffTool = (pi: ExtensionAPI): void => {
|
|
|
73
80
|
paths.getWorkflowPath(specId),
|
|
74
81
|
);
|
|
75
82
|
|
|
76
|
-
const
|
|
83
|
+
const candidateCommit = maestroSessionState.getVerifierCheckpointCommit();
|
|
77
84
|
|
|
78
|
-
|
|
79
|
-
repositoryRoot,
|
|
80
|
-
commit: headCommit,
|
|
81
|
-
});
|
|
85
|
+
assertVerifierCheckpointCommit(candidateCommit);
|
|
82
86
|
|
|
83
87
|
const handoff = {
|
|
84
88
|
version: VERIFIER_HANDOFF_VERSION,
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { isValidSpecId } from '#ids/isValidSpecId.ts';
|
|
7
|
-
import {
|
|
7
|
+
import { resolveToolRunContext } from '#tools/utils/resolveToolRunContext.ts';
|
|
8
8
|
|
|
9
9
|
export type ResolveWorkflowContextInput = {
|
|
10
10
|
cwd: string;
|
|
@@ -19,7 +19,7 @@ export const resolveWorkflowContext = async ({
|
|
|
19
19
|
throw new Error(`Invalid spec ID: "${specId}".`);
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
const { paths, repositoryRoot } = await
|
|
22
|
+
const { paths, repositoryRoot } = await resolveToolRunContext(cwd);
|
|
23
23
|
|
|
24
24
|
return { paths, specId, repositoryRoot };
|
|
25
25
|
};
|
|
@@ -7,7 +7,7 @@ import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
|
7
7
|
import { Type } from 'typebox';
|
|
8
8
|
import maestroSessionState from '#maestro/session/MaestroSessionState.ts';
|
|
9
9
|
import { createSpec } from '#specs/create.ts';
|
|
10
|
-
import {
|
|
10
|
+
import { resolveToolRunContext } from '#tools/utils/resolveToolRunContext.ts';
|
|
11
11
|
|
|
12
12
|
export const CREATE_SPEC_TOOL = {
|
|
13
13
|
NAME: 'maestro_create_spec',
|
|
@@ -30,7 +30,7 @@ export const registerCreateSpecTool = (pi: ExtensionAPI): void => {
|
|
|
30
30
|
description: CREATE_SPEC_TOOL.DESCRIPTION,
|
|
31
31
|
parameters: CreateSpecToolParameters,
|
|
32
32
|
async execute(_toolCallId, { title }, _signal, _onUpdate, context) {
|
|
33
|
-
const { paths } = await
|
|
33
|
+
const { paths } = await resolveToolRunContext(context.cwd);
|
|
34
34
|
|
|
35
35
|
const created = await createSpec({
|
|
36
36
|
paths,
|
|
@@ -7,14 +7,14 @@ import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
|
7
7
|
import { Type } from 'typebox';
|
|
8
8
|
import { SPEC_ID_PATTERN } from '#ids/isValidSpecId.ts';
|
|
9
9
|
import maestroSessionState from '#maestro/session/MaestroSessionState.ts';
|
|
10
|
-
import {
|
|
10
|
+
import { resolveToolRunContext } from '#tools/utils/resolveToolRunContext.ts';
|
|
11
11
|
import { markSpecReady } from '#workflow/spec/markSpecReady.ts';
|
|
12
12
|
|
|
13
13
|
export const MARK_SPEC_READY_TOOL = {
|
|
14
14
|
NAME: 'maestro_mark_spec_ready',
|
|
15
15
|
LABEL: 'Mark Spec Ready',
|
|
16
16
|
DESCRIPTION:
|
|
17
|
-
'Approve the current spec.md after explicit owner approval and move the workflow to ready-for-builder. The owner must commit spec.md and workflow.json before
|
|
17
|
+
'Approve the current spec.md after explicit owner approval and move the workflow to ready-for-builder. The owner must commit spec.md and workflow.json before running the builder.',
|
|
18
18
|
} as const;
|
|
19
19
|
|
|
20
20
|
const MarkSpecReadyToolParameters = Type.Object(
|
|
@@ -31,7 +31,7 @@ export const registerMarkSpecReadyTool = (pi: ExtensionAPI): void => {
|
|
|
31
31
|
description: MARK_SPEC_READY_TOOL.DESCRIPTION,
|
|
32
32
|
parameters: MarkSpecReadyToolParameters,
|
|
33
33
|
async execute(_toolCallId, { specId }, _signal, _onUpdate, context) {
|
|
34
|
-
const { paths } = await
|
|
34
|
+
const { paths } = await resolveToolRunContext(context.cwd);
|
|
35
35
|
|
|
36
36
|
const state = await markSpecReady({
|
|
37
37
|
paths,
|
|
@@ -43,7 +43,7 @@ export const registerMarkSpecReadyTool = (pi: ExtensionAPI): void => {
|
|
|
43
43
|
content: [
|
|
44
44
|
{
|
|
45
45
|
type: 'text',
|
|
46
|
-
text: `Spec ${state.specId} is approved at workflow revision ${state.revision} and ready-for-builder. Commit spec.md and workflow.json before
|
|
46
|
+
text: `Spec ${state.specId} is approved at workflow revision ${state.revision} and ready-for-builder. Commit spec.md and workflow.json before running the builder.`,
|
|
47
47
|
},
|
|
48
48
|
],
|
|
49
49
|
details: state,
|
|
@@ -10,14 +10,14 @@ import {
|
|
|
10
10
|
EscalationResolutionSchema,
|
|
11
11
|
} from '#artifacts/escalation/schema.ts';
|
|
12
12
|
import { SPEC_ID_PATTERN } from '#ids/isValidSpecId.ts';
|
|
13
|
-
import {
|
|
13
|
+
import { resolveToolRunContext } from '#tools/utils/resolveToolRunContext.ts';
|
|
14
14
|
import { resolveBuilderEscalation } from '#workflow/escalation/resolveBuilderEscalation.ts';
|
|
15
15
|
|
|
16
16
|
export const RESOLVE_ESCALATION_TOOL = {
|
|
17
17
|
NAME: 'maestro_resolve_escalation',
|
|
18
18
|
LABEL: 'Resolve Escalation',
|
|
19
19
|
DESCRIPTION:
|
|
20
|
-
'Record the explicit owner decision for the current builder escalation when the approved contract remains valid. Commit the resolution and workflow transition on the current branch, then return ready-for-builder without
|
|
20
|
+
'Record the explicit owner decision for the current builder escalation when the approved contract remains valid. Commit the resolution and workflow transition on the current branch, then return ready-for-builder without running the builder. If the contract must change, edit spec.md and use maestro_mark_spec_ready instead.',
|
|
21
21
|
} as const;
|
|
22
22
|
|
|
23
23
|
const ResolveEscalationToolParameters = Type.Object(
|
|
@@ -37,7 +37,7 @@ export const registerResolveEscalationTool = (pi: ExtensionAPI): void => {
|
|
|
37
37
|
parameters: ResolveEscalationToolParameters,
|
|
38
38
|
async execute(_toolCallId, params, _signal, _onUpdate, context) {
|
|
39
39
|
const { specId, escalationId, ...resolution } = params;
|
|
40
|
-
const { paths } = await
|
|
40
|
+
const { paths } = await resolveToolRunContext(context.cwd);
|
|
41
41
|
|
|
42
42
|
const resolved = await resolveBuilderEscalation({
|
|
43
43
|
paths,
|
|
@@ -50,7 +50,7 @@ export const registerResolveEscalationTool = (pi: ExtensionAPI): void => {
|
|
|
50
50
|
content: [
|
|
51
51
|
{
|
|
52
52
|
type: 'text',
|
|
53
|
-
text: `Escalation ${resolved.escalation.id} resolved. Spec ${resolved.state.specId} is ready-for-builder.
|
|
53
|
+
text: `Escalation ${resolved.escalation.id} resolved. Spec ${resolved.state.specId} is ready-for-builder. Run the builder separately.`,
|
|
54
54
|
},
|
|
55
55
|
],
|
|
56
56
|
details: {
|
|
@@ -7,7 +7,7 @@ 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
|
import { SPEC_ID_PATTERN } from '#ids/isValidSpecId.ts';
|
|
10
|
-
import {
|
|
10
|
+
import { resolveToolRunContext } from '#tools/utils/resolveToolRunContext.ts';
|
|
11
11
|
import {
|
|
12
12
|
FINDING_DECISIONS,
|
|
13
13
|
resolveFindings,
|
|
@@ -73,7 +73,7 @@ const formatResolution = (details: FindingResolutionDetails): string => {
|
|
|
73
73
|
const requiringFix = formatFindingIds(details.findingsRequiringFixIds);
|
|
74
74
|
|
|
75
75
|
if (details.phase === WORKFLOW_PHASES.READY_FOR_BUILDER) {
|
|
76
|
-
return `Recorded rejections for ${rejected}. Findings requiring code fixes: ${requiringFix}. The workflow is ready-for-builder.
|
|
76
|
+
return `Recorded rejections for ${rejected}. Findings requiring code fixes: ${requiringFix}. The workflow is ready-for-builder. Run the builder separately.`;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
return `Recorded rejections for ${rejected}. Findings requiring code fixes: ${requiringFix}. The workflow is candidate-ready.`;
|
|
@@ -92,7 +92,7 @@ export const registerResolveFindingsTool = (pi: ExtensionAPI): void => {
|
|
|
92
92
|
_onUpdate,
|
|
93
93
|
context,
|
|
94
94
|
) {
|
|
95
|
-
const { paths } = await
|
|
95
|
+
const { paths } = await resolveToolRunContext(context.cwd);
|
|
96
96
|
|
|
97
97
|
const resolved = await resolveFindings({
|
|
98
98
|
paths,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Objective:
|
|
3
|
-
* Used: When the owner invokes the
|
|
2
|
+
* Objective: Run and validate one foreground builder pass for the owner session.
|
|
3
|
+
* Used: When the owner invokes the run-builder tool.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { randomUUID } from 'node:crypto';
|
|
@@ -22,20 +22,20 @@ import {
|
|
|
22
22
|
assertDelegationResponse,
|
|
23
23
|
waitForDelegationResponse,
|
|
24
24
|
} from '#tools/utils/pi-subagent-delegation.ts';
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
25
|
+
import { resolveToolRunContext } from '#tools/utils/resolveToolRunContext.ts';
|
|
26
|
+
import { prepareBuilderRun } from '#workflow/builder/prepareBuilderRun.ts';
|
|
27
27
|
import { WORKFLOW_ROLES } from '#workflow/roles.ts';
|
|
28
28
|
import { readWorkflowState } from '#workflow/state/readWorkflowState.ts';
|
|
29
29
|
import { WORKFLOW_PHASES, type WorkflowState } from '#workflow/state/schema.ts';
|
|
30
30
|
|
|
31
|
-
export const
|
|
32
|
-
NAME: '
|
|
33
|
-
LABEL: '
|
|
31
|
+
export const RUN_BUILDER_TOOL = {
|
|
32
|
+
NAME: 'maestro_run_builder',
|
|
33
|
+
LABEL: 'Run Builder',
|
|
34
34
|
DESCRIPTION:
|
|
35
|
-
'
|
|
35
|
+
'Run the builder in the current checkout for an owner-approved spec. The builder runs in the foreground and cannot be retried after a committed failure.',
|
|
36
36
|
} as const;
|
|
37
37
|
|
|
38
|
-
const
|
|
38
|
+
const RunBuilderToolParameters = Type.Object(
|
|
39
39
|
{
|
|
40
40
|
specId: Type.String({ pattern: SPEC_ID_PATTERN.source }),
|
|
41
41
|
},
|
|
@@ -44,7 +44,7 @@ const LaunchBuilderToolParameters = Type.Object(
|
|
|
44
44
|
|
|
45
45
|
const MILLISECONDS_PER_MINUTE = 60_000;
|
|
46
46
|
|
|
47
|
-
type
|
|
47
|
+
type BuilderRunResult =
|
|
48
48
|
| {
|
|
49
49
|
outcome: typeof BUILDER_HANDOFF_STATUSES.DONE;
|
|
50
50
|
specId: string;
|
|
@@ -75,7 +75,7 @@ type ReadCommittedBuilderResultInput = {
|
|
|
75
75
|
const readCommittedBuilderResult = async ({
|
|
76
76
|
paths,
|
|
77
77
|
specId,
|
|
78
|
-
}: ReadCommittedBuilderResultInput): Promise<
|
|
78
|
+
}: ReadCommittedBuilderResultInput): Promise<BuilderRunResult> => {
|
|
79
79
|
try {
|
|
80
80
|
const repositoryStatus = await getRepositoryStatus(
|
|
81
81
|
paths.getRepositoryRoot(),
|
|
@@ -95,7 +95,7 @@ const readCommittedBuilderResult = async ({
|
|
|
95
95
|
);
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
return await
|
|
98
|
+
return await buildRunResult({ paths, specId, state });
|
|
99
99
|
} catch (error) {
|
|
100
100
|
const message = error instanceof Error ? error.message : String(error);
|
|
101
101
|
|
|
@@ -117,7 +117,7 @@ const readTerminalBuilderResult = async ({
|
|
|
117
117
|
specId,
|
|
118
118
|
revision,
|
|
119
119
|
phase,
|
|
120
|
-
}: ReadTerminalBuilderResultInput): Promise<
|
|
120
|
+
}: ReadTerminalBuilderResultInput): Promise<BuilderRunResult> => {
|
|
121
121
|
const handoff = await readBuilderHandoff({
|
|
122
122
|
path: paths.getBuilderHandoffPath(specId),
|
|
123
123
|
specId,
|
|
@@ -154,17 +154,17 @@ const readTerminalBuilderResult = async ({
|
|
|
154
154
|
};
|
|
155
155
|
};
|
|
156
156
|
|
|
157
|
-
type
|
|
157
|
+
type BuildRunResultInput = {
|
|
158
158
|
paths: MaestroPaths;
|
|
159
159
|
specId: string;
|
|
160
160
|
state: WorkflowState;
|
|
161
161
|
};
|
|
162
162
|
|
|
163
|
-
const
|
|
163
|
+
const buildRunResult = async ({
|
|
164
164
|
paths,
|
|
165
165
|
specId,
|
|
166
166
|
state,
|
|
167
|
-
}:
|
|
167
|
+
}: BuildRunResultInput): Promise<BuilderRunResult> => {
|
|
168
168
|
if (
|
|
169
169
|
state.phase === WORKFLOW_PHASES.READY_FOR_VERIFIER ||
|
|
170
170
|
state.phase === WORKFLOW_PHASES.BUILDER_FAILED
|
|
@@ -210,7 +210,7 @@ const buildLaunchResult = async ({
|
|
|
210
210
|
);
|
|
211
211
|
};
|
|
212
212
|
|
|
213
|
-
const formatBuilderResult = (result:
|
|
213
|
+
const formatBuilderResult = (result: BuilderRunResult): string => {
|
|
214
214
|
if (result.outcome === BUILDER_HANDOFF_STATUSES.DONE) {
|
|
215
215
|
return `Builder completed spec ${result.specId}. The workflow is ready-for-verifier.`;
|
|
216
216
|
}
|
|
@@ -222,16 +222,16 @@ const formatBuilderResult = (result: BuilderLaunchResult): string => {
|
|
|
222
222
|
return `Builder opened escalation ${result.escalation.id} for spec ${result.specId}. The workflow is waiting for an owner decision.`;
|
|
223
223
|
};
|
|
224
224
|
|
|
225
|
-
export const
|
|
225
|
+
export const registerRunBuilderTool = (pi: ExtensionAPI): void => {
|
|
226
226
|
pi.registerTool({
|
|
227
|
-
name:
|
|
228
|
-
label:
|
|
229
|
-
description:
|
|
230
|
-
parameters:
|
|
227
|
+
name: RUN_BUILDER_TOOL.NAME,
|
|
228
|
+
label: RUN_BUILDER_TOOL.LABEL,
|
|
229
|
+
description: RUN_BUILDER_TOOL.DESCRIPTION,
|
|
230
|
+
parameters: RunBuilderToolParameters,
|
|
231
231
|
async execute(toolCallId, { specId }, _signal, _onUpdate, context) {
|
|
232
|
-
const { paths, config } = await
|
|
232
|
+
const { paths, config } = await resolveToolRunContext(context.cwd);
|
|
233
233
|
|
|
234
|
-
const
|
|
234
|
+
const run = await prepareBuilderRun({ paths, specId });
|
|
235
235
|
|
|
236
236
|
const request: SubagentDelegationRequest = {
|
|
237
237
|
requestId: randomUUID(),
|
|
@@ -240,9 +240,9 @@ export const registerLaunchBuilderTool = (pi: ExtensionAPI): void => {
|
|
|
240
240
|
// pi-subagents loads this name from agents/builder.md through package.json.
|
|
241
241
|
// See docs/subagent-integration.md.
|
|
242
242
|
agent: AGENTS.BUILDER,
|
|
243
|
-
task: `Implement specId "${specId}" in the current checkout "${
|
|
243
|
+
task: `Implement specId "${specId}" in the current checkout "${run.repositoryRoot}". Read all applicable AGENTS.md files before working.`,
|
|
244
244
|
context: 'fresh',
|
|
245
|
-
cwd:
|
|
245
|
+
cwd: run.repositoryRoot,
|
|
246
246
|
model: config.builder.model,
|
|
247
247
|
thinking: config.builder.thinking,
|
|
248
248
|
timeoutMs: config.builder.timeoutMinutes * MILLISECONDS_PER_MINUTE,
|