@multileaf/ai-workflow 0.1.4 → 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/CHANGELOG.md +25 -1
- package/README.md +85 -49
- package/dist/adapter.js +17 -9
- package/dist/generator.js +13 -3
- package/dist/human-approval.d.ts +51 -0
- package/dist/human-approval.js +286 -0
- package/dist/quality-gates.d.ts +1 -1
- package/dist/quality-gates.js +71 -10
- package/dist/recommendations.d.ts +1 -0
- package/dist/recommendations.js +46 -11
- package/dist/selection.js +32 -2
- package/dist/types.d.ts +2 -0
- package/dist/types.js +10 -0
- package/dist/workflow.js +359 -31
- package/docs/ai-init-workflow-spec.md +9 -4
- package/docs/architecture.md +8 -0
- package/docs/backlog.md +1 -0
- package/package.json +1 -1
- package/resources/package.yaml +2 -0
- package/resources/skills/brainstorming.md +1 -0
- package/resources/skills/code-review.md +1 -0
- package/resources/skills/implementation-planning.md +1 -0
- package/resources/skills/project-init.md +1 -2
- package/resources/skills/requirements-specification.md +1 -0
- package/resources/skills/tdd-development.md +1 -0
- package/resources/skills/technical-design.md +1 -0
- package/resources/skills/verification.md +1 -0
- package/resources/templates/code-review.md +21 -0
- package/resources/templates/implementation-evidence.md +17 -0
package/dist/quality-gates.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export type QualityGateStage = "specification" | "plan" | "verification";
|
|
1
|
+
export type QualityGateStage = "brainstorming" | "specification" | "technical-design" | "plan" | "implementation" | "verification" | "review" | "traceability";
|
|
2
2
|
export declare function evaluateQualityGate(stage: QualityGateStage, content: string): string[];
|
package/dist/quality-gates.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { parseDocument } from "yaml";
|
|
1
2
|
function section(content, heading) {
|
|
2
3
|
const match = content.match(new RegExp(`^## ${heading}\\s*$([\\s\\S]*?)(?=^## |(?![\\s\\S]))`, "im"));
|
|
3
4
|
return match?.[1].trim();
|
|
@@ -11,7 +12,7 @@ function requireSections(content, artifact, headings) {
|
|
|
11
12
|
});
|
|
12
13
|
}
|
|
13
14
|
export function evaluateQualityGate(stage, content) {
|
|
14
|
-
if (stage === "
|
|
15
|
+
if (stage === "brainstorming")
|
|
15
16
|
return requireSections(content, "Brainstorm", [
|
|
16
17
|
"Goal",
|
|
17
18
|
"Users",
|
|
@@ -19,7 +20,7 @@ export function evaluateQualityGate(stage, content) {
|
|
|
19
20
|
"Risks",
|
|
20
21
|
"Non-goals",
|
|
21
22
|
]);
|
|
22
|
-
if (stage === "
|
|
23
|
+
if (stage === "specification") {
|
|
23
24
|
const issues = [];
|
|
24
25
|
if (!/REQ-\d+/.test(content))
|
|
25
26
|
issues.push("Specification must define a stable REQ-### ID.");
|
|
@@ -29,12 +30,72 @@ export function evaluateQualityGate(stage, content) {
|
|
|
29
30
|
issues.push("Requirement acceptance criteria must use Given, When, and Then.");
|
|
30
31
|
return issues;
|
|
31
32
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
33
|
+
if (stage === "technical-design")
|
|
34
|
+
return requireSections(content, "Technical Design", [
|
|
35
|
+
"Context",
|
|
36
|
+
"Proposed Design",
|
|
37
|
+
"Alternatives",
|
|
38
|
+
"Interfaces",
|
|
39
|
+
"Risks",
|
|
40
|
+
"Validation",
|
|
41
|
+
]);
|
|
42
|
+
if (stage === "plan") {
|
|
43
|
+
const fields = ["Requirement", "Code", "Tests", "Validation", "Evidence"];
|
|
44
|
+
const issues = !/TASK-\d+/.test(content)
|
|
45
|
+
? ["Implementation plan must define a TASK-### ID."]
|
|
46
|
+
: [];
|
|
47
|
+
for (const field of fields)
|
|
48
|
+
if (!new RegExp(`^[ \\t]*-?[ \\t]*${field}:[ \\t]*\\S+`, "im").test(content))
|
|
49
|
+
issues.push(`Implementation plan task must define ${field}.`);
|
|
50
|
+
return issues;
|
|
51
|
+
}
|
|
52
|
+
if (stage === "implementation")
|
|
53
|
+
return requireSections(content, "Implementation Evidence", [
|
|
54
|
+
"Changed Files",
|
|
55
|
+
"Tests",
|
|
56
|
+
"Validation",
|
|
57
|
+
"Deviations",
|
|
58
|
+
]);
|
|
59
|
+
if (stage === "verification")
|
|
60
|
+
return requireSections(content, "Verification Report", [
|
|
61
|
+
"Requirements Checked",
|
|
62
|
+
"Checks Passed",
|
|
63
|
+
"Missing Evidence",
|
|
64
|
+
"Residual Risks",
|
|
65
|
+
"Decision",
|
|
66
|
+
]);
|
|
67
|
+
if (stage === "review")
|
|
68
|
+
return requireSections(content, "Code Review", [
|
|
69
|
+
"Scope",
|
|
70
|
+
"Findings",
|
|
71
|
+
"Checks",
|
|
72
|
+
"Residual Risks",
|
|
73
|
+
"Decision",
|
|
74
|
+
]);
|
|
75
|
+
const document = parseDocument(content, { uniqueKeys: true, strict: true });
|
|
76
|
+
if (document.errors.length)
|
|
77
|
+
return ["Traceability evidence must be valid YAML."];
|
|
78
|
+
const value = document.toJS();
|
|
79
|
+
if (!isRecord(value) || value.schema !== 1 || !Array.isArray(value.links))
|
|
80
|
+
return ["Traceability evidence must define schema: 1 and a links list."];
|
|
81
|
+
if (value.links.length === 0)
|
|
82
|
+
return ["Traceability evidence must link at least one requirement."];
|
|
83
|
+
const fields = ["requirement", "tasks", "code", "tests", "evidence"];
|
|
84
|
+
return value.links.flatMap((link, index) => {
|
|
85
|
+
if (!isRecord(link))
|
|
86
|
+
return [`Traceability link ${index + 1} must be a mapping.`];
|
|
87
|
+
return fields.flatMap((field) => {
|
|
88
|
+
const fieldValue = link[field];
|
|
89
|
+
return field === "requirement"
|
|
90
|
+
? typeof fieldValue === "string" && /^REQ-\d+$/.test(fieldValue)
|
|
91
|
+
? []
|
|
92
|
+
: [`Traceability link ${index + 1} requires a valid requirement ID.`]
|
|
93
|
+
: Array.isArray(fieldValue) && fieldValue.length > 0
|
|
94
|
+
? []
|
|
95
|
+
: [`Traceability link ${index + 1} requires non-empty ${field} evidence.`];
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
function isRecord(value) {
|
|
100
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
40
101
|
}
|
|
@@ -13,6 +13,7 @@ export type CapabilityRecommendation = {
|
|
|
13
13
|
permissions: string[];
|
|
14
14
|
conflicts: string[];
|
|
15
15
|
resources: RecommendedResource[];
|
|
16
|
+
requires?: string[];
|
|
16
17
|
};
|
|
17
18
|
export declare function recommendCapabilities(profile: ProjectProfile): CapabilityRecommendation[];
|
|
18
19
|
export declare function resourcesForCapabilities(ids: string[]): RecommendedResource[];
|
package/dist/recommendations.js
CHANGED
|
@@ -38,10 +38,26 @@ const CATALOG = [
|
|
|
38
38
|
{ type: "agents", id: "technical-architect" },
|
|
39
39
|
{ type: "templates", id: "implementation-plan" },
|
|
40
40
|
]),
|
|
41
|
+
requires: ["requirements-specification"],
|
|
41
42
|
matches: (profile) => hasProjectSignals(profile)
|
|
42
43
|
? { evidence: projectEvidence(profile), confidence: 0.88 }
|
|
43
44
|
: undefined,
|
|
44
45
|
},
|
|
46
|
+
{
|
|
47
|
+
id: "technical-design",
|
|
48
|
+
provider: "multileaf",
|
|
49
|
+
rationale: "Architecture changes benefit from explicit alternatives, trade-offs, and decision records.",
|
|
50
|
+
permissions: [],
|
|
51
|
+
resources: local([
|
|
52
|
+
{ type: "skills", id: "technical-design" },
|
|
53
|
+
{ type: "agents", id: "technical-architect" },
|
|
54
|
+
{ type: "templates", id: "technical-design" },
|
|
55
|
+
{ type: "templates", id: "decision-record" },
|
|
56
|
+
]),
|
|
57
|
+
matches: (profile) => hasProjectSignals(profile)
|
|
58
|
+
? { evidence: projectEvidence(profile), confidence: 0.82 }
|
|
59
|
+
: undefined,
|
|
60
|
+
},
|
|
45
61
|
{
|
|
46
62
|
id: "code-review",
|
|
47
63
|
provider: "multileaf",
|
|
@@ -51,6 +67,7 @@ const CATALOG = [
|
|
|
51
67
|
{ type: "skills", id: "code-review" },
|
|
52
68
|
{ type: "agents", id: "quality-reviewer" },
|
|
53
69
|
]),
|
|
70
|
+
requires: ["verification"],
|
|
54
71
|
matches: (profile) => profile.runtime.languages.length
|
|
55
72
|
? { evidence: projectEvidence(profile), confidence: 0.86 }
|
|
56
73
|
: undefined,
|
|
@@ -68,9 +85,25 @@ const CATALOG = [
|
|
|
68
85
|
{ type: "hooks", id: "post-implementation" },
|
|
69
86
|
{ type: "templates", id: "test-plan" },
|
|
70
87
|
]),
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
88
|
+
requires: ["verification"],
|
|
89
|
+
matches: (profile) => {
|
|
90
|
+
const runners = [
|
|
91
|
+
...(profile.testing ? [profile.testing] : []),
|
|
92
|
+
...(profile.modules ?? []).flatMap((module) => module.testing ? [{ ...module.testing, module: module.path }] : []),
|
|
93
|
+
];
|
|
94
|
+
return runners.length
|
|
95
|
+
? {
|
|
96
|
+
evidence: [
|
|
97
|
+
...new Set(runners.flatMap((runner) => [
|
|
98
|
+
...("module" in runner ? [`workspace:${runner.module}`] : []),
|
|
99
|
+
`testing:${runner.name}`,
|
|
100
|
+
...(runner.command ? [`test-command:${runner.command}`] : []),
|
|
101
|
+
])),
|
|
102
|
+
],
|
|
103
|
+
confidence: 1,
|
|
104
|
+
}
|
|
105
|
+
: undefined;
|
|
106
|
+
},
|
|
74
107
|
},
|
|
75
108
|
{
|
|
76
109
|
id: "typescript-quality",
|
|
@@ -116,18 +149,19 @@ const CATALOG = [
|
|
|
116
149
|
: undefined,
|
|
117
150
|
},
|
|
118
151
|
{
|
|
119
|
-
id: "
|
|
152
|
+
id: "verification",
|
|
120
153
|
provider: "multileaf",
|
|
121
|
-
rationale: "
|
|
154
|
+
rationale: "Verification guidance can use the project's detected test runner and quality commands.",
|
|
122
155
|
permissions: [],
|
|
123
156
|
resources: local([
|
|
124
157
|
{ type: "skills", id: "verification" },
|
|
125
|
-
{ type: "agents", id: "
|
|
158
|
+
{ type: "agents", id: "quality-reviewer" },
|
|
126
159
|
{ type: "templates", id: "test-plan" },
|
|
127
160
|
{ type: "templates", id: "verification-report" },
|
|
128
161
|
]),
|
|
129
|
-
|
|
130
|
-
|
|
162
|
+
requires: ["implementation-planning"],
|
|
163
|
+
matches: (profile) => hasProjectSignals(profile)
|
|
164
|
+
? { evidence: projectEvidence(profile), confidence: profile.testing ? 1 : 0.84 }
|
|
131
165
|
: undefined,
|
|
132
166
|
},
|
|
133
167
|
{
|
|
@@ -208,8 +242,9 @@ export function recommendCapabilities(profile) {
|
|
|
208
242
|
});
|
|
209
243
|
}
|
|
210
244
|
export function resourcesForCapabilities(ids) {
|
|
211
|
-
const
|
|
212
|
-
const
|
|
245
|
+
const normalizedIds = ids.map((id) => (id === "vitest-testing" ? "verification" : id));
|
|
246
|
+
const selectedCapabilities = new Set(CATALOG.filter(({ id }) => normalizedIds.includes(id)).map(({ id }) => id));
|
|
247
|
+
const selectedResources = new Set(normalizedIds.filter((id) => id.includes("/")));
|
|
213
248
|
const resources = CATALOG.flatMap(({ id, resources }) => selectedCapabilities.has(id)
|
|
214
249
|
? resources
|
|
215
250
|
: resources.filter(({ type, id: resourceId }) => selectedResources.has(`${type}/${resourceId}`)));
|
|
@@ -219,7 +254,7 @@ export function resourcesForCapabilities(ids) {
|
|
|
219
254
|
}
|
|
220
255
|
export function serializeRecommendations(items, selected = []) {
|
|
221
256
|
return `schema: 1\nrecommendations:\n${items
|
|
222
|
-
.map((item) => ` - id: ${item.id}\n provider: ${item.provider}\n confidence: ${item.confidence}\n selected: ${selected.includes(item.id)}\n resources: [${item.resources.map(({ type, id }) => `${type}/${id}`).join(", ")}]\n selected_resources: [${item.resources
|
|
257
|
+
.map((item) => ` - id: ${item.id}\n provider: ${item.provider}\n confidence: ${item.confidence}\n selected: ${selected.includes(item.id)}\n requires: [${(item.requires ?? []).join(", ")}]\n resources: [${item.resources.map(({ type, id }) => `${type}/${id}`).join(", ")}]\n selected_resources: [${item.resources
|
|
223
258
|
.filter(({ type, id }) => selected.includes(`${type}/${id}`))
|
|
224
259
|
.map(({ type, id }) => `${type}/${id}`)
|
|
225
260
|
.join(", ")}]\n evidence: [${item.evidence.map((evidence) => `"${evidence}"`).join(", ")}]\n rationale: ${item.rationale}`)
|
package/dist/selection.js
CHANGED
|
@@ -3,15 +3,45 @@ export async function selectRecommendations(items, answer, selectedIds) {
|
|
|
3
3
|
item.id,
|
|
4
4
|
...(item.resources ?? []).map(({ type, id }) => `${type}/${id}`),
|
|
5
5
|
]));
|
|
6
|
+
available.add("vitest-testing");
|
|
6
7
|
if (selectedIds?.includes("all"))
|
|
7
8
|
return items.map(({ id }) => id);
|
|
8
9
|
if (selectedIds)
|
|
9
|
-
return
|
|
10
|
+
return includeRequirements(items, selectedIds
|
|
11
|
+
.filter((id) => available.has(id))
|
|
12
|
+
.map((id) => (id === "vitest-testing" ? "verification" : id)));
|
|
10
13
|
const selected = [];
|
|
11
14
|
for (const item of items) {
|
|
12
15
|
const response = (await answer(`Install ${item.id}? [Y/n] `)).trim().toLowerCase();
|
|
13
16
|
if (response === "" || response === "y" || response === "yes")
|
|
14
17
|
selected.push(item.id);
|
|
15
18
|
}
|
|
16
|
-
return selected;
|
|
19
|
+
return includeRequirements(items, selected);
|
|
20
|
+
}
|
|
21
|
+
function includeRequirements(items, selectedIds) {
|
|
22
|
+
const selected = new Set(selectedIds);
|
|
23
|
+
const byId = new Map(items.map((item) => [item.id, item]));
|
|
24
|
+
const owners = new Map();
|
|
25
|
+
for (const item of items)
|
|
26
|
+
for (const resource of item.resources ?? [])
|
|
27
|
+
owners.set(`${resource.type}/${resource.id}`, item.id);
|
|
28
|
+
const visit = (capabilityId, visited) => {
|
|
29
|
+
if (visited.has(capabilityId))
|
|
30
|
+
return;
|
|
31
|
+
visited.add(capabilityId);
|
|
32
|
+
const capability = byId.get(capabilityId);
|
|
33
|
+
if (!capability)
|
|
34
|
+
return;
|
|
35
|
+
for (const requirement of capability.requires ?? []) {
|
|
36
|
+
if (!byId.has(requirement))
|
|
37
|
+
continue;
|
|
38
|
+
selected.add(requirement);
|
|
39
|
+
visit(requirement, visited);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
const initial = [...selected];
|
|
43
|
+
const visited = new Set();
|
|
44
|
+
for (const id of initial)
|
|
45
|
+
visit(byId.has(id) ? id : (owners.get(id) ?? ""), visited);
|
|
46
|
+
return [...selected];
|
|
17
47
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export declare const TARGETS: readonly ["codex", "claude", "cursor", "gemini", "copilot", "universal"];
|
|
2
2
|
export type Target = (typeof TARGETS)[number];
|
|
3
|
+
export declare const HUMAN_APPROVAL_STAGES: readonly ["brainstorming", "specification", "technical-design", "plan", "implementation", "verification", "review", "traceability"];
|
|
4
|
+
export type HumanApprovalStage = (typeof HUMAN_APPROVAL_STAGES)[number];
|
|
3
5
|
export type CommandResult = {
|
|
4
6
|
exitCode: number;
|
|
5
7
|
output?: string;
|
package/dist/types.js
CHANGED
|
@@ -1 +1,11 @@
|
|
|
1
1
|
export const TARGETS = ["codex", "claude", "cursor", "gemini", "copilot", "universal"];
|
|
2
|
+
export const HUMAN_APPROVAL_STAGES = [
|
|
3
|
+
"brainstorming",
|
|
4
|
+
"specification",
|
|
5
|
+
"technical-design",
|
|
6
|
+
"plan",
|
|
7
|
+
"implementation",
|
|
8
|
+
"verification",
|
|
9
|
+
"review",
|
|
10
|
+
"traceability",
|
|
11
|
+
];
|