@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
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { realpath } from 'node:fs/promises';
|
|
7
|
+
|
|
7
8
|
import { loadConfiguration } from '#config/loadConfiguration.ts';
|
|
8
9
|
import type { MaestroConfig } from '#config/schema.ts';
|
|
9
10
|
import { MaestroPaths } from '#MaestroPaths.ts';
|
|
@@ -14,9 +15,7 @@ export type ToolRunContext = {
|
|
|
14
15
|
paths: MaestroPaths;
|
|
15
16
|
};
|
|
16
17
|
|
|
17
|
-
export const resolveToolRunContext = async (
|
|
18
|
-
cwd: string,
|
|
19
|
-
): Promise<ToolRunContext> => {
|
|
18
|
+
export const resolveToolRunContext = async (cwd: string): Promise<ToolRunContext> => {
|
|
20
19
|
const projectRoot = await realpath(cwd);
|
|
21
20
|
const config = await loadConfiguration(projectRoot);
|
|
22
21
|
|
|
@@ -3,7 +3,5 @@
|
|
|
3
3
|
* Used: When filesystem operations handle specific error codes.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
export const isErrnoException = (
|
|
7
|
-
error: unknown,
|
|
8
|
-
): error is NodeJS.ErrnoException =>
|
|
6
|
+
export const isErrnoException = (error: unknown): error is NodeJS.ErrnoException =>
|
|
9
7
|
error instanceof Error && 'code' in error && typeof error.code === 'string';
|
package/src/utils/path-exists.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { relative } from 'node:path';
|
|
7
|
+
|
|
7
8
|
import { isPathWithinOrEqual } from '#utils/path-within-or-equal.ts';
|
|
8
9
|
|
|
9
10
|
type IsPathStrictlyWithinInput = {
|
|
@@ -11,9 +12,5 @@ type IsPathStrictlyWithinInput = {
|
|
|
11
12
|
path: string;
|
|
12
13
|
};
|
|
13
14
|
|
|
14
|
-
export const isPathStrictlyWithin = ({
|
|
15
|
-
parent,
|
|
16
|
-
path,
|
|
17
|
-
}: IsPathStrictlyWithinInput): boolean =>
|
|
18
|
-
relative(parent, path) !== '' &&
|
|
19
|
-
isPathWithinOrEqual({ parent, candidate: path });
|
|
15
|
+
export const isPathStrictlyWithin = ({ parent, path }: IsPathStrictlyWithinInput): boolean =>
|
|
16
|
+
relative(parent, path) !== '' && isPathWithinOrEqual({ parent, candidate: path });
|
|
@@ -10,10 +10,7 @@ type IsPathWithinOrEqualInput = {
|
|
|
10
10
|
candidate: string;
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
-
export const isPathWithinOrEqual = ({
|
|
14
|
-
parent,
|
|
15
|
-
candidate,
|
|
16
|
-
}: IsPathWithinOrEqualInput): boolean => {
|
|
13
|
+
export const isPathWithinOrEqual = ({ parent, candidate }: IsPathWithinOrEqualInput): boolean => {
|
|
17
14
|
const pathToCandidate = relative(parent, candidate);
|
|
18
15
|
|
|
19
16
|
return (
|
package/src/utils/read-json.ts
CHANGED
|
@@ -10,10 +10,7 @@ type ReadJsonFileInput = {
|
|
|
10
10
|
description: string;
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
-
export const readJsonFile = async ({
|
|
14
|
-
path,
|
|
15
|
-
description,
|
|
16
|
-
}: ReadJsonFileInput): Promise<unknown> => {
|
|
13
|
+
export const readJsonFile = async ({ path, description }: ReadJsonFileInput): Promise<unknown> => {
|
|
17
14
|
const content = await readFile(path, 'utf8');
|
|
18
15
|
|
|
19
16
|
try {
|
package/src/utils/write-json.ts
CHANGED
|
@@ -10,10 +10,7 @@ type WriteJsonInput = {
|
|
|
10
10
|
data: unknown;
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
-
export const writeJson = async ({
|
|
14
|
-
path,
|
|
15
|
-
data,
|
|
16
|
-
}: WriteJsonInput): Promise<void> => {
|
|
13
|
+
export const writeJson = async ({ path, data }: WriteJsonInput): Promise<void> => {
|
|
17
14
|
const content = JSON.stringify(data, null, 2);
|
|
18
15
|
|
|
19
16
|
if (content === undefined) {
|
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { mkdir, readFile } from 'node:fs/promises';
|
|
7
|
+
|
|
7
8
|
import { Value } from 'typebox/value';
|
|
9
|
+
|
|
8
10
|
import { assertBuilderHandoff } from '#artifacts/builder-handoff/assertBuilderHandoff.ts';
|
|
9
11
|
import {
|
|
10
12
|
BUILDER_HANDOFF_VERSION,
|
|
@@ -33,8 +35,7 @@ function assertBuilderHandoffSubmission(
|
|
|
33
35
|
): asserts input is BuilderHandoffSubmissionInput {
|
|
34
36
|
const [error] = Value.Errors(BuilderHandoffSubmissionSchema, input);
|
|
35
37
|
|
|
36
|
-
if (error !== undefined)
|
|
37
|
-
throw new Error(`Invalid builder submission: ${error.message}.`);
|
|
38
|
+
if (error !== undefined) throw new Error(`Invalid builder submission: ${error.message}.`);
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
type CompleteBuilderPassInput = {
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* Used: Before Maestro runs a builder pass.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import type { MaestroPaths } from '#MaestroPaths.ts';
|
|
7
6
|
import maestroSessionState from '#maestro/session/MaestroSessionState.ts';
|
|
7
|
+
import type { MaestroPaths } from '#MaestroPaths.ts';
|
|
8
8
|
import { pathExists } from '#utils/path-exists.ts';
|
|
9
9
|
import { readWorkflowState } from '#workflow/state/readWorkflowState.ts';
|
|
10
10
|
import { WORKFLOW_EVENTS, WORKFLOW_PHASES } from '#workflow/state/schema.ts';
|
|
@@ -21,16 +21,11 @@ type AssertBuilderRunBaseInput = {
|
|
|
21
21
|
specId: string;
|
|
22
22
|
};
|
|
23
23
|
|
|
24
|
-
async function assertBuilderRunBase({
|
|
25
|
-
paths,
|
|
26
|
-
specId,
|
|
27
|
-
}: AssertBuilderRunBaseInput): Promise<void> {
|
|
24
|
+
async function assertBuilderRunBase({ paths, specId }: AssertBuilderRunBaseInput): Promise<void> {
|
|
28
25
|
const state = await readWorkflowState(paths.getWorkflowPath(specId));
|
|
29
26
|
|
|
30
27
|
if (state.specId !== specId) {
|
|
31
|
-
throw new Error(
|
|
32
|
-
`Workflow spec ID mismatch: expected "${specId}", found "${state.specId}".`,
|
|
33
|
-
);
|
|
28
|
+
throw new Error(`Workflow spec ID mismatch: expected "${specId}", found "${state.specId}".`);
|
|
34
29
|
}
|
|
35
30
|
|
|
36
31
|
if (state.phase !== WORKFLOW_PHASES.READY_FOR_BUILDER) {
|
|
@@ -26,16 +26,12 @@ type ApplyEscalationDecisionsInput = {
|
|
|
26
26
|
decisions: EscalationDecision[];
|
|
27
27
|
};
|
|
28
28
|
|
|
29
|
-
const applyEscalationDecisions = ({
|
|
30
|
-
escalations,
|
|
31
|
-
decisions,
|
|
32
|
-
}: ApplyEscalationDecisionsInput) => {
|
|
29
|
+
const applyEscalationDecisions = ({ escalations, decisions }: ApplyEscalationDecisionsInput) => {
|
|
33
30
|
if (escalations.some(({ resolution }) => resolution !== null)) {
|
|
34
31
|
throw new Error('Current questions already contain an owner resolution.');
|
|
35
32
|
}
|
|
36
33
|
|
|
37
|
-
if (decisions.length === 0)
|
|
38
|
-
throw new Error('At least one escalation decision is required.');
|
|
34
|
+
if (decisions.length === 0) throw new Error('At least one escalation decision is required.');
|
|
39
35
|
|
|
40
36
|
const expectedIds = new Set(escalations.map(({ id }) => id));
|
|
41
37
|
const resolutions = new Map<string, EscalationResolution>();
|
|
@@ -9,9 +9,7 @@ import {
|
|
|
9
9
|
} from '#artifacts/builder-handoff/schema.ts';
|
|
10
10
|
import { WORKFLOW_EVENTS, type WorkflowEvent } from '#workflow/state/schema.ts';
|
|
11
11
|
|
|
12
|
-
export const getWorkflowEventByBuilderHandoff = ({
|
|
13
|
-
status,
|
|
14
|
-
}: BuilderHandoff): WorkflowEvent => {
|
|
12
|
+
export const getWorkflowEventByBuilderHandoff = ({ status }: BuilderHandoff): WorkflowEvent => {
|
|
15
13
|
if (status === BUILDER_HANDOFF_STATUSES.DONE) {
|
|
16
14
|
return WORKFLOW_EVENTS.BUILDER_DONE;
|
|
17
15
|
}
|
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];
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Objective: Save a validated simplifier handoff before completing the workflow.
|
|
3
|
+
* Used: When the simplifier records its single result.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { mkdir } from 'node:fs/promises';
|
|
7
|
+
import { dirname } from 'node:path';
|
|
8
|
+
|
|
9
|
+
import { assertSimplifierHandoff } from '#artifacts/simplifier-handoff/assertSimplifierHandoff.ts';
|
|
10
|
+
import type { MaestroPaths } from '#MaestroPaths.ts';
|
|
11
|
+
import { writeJson } from '#utils/write-json.ts';
|
|
12
|
+
import { readWorkflowState } from '#workflow/state/readWorkflowState.ts';
|
|
13
|
+
import { WORKFLOW_EVENTS, WORKFLOW_PHASES } from '#workflow/state/schema.ts';
|
|
14
|
+
import { writeWorkflowState } from '#workflow/state/writeWorkflowState.ts';
|
|
15
|
+
import { transitionWorkflow } from '#workflow/transitions.ts';
|
|
16
|
+
|
|
17
|
+
type CompleteSimplifierPassInput = {
|
|
18
|
+
paths: MaestroPaths;
|
|
19
|
+
specId: string;
|
|
20
|
+
handoff: unknown;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const completeSimplifierPass = async ({
|
|
24
|
+
paths,
|
|
25
|
+
specId,
|
|
26
|
+
handoff,
|
|
27
|
+
}: CompleteSimplifierPassInput) => {
|
|
28
|
+
const workflowPath = paths.getWorkflowPath(specId);
|
|
29
|
+
const state = await readWorkflowState(workflowPath);
|
|
30
|
+
|
|
31
|
+
if (state.specId !== specId) {
|
|
32
|
+
throw new Error(`Workflow spec ID mismatch: expected "${specId}", found "${state.specId}".`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (state.phase !== WORKFLOW_PHASES.SIMPLIFIER_RUNNING) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
`Simplifier handoff requires simplifier-running state, found "${state.phase}".`,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const input = { handoff, specId, projectRoot: paths.getProjectRoot() };
|
|
42
|
+
assertSimplifierHandoff(input);
|
|
43
|
+
|
|
44
|
+
const handoffPath = paths.getSimplifierHandoffPath(specId);
|
|
45
|
+
await mkdir(dirname(handoffPath), { recursive: true });
|
|
46
|
+
await writeJson({ path: handoffPath, data: input.handoff });
|
|
47
|
+
|
|
48
|
+
const nextState = transitionWorkflow({
|
|
49
|
+
state,
|
|
50
|
+
event: WORKFLOW_EVENTS.SIMPLIFIER_DONE,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
await writeWorkflowState({ path: workflowPath, state: nextState });
|
|
54
|
+
|
|
55
|
+
return { handoff: input.handoff, handoffPath, state: nextState };
|
|
56
|
+
};
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { readFile } from 'node:fs/promises';
|
|
7
|
+
|
|
7
8
|
import type { MaestroPaths } from '#MaestroPaths.ts';
|
|
8
9
|
import { pathExists } from '#utils/path-exists.ts';
|
|
9
10
|
import { readWorkflowState } from '#workflow/state/readWorkflowState.ts';
|
|
@@ -12,7 +13,8 @@ import { writeWorkflowState } from '#workflow/state/writeWorkflowState.ts';
|
|
|
12
13
|
import { transitionWorkflow } from '#workflow/transitions.ts';
|
|
13
14
|
|
|
14
15
|
function assertAcceptanceCriterionOrdering(markdown: string): void {
|
|
15
|
-
|
|
16
|
+
// Match standalone ACs (###), including regression checks, and ACs nested under requirements (####).
|
|
17
|
+
const criteria = [...markdown.matchAll(/^#{3,4} AC(\d+):/gm)];
|
|
16
18
|
|
|
17
19
|
if (criteria.some((match, index) => Number(match[1]) !== index + 1)) {
|
|
18
20
|
throw new Error('Fix acceptance criterion ordering in spec.md.');
|
|
@@ -46,13 +48,17 @@ export const markSpecReady = async ({
|
|
|
46
48
|
throw new Error(`Spec file is missing: ${specFilePath}.`);
|
|
47
49
|
}
|
|
48
50
|
|
|
51
|
+
const proposalFilePath = paths.getProposalFilePath(specId);
|
|
52
|
+
|
|
53
|
+
if (!(await pathExists(proposalFilePath))) {
|
|
54
|
+
throw new Error(`Proposal file is missing: ${proposalFilePath}.`);
|
|
55
|
+
}
|
|
56
|
+
|
|
49
57
|
const workflowPath = paths.getWorkflowPath(specId);
|
|
50
58
|
const current = await readWorkflowState(workflowPath);
|
|
51
59
|
|
|
52
60
|
if (current.specId !== specId) {
|
|
53
|
-
throw new Error(
|
|
54
|
-
`Workflow spec ID mismatch: expected "${specId}", found "${current.specId}".`,
|
|
55
|
-
);
|
|
61
|
+
throw new Error(`Workflow spec ID mismatch: expected "${specId}", found "${current.specId}".`);
|
|
56
62
|
}
|
|
57
63
|
|
|
58
64
|
const state = transitionWorkflow({
|
|
@@ -4,14 +4,9 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { readJsonFile } from '#utils/read-json.ts';
|
|
7
|
-
import {
|
|
8
|
-
assertWorkflowState,
|
|
9
|
-
type WorkflowState,
|
|
10
|
-
} from '#workflow/state/schema.ts';
|
|
7
|
+
import { assertWorkflowState, type WorkflowState } from '#workflow/state/schema.ts';
|
|
11
8
|
|
|
12
|
-
export const readWorkflowState = async (
|
|
13
|
-
path: string,
|
|
14
|
-
): Promise<WorkflowState> => {
|
|
9
|
+
export const readWorkflowState = async (path: string): Promise<WorkflowState> => {
|
|
15
10
|
const state = await readJsonFile({ path, description: 'Workflow state' });
|
|
16
11
|
assertWorkflowState(state);
|
|
17
12
|
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { type Static, Type } from 'typebox';
|
|
7
7
|
import { Value } from 'typebox/value';
|
|
8
|
+
|
|
8
9
|
import { isValidSpecId, SPEC_ID_PATTERN } from '#ids/isValidSpecId.ts';
|
|
9
10
|
|
|
10
11
|
export const WORKFLOW_STATE_VERSION = '1.0.0';
|
|
@@ -17,6 +18,7 @@ export const WORKFLOW_PHASES = {
|
|
|
17
18
|
READY_FOR_VERIFIER: 'ready-for-verifier',
|
|
18
19
|
VERIFIER_RUNNING: 'verifier-running',
|
|
19
20
|
FINDING_DECISIONS: 'finding-decisions',
|
|
21
|
+
SIMPLIFIER_RUNNING: 'simplifier-running',
|
|
20
22
|
CANDIDATE_READY: 'candidate-ready',
|
|
21
23
|
} as const;
|
|
22
24
|
|
|
@@ -30,6 +32,8 @@ export const WORKFLOW_EVENTS = {
|
|
|
30
32
|
RUN_VERIFIER: 'run-verifier',
|
|
31
33
|
VERIFIER_FOUND_FINDINGS: 'verifier-found-findings',
|
|
32
34
|
VERIFIER_DONE: 'verifier-done',
|
|
35
|
+
SIMPLIFIER_DONE: 'simplifier-done',
|
|
36
|
+
SIMPLIFIER_SKIPPED: 'simplifier-skipped',
|
|
33
37
|
OWNER_REJECT_FINDINGS: 'owner-reject-findings',
|
|
34
38
|
OWNER_REQUEST_FIXES: 'owner-request-fixes',
|
|
35
39
|
} as const;
|
|
@@ -42,6 +46,7 @@ export const WorkflowPhaseSchema = Type.Union([
|
|
|
42
46
|
Type.Literal(WORKFLOW_PHASES.READY_FOR_VERIFIER),
|
|
43
47
|
Type.Literal(WORKFLOW_PHASES.VERIFIER_RUNNING),
|
|
44
48
|
Type.Literal(WORKFLOW_PHASES.FINDING_DECISIONS),
|
|
49
|
+
Type.Literal(WORKFLOW_PHASES.SIMPLIFIER_RUNNING),
|
|
45
50
|
Type.Literal(WORKFLOW_PHASES.CANDIDATE_READY),
|
|
46
51
|
]);
|
|
47
52
|
|
|
@@ -54,17 +59,13 @@ export const WorkflowStateSchema = Type.Object(
|
|
|
54
59
|
{ additionalProperties: false },
|
|
55
60
|
);
|
|
56
61
|
|
|
57
|
-
export type WorkflowPhase =
|
|
58
|
-
(typeof WORKFLOW_PHASES)[keyof typeof WORKFLOW_PHASES];
|
|
62
|
+
export type WorkflowPhase = (typeof WORKFLOW_PHASES)[keyof typeof WORKFLOW_PHASES];
|
|
59
63
|
|
|
60
|
-
export type WorkflowEvent =
|
|
61
|
-
(typeof WORKFLOW_EVENTS)[keyof typeof WORKFLOW_EVENTS];
|
|
64
|
+
export type WorkflowEvent = (typeof WORKFLOW_EVENTS)[keyof typeof WORKFLOW_EVENTS];
|
|
62
65
|
|
|
63
66
|
export type WorkflowState = Static<typeof WorkflowStateSchema>;
|
|
64
67
|
|
|
65
|
-
function assertWorkflowStateSchema(
|
|
66
|
-
input: unknown,
|
|
67
|
-
): asserts input is WorkflowState {
|
|
68
|
+
function assertWorkflowStateSchema(input: unknown): asserts input is WorkflowState {
|
|
68
69
|
if (typeof input !== 'object' || input === null || Array.isArray(input)) {
|
|
69
70
|
throw new Error('Workflow state must be a JSON object.');
|
|
70
71
|
}
|
|
@@ -76,9 +77,7 @@ function assertWorkflowStateSchema(
|
|
|
76
77
|
}
|
|
77
78
|
}
|
|
78
79
|
|
|
79
|
-
export function assertWorkflowState(
|
|
80
|
-
input: unknown,
|
|
81
|
-
): asserts input is WorkflowState {
|
|
80
|
+
export function assertWorkflowState(input: unknown): asserts input is WorkflowState {
|
|
82
81
|
assertWorkflowStateSchema(input);
|
|
83
82
|
|
|
84
83
|
if (!isValidSpecId(input.specId)) {
|
|
@@ -4,10 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { writeJson } from '#utils/write-json.ts';
|
|
7
|
-
import {
|
|
8
|
-
assertWorkflowState,
|
|
9
|
-
type WorkflowState,
|
|
10
|
-
} from '#workflow/state/schema.ts';
|
|
7
|
+
import { assertWorkflowState, type WorkflowState } from '#workflow/state/schema.ts';
|
|
11
8
|
|
|
12
9
|
type WriteWorkflowStateInput = {
|
|
13
10
|
path: string;
|
|
@@ -35,10 +35,7 @@ export const WORKFLOW_TRANSITIONS: Record<WorkflowEvent, WorkflowTransition> = {
|
|
|
35
35
|
to: WORKFLOW_PHASES.READY_FOR_BUILDER,
|
|
36
36
|
},
|
|
37
37
|
[WORKFLOW_EVENTS.OWNER_REQUEST_SPEC_REVISION]: {
|
|
38
|
-
from: [
|
|
39
|
-
WORKFLOW_PHASES.ESCALATION_DECISIONS,
|
|
40
|
-
WORKFLOW_PHASES.FINDING_DECISIONS,
|
|
41
|
-
],
|
|
38
|
+
from: [WORKFLOW_PHASES.ESCALATION_DECISIONS, WORKFLOW_PHASES.FINDING_DECISIONS],
|
|
42
39
|
to: WORKFLOW_PHASES.DRAFTING_SPEC,
|
|
43
40
|
},
|
|
44
41
|
[WORKFLOW_EVENTS.BUILDER_DONE]: {
|
|
@@ -55,10 +52,18 @@ export const WORKFLOW_TRANSITIONS: Record<WorkflowEvent, WorkflowTransition> = {
|
|
|
55
52
|
},
|
|
56
53
|
[WORKFLOW_EVENTS.VERIFIER_DONE]: {
|
|
57
54
|
from: [WORKFLOW_PHASES.VERIFIER_RUNNING],
|
|
58
|
-
to: WORKFLOW_PHASES.
|
|
55
|
+
to: WORKFLOW_PHASES.SIMPLIFIER_RUNNING,
|
|
59
56
|
},
|
|
60
57
|
[WORKFLOW_EVENTS.OWNER_REJECT_FINDINGS]: {
|
|
61
58
|
from: [WORKFLOW_PHASES.FINDING_DECISIONS],
|
|
59
|
+
to: WORKFLOW_PHASES.SIMPLIFIER_RUNNING,
|
|
60
|
+
},
|
|
61
|
+
[WORKFLOW_EVENTS.SIMPLIFIER_DONE]: {
|
|
62
|
+
from: [WORKFLOW_PHASES.SIMPLIFIER_RUNNING],
|
|
63
|
+
to: WORKFLOW_PHASES.CANDIDATE_READY,
|
|
64
|
+
},
|
|
65
|
+
[WORKFLOW_EVENTS.SIMPLIFIER_SKIPPED]: {
|
|
66
|
+
from: [WORKFLOW_PHASES.SIMPLIFIER_RUNNING],
|
|
62
67
|
to: WORKFLOW_PHASES.CANDIDATE_READY,
|
|
63
68
|
},
|
|
64
69
|
[WORKFLOW_EVENTS.OWNER_REQUEST_FIXES]: {
|
|
@@ -72,10 +77,7 @@ type TransitionWorkflowInput = {
|
|
|
72
77
|
event: WorkflowEvent;
|
|
73
78
|
};
|
|
74
79
|
|
|
75
|
-
export const transitionWorkflow = ({
|
|
76
|
-
state,
|
|
77
|
-
event,
|
|
78
|
-
}: TransitionWorkflowInput): WorkflowState => {
|
|
80
|
+
export const transitionWorkflow = ({ state, event }: TransitionWorkflowInput): WorkflowState => {
|
|
79
81
|
assertWorkflowState(state);
|
|
80
82
|
const transition = WORKFLOW_TRANSITIONS[event];
|
|
81
83
|
|
|
@@ -84,9 +86,7 @@ export const transitionWorkflow = ({
|
|
|
84
86
|
}
|
|
85
87
|
|
|
86
88
|
if (!transition.from.includes(state.phase)) {
|
|
87
|
-
throw new Error(
|
|
88
|
-
`Workflow event "${event}" is not allowed from phase "${state.phase}".`,
|
|
89
|
-
);
|
|
89
|
+
throw new Error(`Workflow event "${event}" is not allowed from phase "${state.phase}".`);
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
return {
|
|
@@ -12,7 +12,8 @@ export function assertSpecAcceptanceCriteria({
|
|
|
12
12
|
markdown,
|
|
13
13
|
criterionIds,
|
|
14
14
|
}: AssertSpecAcceptanceCriteriaInput): void {
|
|
15
|
-
|
|
15
|
+
// Match standalone ACs (###), including regression checks, and ACs nested under requirements (####).
|
|
16
|
+
const missingIds = [...markdown.matchAll(/^#{3,4} AC(\d+):/gm)]
|
|
16
17
|
.map((match) => `AC${match[1]}`)
|
|
17
18
|
.filter((id) => !criterionIds.has(id));
|
|
18
19
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Objective: Resolve the agent responsible for a workflow phase.
|
|
3
|
-
* Used: When workflow logic maps persisted phases to
|
|
3
|
+
* Used: When workflow logic maps persisted phases to agent roles.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import type { WorkflowRole } from '#workflow/roles.ts';
|
|
@@ -18,9 +18,7 @@ const VERIFIER_PHASES: readonly WorkflowState['phase'][] = [
|
|
|
18
18
|
WORKFLOW_PHASES.FINDING_DECISIONS,
|
|
19
19
|
];
|
|
20
20
|
|
|
21
|
-
export const getAgentRole = (
|
|
22
|
-
phase: WorkflowState['phase'],
|
|
23
|
-
): WorkflowRole | null => {
|
|
21
|
+
export const getAgentRole = (phase: WorkflowState['phase']): WorkflowRole | null => {
|
|
24
22
|
if (BUILDER_PHASES.includes(phase)) {
|
|
25
23
|
return WORKFLOW_ROLES.BUILDER;
|
|
26
24
|
}
|
|
@@ -29,5 +27,9 @@ export const getAgentRole = (
|
|
|
29
27
|
return WORKFLOW_ROLES.VERIFIER;
|
|
30
28
|
}
|
|
31
29
|
|
|
30
|
+
if (phase === WORKFLOW_PHASES.SIMPLIFIER_RUNNING) {
|
|
31
|
+
return WORKFLOW_ROLES.SIMPLIFIER;
|
|
32
|
+
}
|
|
33
|
+
|
|
32
34
|
return null;
|
|
33
35
|
};
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { mkdir, readFile } from 'node:fs/promises';
|
|
7
|
+
|
|
7
8
|
import { assertVerifierHandoff } from '#artifacts/verifier-handoff/assertVerifierHandoff.ts';
|
|
8
9
|
import type { VerifierHandoff } from '#artifacts/verifier-handoff/schema.ts';
|
|
9
10
|
import { writeVerifierHandoff } from '#artifacts/verifier-handoff/writeVerifierHandoff.ts';
|
|
@@ -5,10 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { assertVerifierHandoff } from '#artifacts/verifier-handoff/assertVerifierHandoff.ts';
|
|
7
7
|
import { readVerifierHandoff } from '#artifacts/verifier-handoff/readVerifierHandoff.ts';
|
|
8
|
-
import type {
|
|
9
|
-
VerifierFinding,
|
|
10
|
-
VerifierHandoff,
|
|
11
|
-
} from '#artifacts/verifier-handoff/schema.ts';
|
|
8
|
+
import type { VerifierFinding, VerifierHandoff } from '#artifacts/verifier-handoff/schema.ts';
|
|
12
9
|
import { FINDING_DECISIONS } from '#artifacts/verifier-handoff/schema.ts';
|
|
13
10
|
import type { MaestroPaths } from '#MaestroPaths.ts';
|
|
14
11
|
import { writeJson } from '#utils/write-json.ts';
|
|
@@ -48,20 +45,13 @@ const applyFindingDecisions = ({
|
|
|
48
45
|
}
|
|
49
46
|
|
|
50
47
|
if (decisionsById.has(decision.findingId)) {
|
|
51
|
-
throw new Error(
|
|
52
|
-
`Duplicate decision for finding "${decision.findingId}".`,
|
|
53
|
-
);
|
|
48
|
+
throw new Error(`Duplicate decision for finding "${decision.findingId}".`);
|
|
54
49
|
}
|
|
55
50
|
|
|
56
51
|
decisionsById.set(decision.findingId, decision);
|
|
57
52
|
|
|
58
|
-
if (
|
|
59
|
-
decision.
|
|
60
|
-
decision.reason.trim().length === 0
|
|
61
|
-
) {
|
|
62
|
-
throw new Error(
|
|
63
|
-
`Rejection reason for "${decision.findingId}" must not be empty.`,
|
|
64
|
-
);
|
|
53
|
+
if (decision.decision === FINDING_DECISIONS.REJECT && decision.reason.trim().length === 0) {
|
|
54
|
+
throw new Error(`Rejection reason for "${decision.findingId}" must not be empty.`);
|
|
65
55
|
}
|
|
66
56
|
}
|
|
67
57
|
|
|
@@ -94,15 +84,11 @@ const readFindingWorkflow = async ({
|
|
|
94
84
|
const state = await readWorkflowState(paths.getWorkflowPath(specId));
|
|
95
85
|
|
|
96
86
|
if (state.specId !== specId) {
|
|
97
|
-
throw new Error(
|
|
98
|
-
`Workflow spec ID mismatch: expected "${specId}", found "${state.specId}".`,
|
|
99
|
-
);
|
|
87
|
+
throw new Error(`Workflow spec ID mismatch: expected "${specId}", found "${state.specId}".`);
|
|
100
88
|
}
|
|
101
89
|
|
|
102
90
|
if (state.phase !== WORKFLOW_PHASES.FINDING_DECISIONS) {
|
|
103
|
-
throw new Error(
|
|
104
|
-
`Finding resolution requires finding-decisions state, found "${state.phase}".`,
|
|
105
|
-
);
|
|
91
|
+
throw new Error(`Finding resolution requires finding-decisions state, found "${state.phase}".`);
|
|
106
92
|
}
|
|
107
93
|
|
|
108
94
|
const handoff = await readVerifierHandoff({
|
|
@@ -111,23 +97,17 @@ const readFindingWorkflow = async ({
|
|
|
111
97
|
});
|
|
112
98
|
|
|
113
99
|
if (handoff.findings.length === 0) {
|
|
114
|
-
throw new Error(
|
|
115
|
-
'Finding resolution requires at least one current finding.',
|
|
116
|
-
);
|
|
100
|
+
throw new Error('Finding resolution requires at least one current finding.');
|
|
117
101
|
}
|
|
118
102
|
|
|
119
103
|
if (handoff.findings.some(({ decision }) => decision !== null)) {
|
|
120
|
-
throw new Error(
|
|
121
|
-
'Current verifier findings already contain an owner decision.',
|
|
122
|
-
);
|
|
104
|
+
throw new Error('Current verifier findings already contain an owner decision.');
|
|
123
105
|
}
|
|
124
106
|
|
|
125
107
|
return { state, handoff };
|
|
126
108
|
};
|
|
127
109
|
|
|
128
|
-
const getSavedFindingDecision = (
|
|
129
|
-
decision: FindingDecision | undefined,
|
|
130
|
-
): SavedFindingDecision => {
|
|
110
|
+
const getSavedFindingDecision = (decision: FindingDecision | undefined): SavedFindingDecision => {
|
|
131
111
|
if (decision?.decision === FINDING_DECISIONS.REVISE_SPEC) {
|
|
132
112
|
return {
|
|
133
113
|
decision: FINDING_DECISIONS.REVISE_SPEC,
|
|
@@ -3,15 +3,10 @@
|
|
|
3
3
|
* Used: When a verifier completes a pass or the owner resolves its findings.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
FINDING_DECISIONS,
|
|
8
|
-
type VerifierHandoff,
|
|
9
|
-
} from '#artifacts/verifier-handoff/schema.ts';
|
|
6
|
+
import { FINDING_DECISIONS, type VerifierHandoff } from '#artifacts/verifier-handoff/schema.ts';
|
|
10
7
|
import { WORKFLOW_EVENTS, type WorkflowEvent } from '#workflow/state/schema.ts';
|
|
11
8
|
|
|
12
|
-
export const getWorkflowEventByVerifierHandoff = (
|
|
13
|
-
handoff: VerifierHandoff,
|
|
14
|
-
): WorkflowEvent => {
|
|
9
|
+
export const getWorkflowEventByVerifierHandoff = (handoff: VerifierHandoff): WorkflowEvent => {
|
|
15
10
|
if (handoff.findings.length === 0) {
|
|
16
11
|
return WORKFLOW_EVENTS.VERIFIER_DONE;
|
|
17
12
|
}
|
|
@@ -21,26 +16,16 @@ export const getWorkflowEventByVerifierHandoff = (
|
|
|
21
16
|
}
|
|
22
17
|
|
|
23
18
|
if (
|
|
24
|
-
handoff.findings.some(
|
|
25
|
-
({ decision }) => decision?.decision === FINDING_DECISIONS.REVISE_SPEC,
|
|
26
|
-
)
|
|
19
|
+
handoff.findings.some(({ decision }) => decision?.decision === FINDING_DECISIONS.REVISE_SPEC)
|
|
27
20
|
) {
|
|
28
21
|
return WORKFLOW_EVENTS.OWNER_REQUEST_SPEC_REVISION;
|
|
29
22
|
}
|
|
30
23
|
|
|
31
|
-
if (
|
|
32
|
-
handoff.findings.some(
|
|
33
|
-
({ decision }) => decision?.decision === FINDING_DECISIONS.FIX_CODE,
|
|
34
|
-
)
|
|
35
|
-
) {
|
|
24
|
+
if (handoff.findings.some(({ decision }) => decision?.decision === FINDING_DECISIONS.FIX_CODE)) {
|
|
36
25
|
return WORKFLOW_EVENTS.OWNER_REQUEST_FIXES;
|
|
37
26
|
}
|
|
38
27
|
|
|
39
|
-
if (
|
|
40
|
-
handoff.findings.every(
|
|
41
|
-
({ decision }) => decision?.decision === FINDING_DECISIONS.REJECT,
|
|
42
|
-
)
|
|
43
|
-
) {
|
|
28
|
+
if (handoff.findings.every(({ decision }) => decision?.decision === FINDING_DECISIONS.REJECT)) {
|
|
44
29
|
return WORKFLOW_EVENTS.OWNER_REJECT_FINDINGS;
|
|
45
30
|
}
|
|
46
31
|
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# <id>: <short, outcome-oriented title>
|
|
2
|
+
|
|
3
|
+
<!--
|
|
4
|
+
Create this document only when significant technical decisions must be preserved.
|
|
5
|
+
Record approved architectural decisions, important technical constraints, or prototype choices, not routine implementation details.
|
|
6
|
+
Keep motivation and scope in proposal.md. Keep required behavior, edge cases, and acceptance criteria in spec.md.
|
|
7
|
+
Use only the sections that matter. Omit empty sections and these instructions.
|
|
8
|
+
-->
|
|
9
|
+
|
|
10
|
+
## Decisions
|
|
11
|
+
|
|
12
|
+
<Describe each approved technical decision and its brief reason. Include only alternatives actually considered and consequences that matter. Link related REQ IDs in spec.md instead of repeating requirements.>
|
|
13
|
+
|
|
14
|
+
## Contracts and prototypes
|
|
15
|
+
|
|
16
|
+
<Include interface or data contracts and links to prototypes only when needed to explain the approved design. Keep observable behavior in spec.md and distinguish illustrative prototype content from requirements.>
|
|
17
|
+
|
|
18
|
+
## Risks and limits
|
|
19
|
+
|
|
20
|
+
<Record relevant technical risks, migration or rollback constraints, and evidence supporting the decision. Do not add investigation logs. Resolve decisions affecting the contract with the owner before approval.>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# <id>: <short, outcome-oriented title>
|
|
2
|
+
|
|
3
|
+
<!--
|
|
4
|
+
Explain why this change exists and where its scope ends. Keep it brief and write for the owner.
|
|
5
|
+
Keep detailed behavior and acceptance criteria in spec.md, and approved technical decisions in design.md when needed.
|
|
6
|
+
Omit empty sections and these instructions. Do not duplicate requirements or investigation logs.
|
|
7
|
+
-->
|
|
8
|
+
|
|
9
|
+
## Problem
|
|
10
|
+
|
|
11
|
+
<Describe the current problem, who is affected, and why the change is needed.>
|
|
12
|
+
|
|
13
|
+
## Goal and scope
|
|
14
|
+
|
|
15
|
+
<State the intended outcome and the boundaries of this one reversible change.>
|
|
16
|
+
|
|
17
|
+
## Out of scope
|
|
18
|
+
|
|
19
|
+
<Name related changes that are explicitly excluded.>
|
|
20
|
+
|
|
21
|
+
## Required behavior
|
|
22
|
+
|
|
23
|
+
See [spec.md](spec.md).
|