@emiliosp/pi-maestro 0.9.5 → 0.10.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/CHANGELOG.md +21 -0
- package/README.md +7 -2
- package/ROADMAP.md +131 -10
- package/agents/verifier.md +1 -1
- package/docs/glossary.md +1 -0
- package/docs/workflow.md +34 -22
- package/package.json +1 -1
- package/src/MaestroPaths.ts +4 -32
- package/src/artifacts/builder-handoff/assertBuilderHandoff.ts +23 -29
- package/src/artifacts/verifier-handoff/assertVerifierHandoff.ts +14 -18
- package/src/artifacts/verifier-handoff/schema.ts +9 -0
- package/src/config/loadConfiguration.ts +20 -50
- package/src/maestro/instructions/getMaestroInstructions.ts +40 -22
- package/src/maestro/status/formatMaestroStatus.ts +6 -6
- package/src/specs/create.ts +7 -24
- package/src/tools/main/mark-spec-ready.ts +1 -1
- package/src/tools/main/resolve-escalations.ts +11 -4
- package/src/tools/main/resolve-findings.ts +30 -3
- package/src/tools/main/run-builder.ts +39 -49
- package/src/tools/main/run-verifier.ts +12 -23
- package/src/utils/path-within-or-equal.ts +3 -4
- package/src/workflow/builder/resolveEscalations.ts +50 -31
- package/src/workflow/builder/utils/getWorkflowEventByBuilderHandoff.ts +1 -1
- package/src/workflow/builder/utils/getWorkflowEventBySpecChangeRequired.ts +16 -0
- package/src/workflow/state/schema.ts +10 -9
- package/src/workflow/transitions.ts +22 -20
- package/src/workflow/utils/getAgentRole.ts +2 -2
- package/src/workflow/verifier/resolveFindings.ts +56 -69
- package/src/workflow/verifier/utils/getWorkflowEventByVerifierHandoff.ts +11 -3
- package/templates/spec.md +9 -5
|
@@ -39,22 +39,17 @@ const RunVerifierToolParameters = Type.Object(
|
|
|
39
39
|
|
|
40
40
|
const MILLISECONDS_PER_MINUTE = 60_000;
|
|
41
41
|
|
|
42
|
-
type
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
phase: typeof WORKFLOW_PHASES.CANDIDATE_READY;
|
|
46
|
-
handoff: VerifierHandoff;
|
|
47
|
-
};
|
|
42
|
+
type VerifierTerminalPhase =
|
|
43
|
+
| typeof WORKFLOW_PHASES.CANDIDATE_READY
|
|
44
|
+
| typeof WORKFLOW_PHASES.FINDING_DECISIONS;
|
|
48
45
|
|
|
49
|
-
type
|
|
50
|
-
outcome:
|
|
46
|
+
type VerifierRunResult = {
|
|
47
|
+
outcome: VerifierTerminalPhase;
|
|
51
48
|
specId: string;
|
|
52
|
-
phase:
|
|
49
|
+
phase: VerifierTerminalPhase;
|
|
53
50
|
handoff: VerifierHandoff;
|
|
54
51
|
};
|
|
55
52
|
|
|
56
|
-
type VerifierRunResult = CandidateReadyResult | FindingsResult;
|
|
57
|
-
|
|
58
53
|
type ReadTerminalVerifierResultInput = {
|
|
59
54
|
paths: MaestroPaths;
|
|
60
55
|
specId: string;
|
|
@@ -65,12 +60,10 @@ const readTerminalVerifierResult = async ({
|
|
|
65
60
|
paths,
|
|
66
61
|
specId,
|
|
67
62
|
state,
|
|
68
|
-
}: ReadTerminalVerifierResultInput): Promise<
|
|
69
|
-
CandidateReadyResult | FindingsResult
|
|
70
|
-
> => {
|
|
63
|
+
}: ReadTerminalVerifierResultInput): Promise<VerifierRunResult> => {
|
|
71
64
|
if (
|
|
72
65
|
state.phase !== WORKFLOW_PHASES.CANDIDATE_READY &&
|
|
73
|
-
state.phase !== WORKFLOW_PHASES.
|
|
66
|
+
state.phase !== WORKFLOW_PHASES.FINDING_DECISIONS
|
|
74
67
|
) {
|
|
75
68
|
throw new Error(
|
|
76
69
|
`Verifier returned with unexpected workflow phase "${state.phase}".`,
|
|
@@ -84,7 +77,7 @@ const readTerminalVerifierResult = async ({
|
|
|
84
77
|
|
|
85
78
|
const expectedPhase =
|
|
86
79
|
handoff.findings.length > 0
|
|
87
|
-
? WORKFLOW_PHASES.
|
|
80
|
+
? WORKFLOW_PHASES.FINDING_DECISIONS
|
|
88
81
|
: WORKFLOW_PHASES.CANDIDATE_READY;
|
|
89
82
|
|
|
90
83
|
if (state.phase !== expectedPhase) {
|
|
@@ -93,16 +86,12 @@ const readTerminalVerifierResult = async ({
|
|
|
93
86
|
);
|
|
94
87
|
}
|
|
95
88
|
|
|
96
|
-
|
|
89
|
+
return {
|
|
97
90
|
specId,
|
|
98
91
|
handoff,
|
|
92
|
+
outcome: state.phase,
|
|
93
|
+
phase: state.phase,
|
|
99
94
|
};
|
|
100
|
-
|
|
101
|
-
if (state.phase === WORKFLOW_PHASES.CANDIDATE_READY) {
|
|
102
|
-
return { ...details, outcome: state.phase, phase: state.phase };
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
return { ...details, outcome: state.phase, phase: state.phase };
|
|
106
95
|
};
|
|
107
96
|
|
|
108
97
|
type ReadVerifierResultInput = {
|
|
@@ -17,9 +17,8 @@ export const isPathWithinOrEqual = ({
|
|
|
17
17
|
const pathToCandidate = relative(parent, candidate);
|
|
18
18
|
|
|
19
19
|
return (
|
|
20
|
-
pathToCandidate
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
!isAbsolute(pathToCandidate))
|
|
20
|
+
!pathToCandidate.startsWith(`..${sep}`) &&
|
|
21
|
+
pathToCandidate !== '..' &&
|
|
22
|
+
!isAbsolute(pathToCandidate)
|
|
24
23
|
);
|
|
25
24
|
};
|
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Objective: Save all owner resolutions in the active builder handoff.
|
|
3
|
-
* Used: When the owner
|
|
3
|
+
* Used: When the owner resolves current questions, with or without spec revision.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { readBuilderHandoff } from '#artifacts/builder-handoff/readBuilderHandoff.ts';
|
|
7
7
|
import {
|
|
8
8
|
BUILDER_HANDOFF_STATUSES,
|
|
9
|
+
type Escalation,
|
|
9
10
|
type EscalationResolution,
|
|
10
11
|
} from '#artifacts/builder-handoff/schema.ts';
|
|
11
12
|
import { writeBuilderHandoff } from '#artifacts/builder-handoff/writeBuilderHandoff.ts';
|
|
12
13
|
import type { MaestroPaths } from '#MaestroPaths.ts';
|
|
14
|
+
import { getWorkflowEventBySpecChangeRequired } from '#workflow/builder/utils/getWorkflowEventBySpecChangeRequired.ts';
|
|
13
15
|
import { readWorkflowState } from '#workflow/state/readWorkflowState.ts';
|
|
14
|
-
import {
|
|
16
|
+
import { WORKFLOW_PHASES } from '#workflow/state/schema.ts';
|
|
15
17
|
import { writeWorkflowState } from '#workflow/state/writeWorkflowState.ts';
|
|
16
18
|
import { transitionWorkflow } from '#workflow/transitions.ts';
|
|
17
19
|
|
|
@@ -19,43 +21,23 @@ export type EscalationDecision = EscalationResolution & {
|
|
|
19
21
|
escalationId: string;
|
|
20
22
|
};
|
|
21
23
|
|
|
22
|
-
type
|
|
23
|
-
|
|
24
|
-
specId: string;
|
|
24
|
+
type ApplyEscalationDecisionsInput = {
|
|
25
|
+
escalations: Escalation[];
|
|
25
26
|
decisions: EscalationDecision[];
|
|
26
27
|
};
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
specId,
|
|
29
|
+
const applyEscalationDecisions = ({
|
|
30
|
+
escalations,
|
|
31
31
|
decisions,
|
|
32
|
-
}:
|
|
33
|
-
|
|
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)) {
|
|
32
|
+
}: ApplyEscalationDecisionsInput) => {
|
|
33
|
+
if (escalations.some(({ resolution }) => resolution !== null)) {
|
|
52
34
|
throw new Error('Current questions already contain an owner resolution.');
|
|
53
35
|
}
|
|
54
36
|
|
|
55
37
|
if (decisions.length === 0)
|
|
56
38
|
throw new Error('At least one escalation decision is required.');
|
|
57
39
|
|
|
58
|
-
const expectedIds = new Set(
|
|
40
|
+
const expectedIds = new Set(escalations.map(({ id }) => id));
|
|
59
41
|
const resolutions = new Map<string, EscalationResolution>();
|
|
60
42
|
|
|
61
43
|
for (const { escalationId, ...resolution } of decisions) {
|
|
@@ -68,7 +50,7 @@ export const resolveEscalations = async ({
|
|
|
68
50
|
resolutions.set(escalationId, resolution);
|
|
69
51
|
}
|
|
70
52
|
|
|
71
|
-
|
|
53
|
+
return escalations.map((escalation) => {
|
|
72
54
|
const resolution = resolutions.get(escalation.id);
|
|
73
55
|
|
|
74
56
|
if (resolution === undefined)
|
|
@@ -76,12 +58,49 @@ export const resolveEscalations = async ({
|
|
|
76
58
|
|
|
77
59
|
return { ...escalation, resolution };
|
|
78
60
|
});
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
type ResolveEscalationsInput = {
|
|
64
|
+
paths: MaestroPaths;
|
|
65
|
+
specId: string;
|
|
66
|
+
specChangeRequired: boolean;
|
|
67
|
+
decisions: EscalationDecision[];
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export const resolveEscalations = async ({
|
|
71
|
+
paths,
|
|
72
|
+
specId,
|
|
73
|
+
specChangeRequired,
|
|
74
|
+
decisions,
|
|
75
|
+
}: ResolveEscalationsInput) => {
|
|
76
|
+
const workflowPath = paths.getWorkflowPath(specId);
|
|
77
|
+
const state = await readWorkflowState(workflowPath);
|
|
78
|
+
|
|
79
|
+
if (state.specId !== specId) throw new Error('Workflow spec ID mismatch.');
|
|
80
|
+
|
|
81
|
+
if (state.phase !== WORKFLOW_PHASES.ESCALATION_DECISIONS) {
|
|
82
|
+
throw new Error(
|
|
83
|
+
`Escalation resolution requires escalation-decisions state, found "${state.phase}".`,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const handoffPath = await paths.getActiveBuilderHandoffPath(specId);
|
|
88
|
+
const handoff = await readBuilderHandoff({ path: handoffPath, specId });
|
|
89
|
+
|
|
90
|
+
if (handoff.status !== BUILDER_HANDOFF_STATUSES.ESCALATION) {
|
|
91
|
+
throw new Error('Escalation resolution requires an escalation handoff.');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const escalations = applyEscalationDecisions({
|
|
95
|
+
escalations: handoff.escalations,
|
|
96
|
+
decisions,
|
|
97
|
+
});
|
|
79
98
|
|
|
80
99
|
const nextHandoff = { ...handoff, escalations };
|
|
81
100
|
|
|
82
101
|
const nextState = transitionWorkflow({
|
|
83
102
|
state,
|
|
84
|
-
event:
|
|
103
|
+
event: getWorkflowEventBySpecChangeRequired(specChangeRequired),
|
|
85
104
|
});
|
|
86
105
|
|
|
87
106
|
// The artifact writer validates every resolution before the first protocol write.
|
|
@@ -17,7 +17,7 @@ export const getWorkflowEventByBuilderHandoff = ({
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
if (status === BUILDER_HANDOFF_STATUSES.ESCALATION) {
|
|
20
|
-
return WORKFLOW_EVENTS.
|
|
20
|
+
return WORKFLOW_EVENTS.BUILDER_OPEN_ESCALATIONS;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
throw new Error(`Unexpected builder handoff status: "${status}".`);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Objective: Select the workflow event for resolving escalations.
|
|
3
|
+
* Used: When owner answers determine whether the spec must change.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { WORKFLOW_EVENTS, type WorkflowEvent } from '#workflow/state/schema.ts';
|
|
7
|
+
|
|
8
|
+
export const getWorkflowEventBySpecChangeRequired = (
|
|
9
|
+
specChangeRequired: boolean,
|
|
10
|
+
): WorkflowEvent => {
|
|
11
|
+
if (specChangeRequired) {
|
|
12
|
+
return WORKFLOW_EVENTS.OWNER_REQUEST_SPEC_REVISION;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return WORKFLOW_EVENTS.RESOLVE_ESCALATIONS;
|
|
16
|
+
};
|
|
@@ -13,34 +13,35 @@ export const WORKFLOW_PHASES = {
|
|
|
13
13
|
DRAFTING_SPEC: 'drafting-spec',
|
|
14
14
|
READY_FOR_BUILDER: 'ready-for-builder',
|
|
15
15
|
BUILDER_RUNNING: 'builder-running',
|
|
16
|
-
|
|
16
|
+
ESCALATION_DECISIONS: 'escalation-decisions',
|
|
17
17
|
READY_FOR_VERIFIER: 'ready-for-verifier',
|
|
18
18
|
VERIFIER_RUNNING: 'verifier-running',
|
|
19
|
-
|
|
19
|
+
FINDING_DECISIONS: 'finding-decisions',
|
|
20
20
|
CANDIDATE_READY: 'candidate-ready',
|
|
21
21
|
} as const;
|
|
22
22
|
|
|
23
23
|
export const WORKFLOW_EVENTS = {
|
|
24
24
|
MARK_SPEC_READY: 'mark-spec-ready',
|
|
25
25
|
RUN_BUILDER: 'run-builder',
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
BUILDER_OPEN_ESCALATIONS: 'builder-open-escalations',
|
|
27
|
+
RESOLVE_ESCALATIONS: 'resolve-escalations',
|
|
28
|
+
OWNER_REQUEST_SPEC_REVISION: 'owner-request-spec-revision',
|
|
28
29
|
BUILDER_DONE: 'builder-done',
|
|
29
30
|
RUN_VERIFIER: 'run-verifier',
|
|
30
31
|
VERIFIER_FOUND_FINDINGS: 'verifier-found-findings',
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
VERIFIER_DONE: 'verifier-done',
|
|
33
|
+
OWNER_REJECT_FINDINGS: 'owner-reject-findings',
|
|
34
|
+
OWNER_REQUEST_FIXES: 'owner-request-fixes',
|
|
34
35
|
} as const;
|
|
35
36
|
|
|
36
37
|
export const WorkflowPhaseSchema = Type.Union([
|
|
37
38
|
Type.Literal(WORKFLOW_PHASES.DRAFTING_SPEC),
|
|
38
39
|
Type.Literal(WORKFLOW_PHASES.READY_FOR_BUILDER),
|
|
39
40
|
Type.Literal(WORKFLOW_PHASES.BUILDER_RUNNING),
|
|
40
|
-
Type.Literal(WORKFLOW_PHASES.
|
|
41
|
+
Type.Literal(WORKFLOW_PHASES.ESCALATION_DECISIONS),
|
|
41
42
|
Type.Literal(WORKFLOW_PHASES.READY_FOR_VERIFIER),
|
|
42
43
|
Type.Literal(WORKFLOW_PHASES.VERIFIER_RUNNING),
|
|
43
|
-
Type.Literal(WORKFLOW_PHASES.
|
|
44
|
+
Type.Literal(WORKFLOW_PHASES.FINDING_DECISIONS),
|
|
44
45
|
Type.Literal(WORKFLOW_PHASES.CANDIDATE_READY),
|
|
45
46
|
]);
|
|
46
47
|
|
|
@@ -19,25 +19,28 @@ type WorkflowTransition = {
|
|
|
19
19
|
|
|
20
20
|
export const WORKFLOW_TRANSITIONS: Record<WorkflowEvent, WorkflowTransition> = {
|
|
21
21
|
[WORKFLOW_EVENTS.MARK_SPEC_READY]: {
|
|
22
|
-
from: [
|
|
23
|
-
WORKFLOW_PHASES.DRAFTING_SPEC,
|
|
24
|
-
WORKFLOW_PHASES.ESCALATION_DECISION,
|
|
25
|
-
WORKFLOW_PHASES.FINDINGS_DECISION,
|
|
26
|
-
],
|
|
22
|
+
from: [WORKFLOW_PHASES.DRAFTING_SPEC],
|
|
27
23
|
to: WORKFLOW_PHASES.READY_FOR_BUILDER,
|
|
28
24
|
},
|
|
29
25
|
[WORKFLOW_EVENTS.RUN_BUILDER]: {
|
|
30
26
|
from: [WORKFLOW_PHASES.READY_FOR_BUILDER],
|
|
31
27
|
to: WORKFLOW_PHASES.BUILDER_RUNNING,
|
|
32
28
|
},
|
|
33
|
-
[WORKFLOW_EVENTS.
|
|
29
|
+
[WORKFLOW_EVENTS.BUILDER_OPEN_ESCALATIONS]: {
|
|
34
30
|
from: [WORKFLOW_PHASES.BUILDER_RUNNING],
|
|
35
|
-
to: WORKFLOW_PHASES.
|
|
31
|
+
to: WORKFLOW_PHASES.ESCALATION_DECISIONS,
|
|
36
32
|
},
|
|
37
|
-
[WORKFLOW_EVENTS.
|
|
38
|
-
from: [WORKFLOW_PHASES.
|
|
33
|
+
[WORKFLOW_EVENTS.RESOLVE_ESCALATIONS]: {
|
|
34
|
+
from: [WORKFLOW_PHASES.ESCALATION_DECISIONS],
|
|
39
35
|
to: WORKFLOW_PHASES.READY_FOR_BUILDER,
|
|
40
36
|
},
|
|
37
|
+
[WORKFLOW_EVENTS.OWNER_REQUEST_SPEC_REVISION]: {
|
|
38
|
+
from: [
|
|
39
|
+
WORKFLOW_PHASES.ESCALATION_DECISIONS,
|
|
40
|
+
WORKFLOW_PHASES.FINDING_DECISIONS,
|
|
41
|
+
],
|
|
42
|
+
to: WORKFLOW_PHASES.DRAFTING_SPEC,
|
|
43
|
+
},
|
|
41
44
|
[WORKFLOW_EVENTS.BUILDER_DONE]: {
|
|
42
45
|
from: [WORKFLOW_PHASES.BUILDER_RUNNING],
|
|
43
46
|
to: WORKFLOW_PHASES.READY_FOR_VERIFIER,
|
|
@@ -48,18 +51,18 @@ export const WORKFLOW_TRANSITIONS: Record<WorkflowEvent, WorkflowTransition> = {
|
|
|
48
51
|
},
|
|
49
52
|
[WORKFLOW_EVENTS.VERIFIER_FOUND_FINDINGS]: {
|
|
50
53
|
from: [WORKFLOW_PHASES.VERIFIER_RUNNING],
|
|
51
|
-
to: WORKFLOW_PHASES.
|
|
54
|
+
to: WORKFLOW_PHASES.FINDING_DECISIONS,
|
|
52
55
|
},
|
|
53
|
-
[WORKFLOW_EVENTS.
|
|
56
|
+
[WORKFLOW_EVENTS.VERIFIER_DONE]: {
|
|
54
57
|
from: [WORKFLOW_PHASES.VERIFIER_RUNNING],
|
|
55
58
|
to: WORKFLOW_PHASES.CANDIDATE_READY,
|
|
56
59
|
},
|
|
57
|
-
[WORKFLOW_EVENTS.
|
|
58
|
-
from: [WORKFLOW_PHASES.
|
|
60
|
+
[WORKFLOW_EVENTS.OWNER_REJECT_FINDINGS]: {
|
|
61
|
+
from: [WORKFLOW_PHASES.FINDING_DECISIONS],
|
|
59
62
|
to: WORKFLOW_PHASES.CANDIDATE_READY,
|
|
60
63
|
},
|
|
61
|
-
[WORKFLOW_EVENTS.
|
|
62
|
-
from: [WORKFLOW_PHASES.
|
|
64
|
+
[WORKFLOW_EVENTS.OWNER_REQUEST_FIXES]: {
|
|
65
|
+
from: [WORKFLOW_PHASES.FINDING_DECISIONS],
|
|
63
66
|
to: WORKFLOW_PHASES.READY_FOR_BUILDER,
|
|
64
67
|
},
|
|
65
68
|
};
|
|
@@ -74,22 +77,21 @@ export const transitionWorkflow = ({
|
|
|
74
77
|
event,
|
|
75
78
|
}: TransitionWorkflowInput): WorkflowState => {
|
|
76
79
|
assertWorkflowState(state);
|
|
77
|
-
const current = state;
|
|
78
80
|
const transition = WORKFLOW_TRANSITIONS[event];
|
|
79
81
|
|
|
80
82
|
if (transition === undefined) {
|
|
81
83
|
throw new Error(`Unknown workflow event: "${event}".`);
|
|
82
84
|
}
|
|
83
85
|
|
|
84
|
-
if (!transition.from.includes(
|
|
86
|
+
if (!transition.from.includes(state.phase)) {
|
|
85
87
|
throw new Error(
|
|
86
|
-
`Workflow event "${event}" is not allowed from phase "${
|
|
88
|
+
`Workflow event "${event}" is not allowed from phase "${state.phase}".`,
|
|
87
89
|
);
|
|
88
90
|
}
|
|
89
91
|
|
|
90
92
|
return {
|
|
91
|
-
version:
|
|
92
|
-
specId:
|
|
93
|
+
version: state.version,
|
|
94
|
+
specId: state.specId,
|
|
93
95
|
phase: transition.to,
|
|
94
96
|
};
|
|
95
97
|
};
|
|
@@ -9,13 +9,13 @@ import { WORKFLOW_PHASES, type WorkflowState } from '#workflow/state/schema.ts';
|
|
|
9
9
|
|
|
10
10
|
const BUILDER_PHASES: readonly WorkflowState['phase'][] = [
|
|
11
11
|
WORKFLOW_PHASES.BUILDER_RUNNING,
|
|
12
|
-
WORKFLOW_PHASES.
|
|
12
|
+
WORKFLOW_PHASES.ESCALATION_DECISIONS,
|
|
13
13
|
WORKFLOW_PHASES.READY_FOR_VERIFIER,
|
|
14
14
|
];
|
|
15
15
|
|
|
16
16
|
const VERIFIER_PHASES: readonly WorkflowState['phase'][] = [
|
|
17
17
|
WORKFLOW_PHASES.VERIFIER_RUNNING,
|
|
18
|
-
WORKFLOW_PHASES.
|
|
18
|
+
WORKFLOW_PHASES.FINDING_DECISIONS,
|
|
19
19
|
];
|
|
20
20
|
|
|
21
21
|
export const getAgentRole = (
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Objective: Record owner decisions for current verifier findings on the active handoff.
|
|
3
|
-
* Used: When the owner resolves a
|
|
3
|
+
* Used: When the owner resolves a finding-decisions handoff.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { assertVerifierHandoff } from '#artifacts/verifier-handoff/assertVerifierHandoff.ts';
|
|
@@ -18,46 +18,42 @@ import { writeWorkflowState } from '#workflow/state/writeWorkflowState.ts';
|
|
|
18
18
|
import { transitionWorkflow } from '#workflow/transitions.ts';
|
|
19
19
|
import { getWorkflowEventByVerifierHandoff } from '#workflow/verifier/utils/getWorkflowEventByVerifierHandoff.ts';
|
|
20
20
|
|
|
21
|
-
type
|
|
22
|
-
findingId: string;
|
|
23
|
-
decision: typeof FINDING_DECISIONS.REJECT;
|
|
24
|
-
reason: string;
|
|
25
|
-
};
|
|
21
|
+
type SavedFindingDecision = NonNullable<VerifierFinding['decision']>;
|
|
26
22
|
|
|
27
|
-
type
|
|
23
|
+
export type FindingDecision = SavedFindingDecision & {
|
|
28
24
|
findingId: string;
|
|
29
|
-
decision: typeof FINDING_DECISIONS.FIX_CODE;
|
|
30
25
|
};
|
|
31
26
|
|
|
32
|
-
export type FindingDecision = FindingDecisionApproved | FindingDecisionRejected;
|
|
33
|
-
|
|
34
27
|
export type ResolvedFindings = {
|
|
35
28
|
findings: VerifierFinding[];
|
|
36
29
|
state: WorkflowState;
|
|
37
30
|
projectRoot: string;
|
|
38
31
|
};
|
|
39
32
|
|
|
40
|
-
type
|
|
33
|
+
type ApplyFindingDecisionsInput = {
|
|
41
34
|
decisions: FindingDecision[];
|
|
42
35
|
findings: VerifierFinding[];
|
|
43
36
|
};
|
|
44
37
|
|
|
45
|
-
|
|
38
|
+
const applyFindingDecisions = ({
|
|
39
|
+
decisions,
|
|
40
|
+
findings,
|
|
41
|
+
}: ApplyFindingDecisionsInput): VerifierFinding[] => {
|
|
46
42
|
const expectedIds = new Set(findings.map(({ id }) => id));
|
|
47
|
-
const
|
|
43
|
+
const decisionsById = new Map<string, FindingDecision>();
|
|
48
44
|
|
|
49
45
|
for (const decision of decisions) {
|
|
50
46
|
if (!expectedIds.has(decision.findingId)) {
|
|
51
47
|
throw new Error(`Unknown finding ID: "${decision.findingId}".`);
|
|
52
48
|
}
|
|
53
49
|
|
|
54
|
-
if (
|
|
50
|
+
if (decisionsById.has(decision.findingId)) {
|
|
55
51
|
throw new Error(
|
|
56
52
|
`Duplicate decision for finding "${decision.findingId}".`,
|
|
57
53
|
);
|
|
58
54
|
}
|
|
59
55
|
|
|
60
|
-
|
|
56
|
+
decisionsById.set(decision.findingId, decision);
|
|
61
57
|
|
|
62
58
|
if (
|
|
63
59
|
decision.decision === FINDING_DECISIONS.REJECT &&
|
|
@@ -69,12 +65,17 @@ function assertDecisions({ decisions, findings }: AssertDecisionsInput): void {
|
|
|
69
65
|
}
|
|
70
66
|
}
|
|
71
67
|
|
|
72
|
-
const missing = findings.find(({ id }) => !
|
|
68
|
+
const missing = findings.find(({ id }) => !decisionsById.has(id));
|
|
73
69
|
|
|
74
70
|
if (missing !== undefined) {
|
|
75
71
|
throw new Error(`Missing decision for finding "${missing.id}".`);
|
|
76
72
|
}
|
|
77
|
-
|
|
73
|
+
|
|
74
|
+
return findings.map((finding) => ({
|
|
75
|
+
...finding,
|
|
76
|
+
decision: getSavedFindingDecision(decisionsById.get(finding.id)),
|
|
77
|
+
}));
|
|
78
|
+
};
|
|
78
79
|
|
|
79
80
|
type FindingWorkflow = {
|
|
80
81
|
state: WorkflowState;
|
|
@@ -98,9 +99,9 @@ const readFindingWorkflow = async ({
|
|
|
98
99
|
);
|
|
99
100
|
}
|
|
100
101
|
|
|
101
|
-
if (state.phase !== WORKFLOW_PHASES.
|
|
102
|
+
if (state.phase !== WORKFLOW_PHASES.FINDING_DECISIONS) {
|
|
102
103
|
throw new Error(
|
|
103
|
-
`Finding resolution requires
|
|
104
|
+
`Finding resolution requires finding-decisions state, found "${state.phase}".`,
|
|
104
105
|
);
|
|
105
106
|
}
|
|
106
107
|
|
|
@@ -124,51 +125,25 @@ const readFindingWorkflow = async ({
|
|
|
124
125
|
return { state, handoff };
|
|
125
126
|
};
|
|
126
127
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
assertDecisions({ decisions, findings: handoff.findings });
|
|
138
|
-
|
|
139
|
-
const decisionsById = new Map(
|
|
140
|
-
decisions.map((decision) => [decision.findingId, decision]),
|
|
141
|
-
);
|
|
142
|
-
|
|
143
|
-
const findings = handoff.findings.map((finding) => {
|
|
144
|
-
const decision = decisionsById.get(finding.id);
|
|
145
|
-
|
|
146
|
-
if (decision?.decision === FINDING_DECISIONS.REJECT) {
|
|
147
|
-
return {
|
|
148
|
-
...finding,
|
|
149
|
-
decision: {
|
|
150
|
-
decision: FINDING_DECISIONS.REJECT,
|
|
151
|
-
reason: decision.reason.trim(),
|
|
152
|
-
},
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
return { ...finding, decision: { decision: FINDING_DECISIONS.FIX_CODE } };
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
const nextHandoff = { ...handoff, findings };
|
|
160
|
-
|
|
161
|
-
const nextState = transitionWorkflow({
|
|
162
|
-
state,
|
|
163
|
-
event: getWorkflowEventByVerifierHandoff(nextHandoff),
|
|
164
|
-
});
|
|
128
|
+
const getSavedFindingDecision = (
|
|
129
|
+
decision: FindingDecision | undefined,
|
|
130
|
+
): SavedFindingDecision => {
|
|
131
|
+
if (decision?.decision === FINDING_DECISIONS.REVISE_SPEC) {
|
|
132
|
+
return {
|
|
133
|
+
decision: FINDING_DECISIONS.REVISE_SPEC,
|
|
134
|
+
reason: decision.reason.trim(),
|
|
135
|
+
specChange: decision.specChange,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
165
138
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
139
|
+
if (decision?.decision === FINDING_DECISIONS.REJECT) {
|
|
140
|
+
return {
|
|
141
|
+
decision: FINDING_DECISIONS.REJECT,
|
|
142
|
+
reason: decision.reason.trim(),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
170
145
|
|
|
171
|
-
return {
|
|
146
|
+
return { decision: FINDING_DECISIONS.FIX_CODE };
|
|
172
147
|
};
|
|
173
148
|
|
|
174
149
|
type ResolveFindingsInput = {
|
|
@@ -182,23 +157,35 @@ export const resolveFindings = async ({
|
|
|
182
157
|
specId,
|
|
183
158
|
decisions,
|
|
184
159
|
}: ResolveFindingsInput): Promise<ResolvedFindings> => {
|
|
185
|
-
const
|
|
160
|
+
const { state, handoff } = await readFindingWorkflow({ paths, specId });
|
|
186
161
|
|
|
187
|
-
const
|
|
188
|
-
workflow: currentWorkflow,
|
|
162
|
+
const findings = applyFindingDecisions({
|
|
189
163
|
decisions,
|
|
164
|
+
findings: handoff.findings,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const nextHandoff = { ...handoff, findings };
|
|
168
|
+
|
|
169
|
+
const nextState = transitionWorkflow({
|
|
170
|
+
state,
|
|
171
|
+
event: getWorkflowEventByVerifierHandoff(nextHandoff),
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
assertVerifierHandoff({
|
|
175
|
+
handoff: nextHandoff,
|
|
176
|
+
specId: state.specId,
|
|
190
177
|
});
|
|
191
178
|
|
|
192
179
|
const handoffPath = await paths.getActiveVerifierHandoffPath(specId);
|
|
193
|
-
await writeJson({ path: handoffPath, data:
|
|
180
|
+
await writeJson({ path: handoffPath, data: nextHandoff });
|
|
194
181
|
await writeWorkflowState({
|
|
195
182
|
path: paths.getWorkflowPath(specId),
|
|
196
|
-
state:
|
|
183
|
+
state: nextState,
|
|
197
184
|
});
|
|
198
185
|
|
|
199
186
|
return {
|
|
200
|
-
findings
|
|
201
|
-
state:
|
|
187
|
+
findings,
|
|
188
|
+
state: nextState,
|
|
202
189
|
projectRoot: paths.getProjectRoot(),
|
|
203
190
|
};
|
|
204
191
|
};
|
|
@@ -13,19 +13,27 @@ export const getWorkflowEventByVerifierHandoff = (
|
|
|
13
13
|
handoff: VerifierHandoff,
|
|
14
14
|
): WorkflowEvent => {
|
|
15
15
|
if (handoff.findings.length === 0) {
|
|
16
|
-
return WORKFLOW_EVENTS.
|
|
16
|
+
return WORKFLOW_EVENTS.VERIFIER_DONE;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
if (handoff.findings.every(({ decision }) => decision === null)) {
|
|
20
20
|
return WORKFLOW_EVENTS.VERIFIER_FOUND_FINDINGS;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
if (
|
|
24
|
+
handoff.findings.some(
|
|
25
|
+
({ decision }) => decision?.decision === FINDING_DECISIONS.REVISE_SPEC,
|
|
26
|
+
)
|
|
27
|
+
) {
|
|
28
|
+
return WORKFLOW_EVENTS.OWNER_REQUEST_SPEC_REVISION;
|
|
29
|
+
}
|
|
30
|
+
|
|
23
31
|
if (
|
|
24
32
|
handoff.findings.some(
|
|
25
33
|
({ decision }) => decision?.decision === FINDING_DECISIONS.FIX_CODE,
|
|
26
34
|
)
|
|
27
35
|
) {
|
|
28
|
-
return WORKFLOW_EVENTS.
|
|
36
|
+
return WORKFLOW_EVENTS.OWNER_REQUEST_FIXES;
|
|
29
37
|
}
|
|
30
38
|
|
|
31
39
|
if (
|
|
@@ -33,7 +41,7 @@ export const getWorkflowEventByVerifierHandoff = (
|
|
|
33
41
|
({ decision }) => decision?.decision === FINDING_DECISIONS.REJECT,
|
|
34
42
|
)
|
|
35
43
|
) {
|
|
36
|
-
return WORKFLOW_EVENTS.
|
|
44
|
+
return WORKFLOW_EVENTS.OWNER_REJECT_FINDINGS;
|
|
37
45
|
}
|
|
38
46
|
|
|
39
47
|
throw new Error('Unexpected verifier finding decisions.');
|