@emiliosp/pi-maestro 0.6.3 → 0.7.1
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 +17 -5
- package/agents/builder.md +6 -7
- package/agents/verifier.md +3 -3
- package/changelog.md +85 -0
- package/docs/glossary.md +69 -0
- package/docs/subagent-integration.md +1 -1
- package/docs/workflow.md +55 -23
- package/extensions/maestro-subagent.ts +0 -2
- package/extensions/maestro.ts +5 -5
- package/mission.md +7 -0
- package/package.json +7 -4
- package/roadmap.md +85 -0
- package/src/MaestroPaths.ts +0 -32
- package/src/artifacts/builder-handoff/assertBuilderHandoff.ts +29 -0
- package/src/artifacts/builder-handoff/schema.ts +87 -23
- package/src/maestro/instructions/getMaestroInstructions.ts +7 -7
- package/src/specs/create.ts +0 -4
- package/src/tools/child/record-builder-handoff.ts +14 -16
- package/src/tools/main/resolve-escalations.ts +72 -0
- package/src/tools/main/run-builder.ts +14 -10
- package/src/workflow/builder/completeBuilderPass.ts +20 -32
- package/src/workflow/escalation/resolveEscalations.ts +100 -0
- package/tech-stack.md +14 -0
- package/src/artifacts/escalation/assertEscalation.ts +0 -66
- package/src/artifacts/escalation/createEscalation.ts +0 -52
- package/src/artifacts/escalation/getNextEscalationId.ts +0 -23
- package/src/artifacts/escalation/readEscalation.ts +0 -40
- package/src/artifacts/escalation/readEscalationHistory.ts +0 -44
- package/src/artifacts/escalation/resolveEscalation.ts +0 -43
- package/src/artifacts/escalation/schema.ts +0 -73
- package/src/tools/child/open-escalation.ts +0 -71
- package/src/tools/main/resolve-escalation.ts +0 -65
- package/src/workflow/escalation/openBuilderEscalation.ts +0 -82
- package/src/workflow/escalation/resolveBuilderEscalation.ts +0 -118
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Objective: Save all owner resolutions in the active builder handoff.
|
|
3
|
+
* Used: When the owner keeps the contract and resolves current questions together.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readBuilderHandoff } from '#artifacts/builder-handoff/readBuilderHandoff.ts';
|
|
7
|
+
import {
|
|
8
|
+
BUILDER_HANDOFF_STATUSES,
|
|
9
|
+
type EscalationResolution,
|
|
10
|
+
} from '#artifacts/builder-handoff/schema.ts';
|
|
11
|
+
import { writeBuilderHandoff } from '#artifacts/builder-handoff/writeBuilderHandoff.ts';
|
|
12
|
+
import type { MaestroPaths } from '#MaestroPaths.ts';
|
|
13
|
+
import { readWorkflowState } from '#workflow/state/readWorkflowState.ts';
|
|
14
|
+
import { WORKFLOW_EVENTS, WORKFLOW_PHASES } from '#workflow/state/schema.ts';
|
|
15
|
+
import { writeWorkflowState } from '#workflow/state/writeWorkflowState.ts';
|
|
16
|
+
import { transitionWorkflow } from '#workflow/transitions.ts';
|
|
17
|
+
|
|
18
|
+
export type EscalationDecision = EscalationResolution & {
|
|
19
|
+
escalationId: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type ResolveEscalationsInput = {
|
|
23
|
+
paths: MaestroPaths;
|
|
24
|
+
specId: string;
|
|
25
|
+
decisions: EscalationDecision[];
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const resolveEscalations = async ({
|
|
29
|
+
paths,
|
|
30
|
+
specId,
|
|
31
|
+
decisions,
|
|
32
|
+
}: ResolveEscalationsInput) => {
|
|
33
|
+
const workflowPath = paths.getWorkflowPath(specId);
|
|
34
|
+
const state = await readWorkflowState(workflowPath);
|
|
35
|
+
|
|
36
|
+
if (state.specId !== specId) throw new Error('Workflow spec ID mismatch.');
|
|
37
|
+
|
|
38
|
+
if (state.phase !== WORKFLOW_PHASES.ESCALATION_DECISION) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
`Escalation resolution requires escalation-decision state, found "${state.phase}".`,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const handoffPath = await paths.getActiveBuilderHandoffPath(specId);
|
|
45
|
+
const handoff = await readBuilderHandoff({ path: handoffPath, specId });
|
|
46
|
+
|
|
47
|
+
if (handoff.status !== BUILDER_HANDOFF_STATUSES.ESCALATION) {
|
|
48
|
+
throw new Error('Escalation resolution requires an escalation handoff.');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (handoff.escalations.some(({ resolution }) => resolution !== null)) {
|
|
52
|
+
throw new Error('Current questions already contain an owner resolution.');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (decisions.length === 0)
|
|
56
|
+
throw new Error('At least one escalation decision is required.');
|
|
57
|
+
|
|
58
|
+
const expectedIds = new Set(handoff.escalations.map(({ id }) => id));
|
|
59
|
+
const resolutions = new Map<string, EscalationResolution>();
|
|
60
|
+
|
|
61
|
+
for (const { escalationId, ...resolution } of decisions) {
|
|
62
|
+
if (!expectedIds.has(escalationId))
|
|
63
|
+
throw new Error(`Unknown escalation ID: "${escalationId}".`);
|
|
64
|
+
|
|
65
|
+
if (resolutions.has(escalationId))
|
|
66
|
+
throw new Error(`Duplicate decision for escalation "${escalationId}".`);
|
|
67
|
+
|
|
68
|
+
resolutions.set(escalationId, resolution);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const escalations = handoff.escalations.map((escalation) => {
|
|
72
|
+
const resolution = resolutions.get(escalation.id);
|
|
73
|
+
|
|
74
|
+
if (resolution === undefined)
|
|
75
|
+
throw new Error(`Missing decision for escalation "${escalation.id}".`);
|
|
76
|
+
|
|
77
|
+
return { ...escalation, resolution };
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const nextHandoff = { ...handoff, escalations };
|
|
81
|
+
|
|
82
|
+
const nextState = transitionWorkflow({
|
|
83
|
+
state,
|
|
84
|
+
event: WORKFLOW_EVENTS.RESOLVE_ESCALATION,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// The artifact writer validates every resolution before the first protocol write.
|
|
88
|
+
await writeBuilderHandoff({
|
|
89
|
+
path: handoffPath,
|
|
90
|
+
handoff: nextHandoff,
|
|
91
|
+
specId,
|
|
92
|
+
});
|
|
93
|
+
await writeWorkflowState({ path: workflowPath, state: nextState });
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
handoff: nextHandoff,
|
|
97
|
+
state: nextState,
|
|
98
|
+
projectRoot: paths.getProjectRoot(),
|
|
99
|
+
};
|
|
100
|
+
};
|
package/tech-stack.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Tech stack
|
|
2
|
+
|
|
3
|
+
Maestro is a source-only TypeScript package that uses ECMAScript modules (ESM). Pi loads the TypeScript source directly. The package has no build step or compiled `dist/` directory.
|
|
4
|
+
|
|
5
|
+
| Technology | Use |
|
|
6
|
+
|---|---|
|
|
7
|
+
| TypeScript | Application code and static type checks with `tsc --noEmit`. |
|
|
8
|
+
| Node.js | Runtime. |
|
|
9
|
+
| Pi | Extension host, agent APIs, and terminal interface. |
|
|
10
|
+
| `pi-subagents` | Builder and verifier execution and activity tracking. |
|
|
11
|
+
| TypeBox | Schemas and runtime validation for configuration, workflow state, tool inputs, and agent reports. |
|
|
12
|
+
| Vitest with V8 coverage | Unit tests, integration tests, and code coverage. |
|
|
13
|
+
| Biome | Formatting and lint checks. |
|
|
14
|
+
| Oxlint with `oxlint-anti-slop` | Additional lint and anti-slop checks. |
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Objective: Check the escalation schema and option references.
|
|
3
|
-
* Used: When Maestro handles escalation artifacts.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { Value } from 'typebox/value';
|
|
7
|
-
import {
|
|
8
|
-
type Escalation,
|
|
9
|
-
EscalationSchema,
|
|
10
|
-
} from '#artifacts/escalation/schema.ts';
|
|
11
|
-
import { isValidSpecId } from '#ids/isValidSpecId.ts';
|
|
12
|
-
|
|
13
|
-
type HasOptionInput = {
|
|
14
|
-
escalation: Escalation;
|
|
15
|
-
optionId: string;
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
const hasOption = ({ escalation, optionId }: HasOptionInput): boolean =>
|
|
19
|
-
escalation.options.some((option) => option.id === optionId);
|
|
20
|
-
|
|
21
|
-
function assertEscalationSchema(input: unknown): asserts input is Escalation {
|
|
22
|
-
if (typeof input !== 'object' || input === null || Array.isArray(input)) {
|
|
23
|
-
throw new Error('Escalation must be a JSON object.');
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const [error] = Value.Errors(EscalationSchema, input);
|
|
27
|
-
|
|
28
|
-
if (error !== undefined) {
|
|
29
|
-
throw new Error(`Invalid escalation: ${error.message}.`);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function assertEscalation(input: unknown): asserts input is Escalation {
|
|
34
|
-
assertEscalationSchema(input);
|
|
35
|
-
const escalation = input;
|
|
36
|
-
|
|
37
|
-
if (!isValidSpecId(escalation.specId)) {
|
|
38
|
-
throw new Error(`Invalid escalation spec ID: "${escalation.specId}".`);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
if (
|
|
42
|
-
new Set(escalation.options.map((option) => option.id)).size !==
|
|
43
|
-
escalation.options.length
|
|
44
|
-
) {
|
|
45
|
-
throw new Error('Escalation option IDs must be unique.');
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
if (
|
|
49
|
-
escalation.recommendation !== null &&
|
|
50
|
-
!hasOption({ escalation, optionId: escalation.recommendation.optionId })
|
|
51
|
-
) {
|
|
52
|
-
throw new Error(
|
|
53
|
-
`Escalation recommendation references unknown option "${escalation.recommendation.optionId}".`,
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (
|
|
58
|
-
escalation.resolution !== null &&
|
|
59
|
-
escalation.resolution.selectedOptionId !== null &&
|
|
60
|
-
!hasOption({ escalation, optionId: escalation.resolution.selectedOptionId })
|
|
61
|
-
) {
|
|
62
|
-
throw new Error(
|
|
63
|
-
`Escalation resolution references unknown option "${escalation.resolution.selectedOptionId}".`,
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Objective: Create an unresolved escalation.
|
|
3
|
-
* Used: When Maestro handles escalation artifacts.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { join } from 'node:path';
|
|
7
|
-
import { assertEscalation } from '#artifacts/escalation/assertEscalation.ts';
|
|
8
|
-
import { getNextEscalationId } from '#artifacts/escalation/getNextEscalationId.ts';
|
|
9
|
-
import {
|
|
10
|
-
ESCALATION_VERSION,
|
|
11
|
-
type Escalation,
|
|
12
|
-
type NewEscalation,
|
|
13
|
-
} from '#artifacts/escalation/schema.ts';
|
|
14
|
-
import { writeJson } from '#utils/write-json.ts';
|
|
15
|
-
|
|
16
|
-
type CreateEscalationInput = {
|
|
17
|
-
directory: string;
|
|
18
|
-
specId: string;
|
|
19
|
-
escalation: NewEscalation;
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
export const createEscalation = async ({
|
|
23
|
-
directory,
|
|
24
|
-
specId,
|
|
25
|
-
escalation,
|
|
26
|
-
}: CreateEscalationInput): Promise<{
|
|
27
|
-
path: string;
|
|
28
|
-
escalation: Escalation;
|
|
29
|
-
}> => {
|
|
30
|
-
const id = await getNextEscalationId({
|
|
31
|
-
directory,
|
|
32
|
-
specId,
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
const newEscalation = {
|
|
36
|
-
question: escalation.question,
|
|
37
|
-
context: escalation.context,
|
|
38
|
-
options: escalation.options,
|
|
39
|
-
recommendation: escalation.recommendation,
|
|
40
|
-
notes: escalation.notes,
|
|
41
|
-
version: ESCALATION_VERSION,
|
|
42
|
-
specId,
|
|
43
|
-
id,
|
|
44
|
-
resolution: null,
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
assertEscalation(newEscalation);
|
|
48
|
-
const path = join(directory, `${id}.json`);
|
|
49
|
-
await writeJson({ path, data: newEscalation });
|
|
50
|
-
|
|
51
|
-
return { path, escalation: newEscalation };
|
|
52
|
-
};
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Objective: Allocate the next escalation ID from history.
|
|
3
|
-
* Used: When Maestro handles escalation artifacts.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { readEscalationHistory } from '#artifacts/escalation/readEscalationHistory.ts';
|
|
7
|
-
|
|
8
|
-
type GetNextEscalationIdInput = {
|
|
9
|
-
directory: string;
|
|
10
|
-
specId: string;
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
export const getNextEscalationId = async ({
|
|
14
|
-
directory,
|
|
15
|
-
specId,
|
|
16
|
-
}: GetNextEscalationIdInput): Promise<string> => {
|
|
17
|
-
const history = await readEscalationHistory({
|
|
18
|
-
directory,
|
|
19
|
-
specId,
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
return `E${history.length + 1}`;
|
|
23
|
-
};
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Objective: Read an escalation for its workflow.
|
|
3
|
-
* Used: When Maestro handles escalation artifacts.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { assertEscalation } from '#artifacts/escalation/assertEscalation.ts';
|
|
7
|
-
import type { Escalation } from '#artifacts/escalation/schema.ts';
|
|
8
|
-
import { readJsonFile } from '#utils/read-json.ts';
|
|
9
|
-
|
|
10
|
-
type AssertWorkflowEscalationInput = {
|
|
11
|
-
escalation: Escalation;
|
|
12
|
-
specId: string;
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
function assertWorkflowEscalation({
|
|
16
|
-
escalation,
|
|
17
|
-
specId,
|
|
18
|
-
}: AssertWorkflowEscalationInput): void {
|
|
19
|
-
if (escalation.specId !== specId) {
|
|
20
|
-
throw new Error(
|
|
21
|
-
`Escalation spec ID mismatch: expected "${specId}", found "${escalation.specId}".`,
|
|
22
|
-
);
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
type ReadEscalationInput = {
|
|
27
|
-
path: string;
|
|
28
|
-
specId: string;
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
export const readEscalation = async ({
|
|
32
|
-
path,
|
|
33
|
-
specId,
|
|
34
|
-
}: ReadEscalationInput): Promise<Escalation> => {
|
|
35
|
-
const escalation = await readJsonFile({ path, description: 'Escalation' });
|
|
36
|
-
assertEscalation(escalation);
|
|
37
|
-
assertWorkflowEscalation({ escalation, specId });
|
|
38
|
-
|
|
39
|
-
return escalation;
|
|
40
|
-
};
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Objective: Read and order validated escalation history.
|
|
3
|
-
* Used: When Maestro handles escalation artifacts.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { readdir } from 'node:fs/promises';
|
|
7
|
-
import { join } from 'node:path';
|
|
8
|
-
import { readEscalation } from '#artifacts/escalation/readEscalation.ts';
|
|
9
|
-
import type { Escalation } from '#artifacts/escalation/schema.ts';
|
|
10
|
-
|
|
11
|
-
const ESCALATION_FILE_PATTERN = /^E([1-9]\d*)\.json$/;
|
|
12
|
-
|
|
13
|
-
type ReadEscalationHistoryInput = {
|
|
14
|
-
directory: string;
|
|
15
|
-
specId: string;
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
export const readEscalationHistory = async ({
|
|
19
|
-
directory,
|
|
20
|
-
specId,
|
|
21
|
-
}: ReadEscalationHistoryInput): Promise<Escalation[]> => {
|
|
22
|
-
const files = await readdir(directory);
|
|
23
|
-
|
|
24
|
-
if (!files.every((file) => ESCALATION_FILE_PATTERN.test(file))) {
|
|
25
|
-
throw new Error('Escalation history contains an invalid entry.');
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const orderedFiles = files.sort(
|
|
29
|
-
(left, right) => Number(left.slice(1, -5)) - Number(right.slice(1, -5)),
|
|
30
|
-
);
|
|
31
|
-
|
|
32
|
-
const escalations: Escalation[] = [];
|
|
33
|
-
|
|
34
|
-
for (const file of orderedFiles.values()) {
|
|
35
|
-
const escalation = await readEscalation({
|
|
36
|
-
path: join(directory, file),
|
|
37
|
-
specId,
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
escalations.push(escalation);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return escalations;
|
|
44
|
-
};
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Objective: Record an owner escalation resolution.
|
|
3
|
-
* Used: When Maestro handles escalation artifacts.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { assertEscalation } from '#artifacts/escalation/assertEscalation.ts';
|
|
7
|
-
import { readEscalation } from '#artifacts/escalation/readEscalation.ts';
|
|
8
|
-
import type {
|
|
9
|
-
Escalation,
|
|
10
|
-
EscalationResolution,
|
|
11
|
-
} from '#artifacts/escalation/schema.ts';
|
|
12
|
-
import { writeJson } from '#utils/write-json.ts';
|
|
13
|
-
|
|
14
|
-
type ResolveEscalationInput = {
|
|
15
|
-
path: string;
|
|
16
|
-
specId: string;
|
|
17
|
-
resolution: EscalationResolution;
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
export const resolveEscalation = async ({
|
|
21
|
-
path,
|
|
22
|
-
specId,
|
|
23
|
-
resolution,
|
|
24
|
-
}: ResolveEscalationInput): Promise<Escalation> => {
|
|
25
|
-
const current = await readEscalation({
|
|
26
|
-
path,
|
|
27
|
-
specId,
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
if (current.resolution !== null) {
|
|
31
|
-
throw new Error(`Escalation "${current.id}" is already resolved.`);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const resolvedEscalation = {
|
|
35
|
-
...current,
|
|
36
|
-
resolution,
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
assertEscalation(resolvedEscalation);
|
|
40
|
-
await writeJson({ path, data: resolvedEscalation });
|
|
41
|
-
|
|
42
|
-
return resolvedEscalation;
|
|
43
|
-
};
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Objective: Define escalation options and resolution contracts.
|
|
3
|
-
* Used: When Maestro handles escalation artifacts.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { type Static, Type } from 'typebox';
|
|
7
|
-
import { SPEC_ID_PATTERN } from '#ids/isValidSpecId.ts';
|
|
8
|
-
|
|
9
|
-
export const ESCALATION_VERSION = '1.0.0';
|
|
10
|
-
|
|
11
|
-
export const ESCALATION_ID_PATTERN = /^E([1-9]\d*)$/;
|
|
12
|
-
|
|
13
|
-
export const EscalationOptionSchema = Type.Object(
|
|
14
|
-
{
|
|
15
|
-
id: Type.String({ minLength: 1 }),
|
|
16
|
-
description: Type.String({ minLength: 1 }),
|
|
17
|
-
consequences: Type.String({ minLength: 1 }),
|
|
18
|
-
nextStep: Type.String({ minLength: 1 }),
|
|
19
|
-
},
|
|
20
|
-
{ additionalProperties: false },
|
|
21
|
-
);
|
|
22
|
-
|
|
23
|
-
export const EscalationRecommendationSchema = Type.Object(
|
|
24
|
-
{
|
|
25
|
-
optionId: Type.String({ minLength: 1 }),
|
|
26
|
-
reason: Type.String({ minLength: 1 }),
|
|
27
|
-
},
|
|
28
|
-
{ additionalProperties: false },
|
|
29
|
-
);
|
|
30
|
-
|
|
31
|
-
export const EscalationResolutionSchema = Type.Object(
|
|
32
|
-
{
|
|
33
|
-
selectedOptionId: Type.Union([Type.String({ minLength: 1 }), Type.Null()]),
|
|
34
|
-
decision: Type.String({ minLength: 1 }),
|
|
35
|
-
reason: Type.String({ minLength: 1 }),
|
|
36
|
-
},
|
|
37
|
-
{ additionalProperties: false },
|
|
38
|
-
);
|
|
39
|
-
|
|
40
|
-
const NewEscalationFields = {
|
|
41
|
-
question: Type.String({ minLength: 1 }),
|
|
42
|
-
context: Type.String({ minLength: 1 }),
|
|
43
|
-
options: Type.Array(EscalationOptionSchema, { minItems: 1 }),
|
|
44
|
-
notes: Type.Array(Type.String()),
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
export const NewEscalationSchema = Type.Object(
|
|
48
|
-
{
|
|
49
|
-
...NewEscalationFields,
|
|
50
|
-
recommendation: Type.Union([EscalationRecommendationSchema, Type.Null()]),
|
|
51
|
-
},
|
|
52
|
-
{ additionalProperties: false },
|
|
53
|
-
);
|
|
54
|
-
|
|
55
|
-
export const EscalationSchema = Type.Object(
|
|
56
|
-
{
|
|
57
|
-
version: Type.Literal(ESCALATION_VERSION),
|
|
58
|
-
specId: Type.String({ pattern: SPEC_ID_PATTERN.source }),
|
|
59
|
-
id: Type.String({ pattern: ESCALATION_ID_PATTERN.source }),
|
|
60
|
-
...NewEscalationFields,
|
|
61
|
-
recommendation: Type.Union([EscalationRecommendationSchema, Type.Null()]),
|
|
62
|
-
resolution: Type.Union([EscalationResolutionSchema, Type.Null()]),
|
|
63
|
-
},
|
|
64
|
-
{ additionalProperties: false },
|
|
65
|
-
);
|
|
66
|
-
|
|
67
|
-
export type EscalationOption = Static<typeof EscalationOptionSchema>;
|
|
68
|
-
|
|
69
|
-
export type EscalationResolution = Static<typeof EscalationResolutionSchema>;
|
|
70
|
-
|
|
71
|
-
export type Escalation = Static<typeof EscalationSchema>;
|
|
72
|
-
|
|
73
|
-
export type NewEscalation = Static<typeof NewEscalationSchema>;
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Objective: Register the builder escalation tool for the current project.
|
|
3
|
-
* Used: When the builder finds a significant discovery that requires an owner decision.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
7
|
-
import { Type } from 'typebox';
|
|
8
|
-
import {
|
|
9
|
-
EscalationOptionSchema,
|
|
10
|
-
EscalationRecommendationSchema,
|
|
11
|
-
} from '#artifacts/escalation/schema.ts';
|
|
12
|
-
import { SPEC_ID_PATTERN } from '#ids/isValidSpecId.ts';
|
|
13
|
-
import { resolveWorkflowContext } from '#tools/child/utils/resolveWorkflowContext.ts';
|
|
14
|
-
import { openBuilderEscalation } from '#workflow/escalation/openBuilderEscalation.ts';
|
|
15
|
-
|
|
16
|
-
export const BUILDER_ESCALATION_TOOL = {
|
|
17
|
-
NAME: 'maestro_open_escalation',
|
|
18
|
-
LABEL: 'Open Builder Escalation',
|
|
19
|
-
DESCRIPTION:
|
|
20
|
-
'Ask the owner to choose between options for a significant discovery or unresolved decision. An escalation is not limited to a technical failure or blocker. After success, stop. The tool saves the escalation and workflow phase. Do not wait for the owner.',
|
|
21
|
-
} as const;
|
|
22
|
-
|
|
23
|
-
const BuilderEscalationToolParameters = Type.Object(
|
|
24
|
-
{
|
|
25
|
-
specId: Type.String({ pattern: SPEC_ID_PATTERN.source }),
|
|
26
|
-
question: Type.String({ minLength: 1 }),
|
|
27
|
-
context: Type.String({ minLength: 1 }),
|
|
28
|
-
options: Type.Array(EscalationOptionSchema, { minItems: 1 }),
|
|
29
|
-
recommendation: Type.Union([EscalationRecommendationSchema, Type.Null()]),
|
|
30
|
-
notes: Type.Array(Type.String()),
|
|
31
|
-
},
|
|
32
|
-
{ additionalProperties: false },
|
|
33
|
-
);
|
|
34
|
-
|
|
35
|
-
export const registerOpenEscalationTool = (pi: ExtensionAPI): void => {
|
|
36
|
-
pi.registerTool({
|
|
37
|
-
name: BUILDER_ESCALATION_TOOL.NAME,
|
|
38
|
-
label: BUILDER_ESCALATION_TOOL.LABEL,
|
|
39
|
-
description: BUILDER_ESCALATION_TOOL.DESCRIPTION,
|
|
40
|
-
parameters: BuilderEscalationToolParameters,
|
|
41
|
-
async execute(_toolCallId, params, _signal, _onUpdate, context) {
|
|
42
|
-
const { specId, ...escalation } = params;
|
|
43
|
-
|
|
44
|
-
const { paths } = await resolveWorkflowContext({
|
|
45
|
-
cwd: context.cwd,
|
|
46
|
-
specId,
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
const opened = await openBuilderEscalation({
|
|
50
|
-
paths,
|
|
51
|
-
specId,
|
|
52
|
-
escalation,
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
return {
|
|
56
|
-
content: [
|
|
57
|
-
{
|
|
58
|
-
type: 'text',
|
|
59
|
-
text: `Escalation ${opened.escalation.id} recorded. The escalation and workflow phase are saved. Stop now. Do not wait for the owner.`,
|
|
60
|
-
},
|
|
61
|
-
],
|
|
62
|
-
details: {
|
|
63
|
-
specId: opened.escalation.specId,
|
|
64
|
-
escalationId: opened.escalation.id,
|
|
65
|
-
escalationPath: opened.escalationPath,
|
|
66
|
-
phase: opened.state.phase,
|
|
67
|
-
},
|
|
68
|
-
};
|
|
69
|
-
},
|
|
70
|
-
});
|
|
71
|
-
};
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Objective: Register owner resolution of the current builder escalation.
|
|
3
|
-
* Used: When the owner keeps the approved contract and chooses an escalation resolution.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
7
|
-
import { Type } from 'typebox';
|
|
8
|
-
import {
|
|
9
|
-
ESCALATION_ID_PATTERN,
|
|
10
|
-
EscalationResolutionSchema,
|
|
11
|
-
} from '#artifacts/escalation/schema.ts';
|
|
12
|
-
import { SPEC_ID_PATTERN } from '#ids/isValidSpecId.ts';
|
|
13
|
-
import { resolveToolRunContext } from '#tools/utils/resolveToolRunContext.ts';
|
|
14
|
-
import { resolveBuilderEscalation } from '#workflow/escalation/resolveBuilderEscalation.ts';
|
|
15
|
-
|
|
16
|
-
export const RESOLVE_ESCALATION_TOOL = {
|
|
17
|
-
NAME: 'maestro_resolve_escalation',
|
|
18
|
-
LABEL: 'Resolve Escalation',
|
|
19
|
-
DESCRIPTION:
|
|
20
|
-
'Record the explicit owner decision for the current builder escalation when the approved contract remains valid. Save the resolution and workflow transition, 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
|
-
} as const;
|
|
22
|
-
|
|
23
|
-
const ResolveEscalationToolParameters = Type.Object(
|
|
24
|
-
{
|
|
25
|
-
specId: Type.String({ pattern: SPEC_ID_PATTERN.source }),
|
|
26
|
-
escalationId: Type.String({ pattern: ESCALATION_ID_PATTERN.source }),
|
|
27
|
-
...EscalationResolutionSchema.properties,
|
|
28
|
-
},
|
|
29
|
-
{ additionalProperties: false },
|
|
30
|
-
);
|
|
31
|
-
|
|
32
|
-
export const registerResolveEscalationTool = (pi: ExtensionAPI): void => {
|
|
33
|
-
pi.registerTool({
|
|
34
|
-
name: RESOLVE_ESCALATION_TOOL.NAME,
|
|
35
|
-
label: RESOLVE_ESCALATION_TOOL.LABEL,
|
|
36
|
-
description: RESOLVE_ESCALATION_TOOL.DESCRIPTION,
|
|
37
|
-
parameters: ResolveEscalationToolParameters,
|
|
38
|
-
async execute(_toolCallId, params, _signal, _onUpdate, context) {
|
|
39
|
-
const { specId, escalationId, ...resolution } = params;
|
|
40
|
-
const { paths } = await resolveToolRunContext(context.cwd);
|
|
41
|
-
|
|
42
|
-
const resolved = await resolveBuilderEscalation({
|
|
43
|
-
paths,
|
|
44
|
-
specId,
|
|
45
|
-
escalationId,
|
|
46
|
-
resolution,
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
return {
|
|
50
|
-
content: [
|
|
51
|
-
{
|
|
52
|
-
type: 'text',
|
|
53
|
-
text: `Escalation ${resolved.escalation.id} resolved. Spec ${resolved.state.specId} is ready-for-builder. Run the builder separately.`,
|
|
54
|
-
},
|
|
55
|
-
],
|
|
56
|
-
details: {
|
|
57
|
-
specId: resolved.state.specId,
|
|
58
|
-
escalationId: resolved.escalation.id,
|
|
59
|
-
phase: resolved.state.phase,
|
|
60
|
-
projectRoot: resolved.projectRoot,
|
|
61
|
-
},
|
|
62
|
-
};
|
|
63
|
-
},
|
|
64
|
-
});
|
|
65
|
-
};
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Objective: Open a builder escalation and move the workflow to an owner decision.
|
|
3
|
-
* Used: When a builder identifies a significant discovery that requires owner attention.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { mkdir } from 'node:fs/promises';
|
|
7
|
-
import { createEscalation } from '#artifacts/escalation/createEscalation.ts';
|
|
8
|
-
import type {
|
|
9
|
-
Escalation,
|
|
10
|
-
NewEscalation,
|
|
11
|
-
} from '#artifacts/escalation/schema.ts';
|
|
12
|
-
import type { MaestroPaths } from '#MaestroPaths.ts';
|
|
13
|
-
import { readWorkflowState } from '#workflow/state/readWorkflowState.ts';
|
|
14
|
-
import {
|
|
15
|
-
WORKFLOW_EVENTS,
|
|
16
|
-
WORKFLOW_PHASES,
|
|
17
|
-
type WorkflowState,
|
|
18
|
-
} from '#workflow/state/schema.ts';
|
|
19
|
-
import { writeWorkflowState } from '#workflow/state/writeWorkflowState.ts';
|
|
20
|
-
import { transitionWorkflow } from '#workflow/transitions.ts';
|
|
21
|
-
|
|
22
|
-
export type OpenedBuilderEscalation = {
|
|
23
|
-
escalation: Escalation;
|
|
24
|
-
state: WorkflowState;
|
|
25
|
-
projectRoot: string;
|
|
26
|
-
workflowPath: string;
|
|
27
|
-
escalationPath: string;
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
type OpenBuilderEscalationInput = {
|
|
31
|
-
paths: MaestroPaths;
|
|
32
|
-
specId: string;
|
|
33
|
-
escalation: NewEscalation;
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
export const openBuilderEscalation = async ({
|
|
37
|
-
paths,
|
|
38
|
-
specId,
|
|
39
|
-
escalation,
|
|
40
|
-
}: OpenBuilderEscalationInput): Promise<OpenedBuilderEscalation> => {
|
|
41
|
-
const workflowPath = paths.getWorkflowPath(specId);
|
|
42
|
-
const escalationsPath = paths.getEscalationsPath(specId);
|
|
43
|
-
const currentState = await readWorkflowState(workflowPath);
|
|
44
|
-
|
|
45
|
-
if (currentState.specId !== specId) {
|
|
46
|
-
throw new Error(
|
|
47
|
-
`Workflow spec ID mismatch: expected "${specId}", found "${currentState.specId}".`,
|
|
48
|
-
);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
if (currentState.phase !== WORKFLOW_PHASES.BUILDER_RUNNING) {
|
|
52
|
-
throw new Error(
|
|
53
|
-
`Builder escalation requires builder-running state, found "${currentState.phase}".`,
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const nextState = transitionWorkflow({
|
|
58
|
-
state: currentState,
|
|
59
|
-
event: WORKFLOW_EVENTS.OPEN_ESCALATION,
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
await mkdir(escalationsPath, { recursive: true });
|
|
63
|
-
|
|
64
|
-
const created = await createEscalation({
|
|
65
|
-
directory: escalationsPath,
|
|
66
|
-
specId: currentState.specId,
|
|
67
|
-
escalation,
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
await writeWorkflowState({
|
|
71
|
-
path: workflowPath,
|
|
72
|
-
state: nextState,
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
return {
|
|
76
|
-
escalation: created.escalation,
|
|
77
|
-
state: nextState,
|
|
78
|
-
projectRoot: paths.getProjectRoot(),
|
|
79
|
-
workflowPath,
|
|
80
|
-
escalationPath: created.path,
|
|
81
|
-
};
|
|
82
|
-
};
|