@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/workflow.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
2
3
|
import { BASE_DIRECTORIES, WORKFLOW_DIRECTORY } from "./constants.js";
|
|
3
4
|
import { detectTarget, isTarget, renderAiInit, renderAiInitPath, renderResourcePath, } from "./adapter.js";
|
|
4
5
|
import { loadBundledResources } from "./bundled-resources.js";
|
|
@@ -31,6 +32,7 @@ import { composeContext, CONTEXT_LAYERS, readContextStore, writeContextLayer, }
|
|
|
31
32
|
import { loadContextDelta, retrieveTaskContext } from "./context-retrieval.js";
|
|
32
33
|
import { createSummaryCache, isSummaryFresh, parseSummaryCache, serializeSummaryCache, } from "./context-cache.js";
|
|
33
34
|
import { parseTokenUsage, recordTokenUsage, serializeTokenUsage } from "./token-usage.js";
|
|
35
|
+
import { APPROVAL_STAGES, approveStage, beginStageApproval, createApprovalLedger, getUnresolvedPrerequisites, invalidateStageAndDescendants, parseApprovalLedger, parseWorkflowSession, rejectStage, serializeApprovalLedger, serializeWorkflowSession, skipOptionalStage, stageDefinition, stageFingerprint, } from "./human-approval.js";
|
|
34
36
|
import { measureContextQuality, serializeContextQuality } from "./context-quality.js";
|
|
35
37
|
import { serializeAdapterCapabilities } from "./adapter-contract.js";
|
|
36
38
|
import { buildTraceabilityGraph, serializeTraceabilityGraph } from "./traceability.js";
|
|
@@ -46,6 +48,139 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
46
48
|
try {
|
|
47
49
|
fs = withSafeProjectWrites(fs, root);
|
|
48
50
|
const command = args[0] ?? "help";
|
|
51
|
+
if (command === "workflow") {
|
|
52
|
+
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
53
|
+
return { exitCode: 1, error: "Run `aiw install` first." };
|
|
54
|
+
const action = args[1];
|
|
55
|
+
const sessionPath = join(aiw, "workflow.yml");
|
|
56
|
+
const ledgerPath = join(aiw, "approvals.yml");
|
|
57
|
+
if (action === "start") {
|
|
58
|
+
const options = parseNamedOptions(args.slice(2), ["id"]);
|
|
59
|
+
if (!options)
|
|
60
|
+
return { exitCode: 1, error: "Usage: aiw workflow start [--id=workflow-id]" };
|
|
61
|
+
if (fs.exists(sessionPath)) {
|
|
62
|
+
const previous = parseWorkflowSession(fs.read(sessionPath));
|
|
63
|
+
if (previous.status === "active")
|
|
64
|
+
return {
|
|
65
|
+
exitCode: 1,
|
|
66
|
+
error: `Workflow ${previous.workflowId} is still active. Complete it before starting another.`,
|
|
67
|
+
};
|
|
68
|
+
if (!fs.exists(ledgerPath))
|
|
69
|
+
return { exitCode: 1, error: "Completed workflow is missing its approval ledger." };
|
|
70
|
+
const archive = join(aiw, `checkpoints/workflow-${previous.workflowId}-approvals.yml`);
|
|
71
|
+
if (fs.exists(archive))
|
|
72
|
+
return { exitCode: 1, error: `Workflow approval archive already exists: ${archive}` };
|
|
73
|
+
fs.mkdir(join(aiw, "checkpoints"));
|
|
74
|
+
fs.write(archive, fs.read(ledgerPath));
|
|
75
|
+
}
|
|
76
|
+
else if (fs.exists(ledgerPath)) {
|
|
77
|
+
return {
|
|
78
|
+
exitCode: 1,
|
|
79
|
+
error: "Approval ledger exists without a workflow session; run doctor.",
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
const workflowId = options.id ?? randomUUID();
|
|
83
|
+
const priorRun = join(aiw, `checkpoints/workflow-${workflowId}-approvals.yml`);
|
|
84
|
+
if (fs.exists(priorRun))
|
|
85
|
+
return {
|
|
86
|
+
exitCode: 1,
|
|
87
|
+
error: `Workflow ID has already been used: ${workflowId}. Choose a new ID.`,
|
|
88
|
+
};
|
|
89
|
+
const now = new Date().toISOString();
|
|
90
|
+
const session = {
|
|
91
|
+
schema: 1,
|
|
92
|
+
workflowId,
|
|
93
|
+
status: "active",
|
|
94
|
+
startedAt: now,
|
|
95
|
+
updatedAt: now,
|
|
96
|
+
};
|
|
97
|
+
const ledger = createApprovalLedger(workflowId);
|
|
98
|
+
fs.write(ledgerPath, serializeApprovalLedger(ledger));
|
|
99
|
+
fs.write(sessionPath, serializeWorkflowSession(session));
|
|
100
|
+
return { exitCode: 0, output: `Workflow started: ${workflowId}.` };
|
|
101
|
+
}
|
|
102
|
+
if (action === "status") {
|
|
103
|
+
if (!fs.exists(sessionPath))
|
|
104
|
+
return { exitCode: 0, output: "No SDD workflow is active." };
|
|
105
|
+
const { session, ledger } = readWorkflowState(aiw, fs, false);
|
|
106
|
+
const stages = APPROVAL_STAGES.map((stage) => `- ${stage}: ${ledger.stages[stage]?.status ?? "not-started"}`);
|
|
107
|
+
return {
|
|
108
|
+
exitCode: 0,
|
|
109
|
+
output: `Workflow ${session.workflowId} (${session.status})\n${stages.join("\n")}\n${workflowNextAction(ledger)}`,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
if (action === "complete") {
|
|
113
|
+
const { session, ledger } = readActiveWorkflow(aiw, fs);
|
|
114
|
+
const unresolved = APPROVAL_STAGES.filter((stage) => ledger.stages[stage]?.status !== "approved" &&
|
|
115
|
+
ledger.stages[stage]?.status !== "skipped");
|
|
116
|
+
if (unresolved.length)
|
|
117
|
+
return {
|
|
118
|
+
exitCode: 1,
|
|
119
|
+
error: `Workflow cannot complete before every stage is approved or explicitly skipped: ${unresolved.join(", ")}.`,
|
|
120
|
+
};
|
|
121
|
+
fs.write(sessionPath, serializeWorkflowSession({
|
|
122
|
+
...session,
|
|
123
|
+
status: "complete",
|
|
124
|
+
updatedAt: new Date().toISOString(),
|
|
125
|
+
}));
|
|
126
|
+
return { exitCode: 0, output: `Workflow completed: ${session.workflowId}.` };
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
exitCode: 1,
|
|
130
|
+
error: "Usage: aiw workflow <start|status|complete> [options]",
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
if (command === "approve" || command === "reject" || command === "skip") {
|
|
134
|
+
const stage = args[1];
|
|
135
|
+
if (!stage || !APPROVAL_STAGES.includes(stage))
|
|
136
|
+
return { exitCode: 1, error: `Usage: aiw ${command} <workflow-stage> [options]` };
|
|
137
|
+
const { ledger } = readActiveWorkflow(aiw, fs);
|
|
138
|
+
const workflowStage = stage;
|
|
139
|
+
if (command === "skip") {
|
|
140
|
+
const options = parseNamedOptions(args.slice(2), ["reason"]);
|
|
141
|
+
if (!options?.reason)
|
|
142
|
+
return {
|
|
143
|
+
exitCode: 1,
|
|
144
|
+
error: "Usage: aiw skip <optional-stage> --reason=human-approved-reason",
|
|
145
|
+
};
|
|
146
|
+
const updated = skipOptionalStage(ledger, workflowStage, {
|
|
147
|
+
reason: options.reason,
|
|
148
|
+
humanAccepted: true,
|
|
149
|
+
updatedAt: new Date().toISOString(),
|
|
150
|
+
});
|
|
151
|
+
fs.write(join(aiw, "approvals.yml"), serializeApprovalLedger(updated));
|
|
152
|
+
return { exitCode: 0, output: `Human-approved skip recorded for ${workflowStage}.` };
|
|
153
|
+
}
|
|
154
|
+
const artifact = approvalArtifactPath(aiw, workflowStage);
|
|
155
|
+
if (!artifact || !fs.exists(artifact))
|
|
156
|
+
return { exitCode: 1, error: `Approval evidence is missing for ${workflowStage}.` };
|
|
157
|
+
const artifactHash = stageFingerprint(fs.read(artifact));
|
|
158
|
+
if (command === "approve") {
|
|
159
|
+
if (ledger.stages[workflowStage]?.artifactHash !== artifactHash) {
|
|
160
|
+
const invalidated = invalidateStageAndDescendants(ledger, workflowStage, new Date().toISOString());
|
|
161
|
+
fs.write(join(aiw, "approvals.yml"), serializeApprovalLedger(invalidated));
|
|
162
|
+
return {
|
|
163
|
+
exitCode: 1,
|
|
164
|
+
error: `The ${workflowStage} artifact changed after review; its approval is stale. Rerun the quality gate and request approval again.`,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
const updated = approveStage(ledger, workflowStage, {
|
|
168
|
+
artifactHash,
|
|
169
|
+
updatedAt: new Date().toISOString(),
|
|
170
|
+
});
|
|
171
|
+
fs.write(join(aiw, "approvals.yml"), serializeApprovalLedger(updated));
|
|
172
|
+
return { exitCode: 0, output: `Human approval recorded for ${workflowStage}.` };
|
|
173
|
+
}
|
|
174
|
+
const options = parseNamedOptions(args.slice(2), ["reason"]);
|
|
175
|
+
if (!options?.reason)
|
|
176
|
+
return { exitCode: 1, error: "Usage: aiw reject <stage> --reason=human-feedback" };
|
|
177
|
+
const updated = rejectStage(ledger, workflowStage, options.reason, new Date().toISOString());
|
|
178
|
+
fs.write(join(aiw, "approvals.yml"), serializeApprovalLedger(updated));
|
|
179
|
+
return {
|
|
180
|
+
exitCode: 0,
|
|
181
|
+
output: `Human rejection recorded for ${workflowStage}. Revise this stage.`,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
49
184
|
if (command === "ui") {
|
|
50
185
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
51
186
|
return { exitCode: 1, error: "Run `aiw install` first." };
|
|
@@ -749,6 +884,52 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
749
884
|
: await profileProject(root);
|
|
750
885
|
const selectedArg = args.find((arg) => arg.startsWith("--select="))?.split("=", 2)[1];
|
|
751
886
|
const selected = selectedArg?.split(",") ?? (command === "sync" ? readSelectedRecommendations(fs, aiw) : []);
|
|
887
|
+
if (command === "sync") {
|
|
888
|
+
const previewOnly = args.includes("--preview");
|
|
889
|
+
const planApprovals = args.filter((arg) => arg.startsWith("--approve-plan="));
|
|
890
|
+
if (planApprovals.length > 1 || (previewOnly && planApprovals.length > 0))
|
|
891
|
+
return {
|
|
892
|
+
exitCode: 1,
|
|
893
|
+
error: "Usage: aiw sync [--preview | --approve-plan=sha256-fingerprint]",
|
|
894
|
+
};
|
|
895
|
+
const approval = planApprovals[0]?.slice("--approve-plan=".length);
|
|
896
|
+
const result = syncSelectedResources(fs, root, aiw, profile, selected, !previewOnly, approval);
|
|
897
|
+
if (result.error)
|
|
898
|
+
return { exitCode: 1, error: result.error };
|
|
899
|
+
if (previewOnly)
|
|
900
|
+
return { exitCode: 0, output: result.preview };
|
|
901
|
+
if (!approval)
|
|
902
|
+
return {
|
|
903
|
+
exitCode: 1,
|
|
904
|
+
error: `Sync is read-only until the exact preview is approved. Review the plan, then rerun with --approve-plan=${result.fingerprint}.`,
|
|
905
|
+
};
|
|
906
|
+
if (approval !== result.fingerprint)
|
|
907
|
+
return {
|
|
908
|
+
exitCode: 1,
|
|
909
|
+
error: `The sync plan changed after review. Run aiw sync --preview again and approve the new fingerprint (${result.fingerprint}).`,
|
|
910
|
+
};
|
|
911
|
+
if (result.conflicts?.length)
|
|
912
|
+
return {
|
|
913
|
+
exitCode: 1,
|
|
914
|
+
error: `Sync plan has conflicts and made no changes: ${result.conflicts.join(", ")}`,
|
|
915
|
+
};
|
|
916
|
+
const messages = [result.output ?? "Selected bundled resources synchronized."];
|
|
917
|
+
if (selected.includes("react-best-practices")) {
|
|
918
|
+
let pendingReason = "network:external was not approved";
|
|
919
|
+
try {
|
|
920
|
+
enforceConfiguredOrganizationPolicy(fs, aiw, {
|
|
921
|
+
provider: "vercel-skills",
|
|
922
|
+
source: "vercel-labs/agent-skills",
|
|
923
|
+
permissions: ["network:external"],
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
catch (error) {
|
|
927
|
+
pendingReason = error instanceof Error ? error.message : String(error);
|
|
928
|
+
}
|
|
929
|
+
messages.push(`Remote skill pending: ${pendingReason}. Install it separately with aiw skills install.`);
|
|
930
|
+
}
|
|
931
|
+
return { exitCode: 0, output: messages.join(" ") };
|
|
932
|
+
}
|
|
752
933
|
if (selected.includes("react-best-practices")) {
|
|
753
934
|
const approvedPermissions = parseApprovedPermissions(args);
|
|
754
935
|
if (!approvedPermissions.includes("network:external"))
|
|
@@ -765,12 +946,6 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
765
946
|
await installVercelSkill(services.externalSkills ?? nodeCommandExecutor, "vercel-labs/agent-skills", "vercel-react-best-practices", target);
|
|
766
947
|
writeVercelSkillLock(fs, root, aiw, "vercel-labs/agent-skills", "vercel-react-best-practices");
|
|
767
948
|
}
|
|
768
|
-
if (command === "sync") {
|
|
769
|
-
const result = syncSelectedResources(fs, root, aiw, profile, selected);
|
|
770
|
-
return result.error
|
|
771
|
-
? { exitCode: 1, error: result.error }
|
|
772
|
-
: { exitCode: 0, output: result.output };
|
|
773
|
-
}
|
|
774
949
|
const conflicts = [];
|
|
775
950
|
generateProjectResources(profile, selected, (path, content) => {
|
|
776
951
|
const destination = join(aiw, path);
|
|
@@ -794,7 +969,14 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
794
969
|
try {
|
|
795
970
|
const manifest = fs.read(join(aiw, "manifest.yml"));
|
|
796
971
|
parseManifest(manifest);
|
|
797
|
-
|
|
972
|
+
const workflowPath = join(aiw, "workflow.yml");
|
|
973
|
+
if (!fs.exists(workflowPath))
|
|
974
|
+
return { exitCode: 0, output: manifest };
|
|
975
|
+
const { session, ledger } = readWorkflowState(aiw, fs, false);
|
|
976
|
+
return {
|
|
977
|
+
exitCode: 0,
|
|
978
|
+
output: `${manifest}\nSDD workflow ${session.workflowId} (${session.status})\n${workflowNextAction(ledger)}`,
|
|
979
|
+
};
|
|
798
980
|
}
|
|
799
981
|
catch (error) {
|
|
800
982
|
return { exitCode: 1, error: error instanceof Error ? error.message : String(error) };
|
|
@@ -922,6 +1104,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
922
1104
|
if (command === "brainstorm") {
|
|
923
1105
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
924
1106
|
return { exitCode: 1, error: "Run `aiw install` first." };
|
|
1107
|
+
assertStageReady(aiw, fs, "brainstorming");
|
|
925
1108
|
const title = args.find((arg) => arg.startsWith("--title="))?.slice(8) || "Untitled Brainstorm";
|
|
926
1109
|
const path = join(aiw, "generated/specs/brainstorm.md");
|
|
927
1110
|
fs.write(path, `# ${title}\n\n## Goal\n\n## Users\n\n## Facts\n\n## Assumptions\n\n## Hypotheses\n\n## Constraints\n\n## Risks\n\n## Non-goals\n\n## Open Questions\n\n`);
|
|
@@ -930,6 +1113,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
930
1113
|
if (command === "spec") {
|
|
931
1114
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
932
1115
|
return { exitCode: 1, error: "Run `aiw install` first." };
|
|
1116
|
+
assertStageReady(aiw, fs, "specification");
|
|
933
1117
|
const title = args.find((arg) => arg.startsWith("--title="))?.slice(8) || "Untitled Specification";
|
|
934
1118
|
const path = join(aiw, "generated/specs/specification.md");
|
|
935
1119
|
fs.write(path, `# ${title}\n\n## Context\n\n## Requirements\n\n### REQ-001: Requirement title\n\n- **Description:**\n- **Acceptance criteria:**\n - Given\n - When\n - Then\n\n## Non-functional Requirements\n\n## Constraints\n\n## Open Questions\n\n`);
|
|
@@ -938,6 +1122,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
938
1122
|
if (command === "adr") {
|
|
939
1123
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
940
1124
|
return { exitCode: 1, error: "Run `aiw install` first." };
|
|
1125
|
+
assertStageReady(aiw, fs, "technical-design");
|
|
941
1126
|
const id = args.find((arg) => arg.startsWith("--id="))?.slice(5) || "001";
|
|
942
1127
|
const title = args.find((arg) => arg.startsWith("--title="))?.slice(8) || "Untitled Decision";
|
|
943
1128
|
const path = join(root, ".context/adrs", `ADR-${id}-${title.toLowerCase().replaceAll(" ", "-")}.md`);
|
|
@@ -947,6 +1132,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
947
1132
|
if (command === "plan") {
|
|
948
1133
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
949
1134
|
return { exitCode: 1, error: "Run `aiw install` first." };
|
|
1135
|
+
assertStageReady(aiw, fs, "plan");
|
|
950
1136
|
const title = args.find((arg) => arg.startsWith("--title="))?.slice(8) || "Untitled Implementation Plan";
|
|
951
1137
|
const path = join(aiw, "generated/plans/implementation-plan.md");
|
|
952
1138
|
fs.write(path, `# ${title}\n\n## Linked Requirements\n\n- REQ-001: Link to the specification requirement.\n\n## Tasks\n\n- [ ] TASK-001: Implement the first increment.\n - Requirement: REQ-001\n - Code: src/\n - Tests: src/**/*.test.ts\n - Validation: npm test\n - Evidence: Record the passing command output and changed files.\n\n## Validation Commands\n\n- npm run check\n\n## Risks and Dependencies\n\n- Dependencies: List prerequisite tasks, packages, or decisions.\n- Risks: Record delivery, compatibility, and validation risks with mitigations.\n\n## Completion Evidence\n\n- TASK-001: Link code changes, test results, and validation output.\n\n`);
|
|
@@ -955,6 +1141,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
955
1141
|
if (command === "verify") {
|
|
956
1142
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
957
1143
|
return { exitCode: 1, error: "Run `aiw install` first." };
|
|
1144
|
+
assertStageReady(aiw, fs, "verification");
|
|
958
1145
|
const specPath = join(aiw, "generated/specs/specification.md");
|
|
959
1146
|
const planPath = join(aiw, "generated/plans/implementation-plan.md");
|
|
960
1147
|
const issues = [];
|
|
@@ -987,6 +1174,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
987
1174
|
if (command === "trace") {
|
|
988
1175
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
989
1176
|
return { exitCode: 1, error: "Run `aiw install` first." };
|
|
1177
|
+
assertStageReady(aiw, fs, "traceability");
|
|
990
1178
|
const specPath = join(aiw, "generated/specs/specification.md");
|
|
991
1179
|
const planPath = join(aiw, "generated/plans/implementation-plan.md");
|
|
992
1180
|
if (!fs.exists(specPath) || !fs.exists(planPath))
|
|
@@ -1018,21 +1206,36 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
1018
1206
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
1019
1207
|
return { exitCode: 1, error: "Run `aiw install` first." };
|
|
1020
1208
|
const stage = args[1];
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1209
|
+
if (!stage || !APPROVAL_STAGES.includes(stage))
|
|
1210
|
+
return {
|
|
1211
|
+
exitCode: 1,
|
|
1212
|
+
error: `Usage: aiw gate <${APPROVAL_STAGES.join("|")}>`,
|
|
1213
|
+
};
|
|
1214
|
+
const workflowStage = stage;
|
|
1215
|
+
assertStageReady(aiw, fs, workflowStage);
|
|
1216
|
+
const artifactPath = approvalArtifactPath(aiw, workflowStage);
|
|
1217
|
+
if (!artifactPath || !fs.exists(artifactPath))
|
|
1218
|
+
return {
|
|
1219
|
+
exitCode: 1,
|
|
1220
|
+
error: `Quality gate blocked: required evidence is missing for ${stage}.`,
|
|
1221
|
+
};
|
|
1222
|
+
const hasQualityGate = stageDefinition(workflowStage).requiresQualityGate;
|
|
1223
|
+
if (hasQualityGate) {
|
|
1224
|
+
const issues = evaluateQualityGate(stage, fs.read(artifactPath));
|
|
1225
|
+
if (issues.length)
|
|
1226
|
+
return { exitCode: 1, error: `Quality gate blocked: ${issues.join(" ")}` };
|
|
1227
|
+
}
|
|
1228
|
+
const { ledger } = readActiveWorkflow(aiw, fs);
|
|
1229
|
+
const updated = beginStageApproval(ledger, workflowStage, {
|
|
1230
|
+
artifactHash: stageFingerprint(fs.read(artifactPath)),
|
|
1231
|
+
qualityGate: hasQualityGate ? "passed" : "not-applicable",
|
|
1232
|
+
updatedAt: new Date().toISOString(),
|
|
1233
|
+
});
|
|
1234
|
+
fs.write(join(aiw, "approvals.yml"), serializeApprovalLedger(updated));
|
|
1235
|
+
return {
|
|
1236
|
+
exitCode: 0,
|
|
1237
|
+
output: `${hasQualityGate ? `Quality gate passed for ${stage}.` : `Evidence recorded for ${stage}; no automated quality gate applies.`} Human approval is still required: run aiw approve ${stage}.`,
|
|
1025
1238
|
};
|
|
1026
|
-
if (!stage || !(stage in requirements))
|
|
1027
|
-
return { exitCode: 1, error: "Usage: aiw gate <specification|plan|verification>" };
|
|
1028
|
-
const gateStage = stage;
|
|
1029
|
-
const artifact = requirements[gateStage];
|
|
1030
|
-
if (!fs.exists(join(aiw, artifact)))
|
|
1031
|
-
return { exitCode: 1, error: `Quality gate blocked: missing ${artifact}` };
|
|
1032
|
-
const issues = evaluateQualityGate(gateStage, fs.read(join(aiw, artifact)));
|
|
1033
|
-
return issues.length
|
|
1034
|
-
? { exitCode: 1, error: `Quality gate blocked: ${issues.join(" ")}` }
|
|
1035
|
-
: { exitCode: 0, output: `Quality gate passed for ${stage}.` };
|
|
1036
1239
|
}
|
|
1037
1240
|
if (command === "validate") {
|
|
1038
1241
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
@@ -1383,13 +1586,81 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
1383
1586
|
}
|
|
1384
1587
|
return {
|
|
1385
1588
|
exitCode: 0,
|
|
1386
|
-
output: "aiw install [--target target] | ui [--port=number] | orchestrate --plan=path [--execute] [--max-parallel=number] | telemetry <status|enable|disable> [privacy options] | plugin <create|validate> [options] | scan | self-validate --ticket=TYPE-000 | organization-policy --file=path [--replace] | policy-check --package=path [--package=path] | preset --file=path [--replace] | context | context-summary | context-quality | capabilities [--target=target] | token-usage [--stage=name --budget=number --used=number] | registry [--search=query] [--private=name] | registry configure --file=path [--replace] | skills <search|inspect|install|check|update> [options] | audit-package --package=path | verify-package --package=path --checksum=sha256 | brainstorm [--title=title] | spec [--title=title] | adr [--id=id --title=title] | plan [--title=title] | verify | trace | gate <stage> | recommend [--select=id,id] | status | doctor | repair | uninstall [--dry-run] | target <target> | rollback | confirm | resolve --package=path | update --package=path --target=target | validate [--package=path]",
|
|
1589
|
+
output: "aiw install [--target target] | workflow <start|status|complete> [--id=id] | approve <stage> | reject <stage> --reason=text | skip <optional-stage> --reason=text | ui [--port=number] | orchestrate --plan=path [--execute] [--max-parallel=number] | telemetry <status|enable|disable> [privacy options] | plugin <create|validate> [options] | scan | self-validate --ticket=TYPE-000 | organization-policy --file=path [--replace] | policy-check --package=path [--package=path] | preset --file=path [--replace] | context | context-summary | context-quality | capabilities [--target=target] | token-usage [--stage=name --budget=number --used=number] | registry [--search=query] [--private=name] | registry configure --file=path [--replace] | skills <search|inspect|install|check|update> [options] | audit-package --package=path | verify-package --package=path --checksum=sha256 | brainstorm [--title=title] | spec [--title=title] | adr [--id=id --title=title] | plan [--title=title] | verify | trace | gate <stage> | recommend [--select=id,id] | status | doctor | repair | uninstall [--dry-run] | target <target> | rollback | confirm | resolve --package=path | update --package=path --target=target | validate [--package=path]",
|
|
1387
1590
|
};
|
|
1388
1591
|
}
|
|
1389
1592
|
catch (error) {
|
|
1390
1593
|
return { exitCode: 1, error: error instanceof Error ? error.message : String(error) };
|
|
1391
1594
|
}
|
|
1392
1595
|
}
|
|
1596
|
+
function readWorkflowState(aiw, fs, requireActive = true) {
|
|
1597
|
+
const sessionPath = join(aiw, "workflow.yml");
|
|
1598
|
+
const ledgerPath = join(aiw, "approvals.yml");
|
|
1599
|
+
if (!fs.exists(sessionPath) || !fs.exists(ledgerPath))
|
|
1600
|
+
throw new Error("No SDD workflow is active. Start one with `aiw workflow start`.");
|
|
1601
|
+
const session = parseWorkflowSession(fs.read(sessionPath));
|
|
1602
|
+
let ledger = parseApprovalLedger(fs.read(ledgerPath));
|
|
1603
|
+
if (session.workflowId !== ledger.workflowId)
|
|
1604
|
+
throw new Error("Workflow session and approval ledger IDs do not match; run doctor.");
|
|
1605
|
+
if (requireActive && session.status !== "active")
|
|
1606
|
+
throw new Error(`Workflow ${session.workflowId} is complete; start a new workflow first.`);
|
|
1607
|
+
const changed = APPROVAL_STAGES.find((stage) => {
|
|
1608
|
+
const record = ledger.stages[stage];
|
|
1609
|
+
if (!record?.artifactHash || !["pending-human", "approved"].includes(record.status))
|
|
1610
|
+
return false;
|
|
1611
|
+
const artifact = approvalArtifactPath(aiw, stage);
|
|
1612
|
+
return (!artifact ||
|
|
1613
|
+
!fs.exists(artifact) ||
|
|
1614
|
+
stageFingerprint(fs.read(artifact)) !== record.artifactHash);
|
|
1615
|
+
});
|
|
1616
|
+
if (changed) {
|
|
1617
|
+
ledger = invalidateStageAndDescendants(ledger, changed, new Date().toISOString());
|
|
1618
|
+
fs.write(ledgerPath, serializeApprovalLedger(ledger));
|
|
1619
|
+
}
|
|
1620
|
+
return { session, ledger };
|
|
1621
|
+
}
|
|
1622
|
+
function readActiveWorkflow(aiw, fs) {
|
|
1623
|
+
return readWorkflowState(aiw, fs, true);
|
|
1624
|
+
}
|
|
1625
|
+
function workflowNextAction(ledger) {
|
|
1626
|
+
const next = APPROVAL_STAGES.find((stage) => ledger.stages[stage]?.status !== "approved" && ledger.stages[stage]?.status !== "skipped");
|
|
1627
|
+
if (!next)
|
|
1628
|
+
return "Next action: all stages are resolved; run aiw workflow complete.";
|
|
1629
|
+
if (ledger.stages[next]?.status === "pending-human")
|
|
1630
|
+
return `Next action: present ${next} evidence and wait for the human; then run aiw approve ${next} (or reject it).`;
|
|
1631
|
+
if (ledger.stages[next]?.status === "stale" || ledger.stages[next]?.status === "rejected")
|
|
1632
|
+
return `Next action: revise ${next}, rerun its gate, and request human approval.`;
|
|
1633
|
+
return `Next action: prepare ${next} evidence.`;
|
|
1634
|
+
}
|
|
1635
|
+
function assertStageReady(aiw, fs, stage) {
|
|
1636
|
+
const { ledger } = readActiveWorkflow(aiw, fs);
|
|
1637
|
+
const unresolved = getUnresolvedPrerequisites(ledger, stage);
|
|
1638
|
+
if (unresolved.length) {
|
|
1639
|
+
const pending = unresolved.filter((prerequisite) => ledger.stages[prerequisite]?.status === "pending-human");
|
|
1640
|
+
if (pending.length)
|
|
1641
|
+
throw new Error(`Cannot enter ${stage}; previous stage(s) are awaiting human approval: ${pending.join(", ")}.`);
|
|
1642
|
+
throw new Error(`Cannot enter ${stage}; previous stages require human approval or an accepted optional skip: ${unresolved.join(", ")}.`);
|
|
1643
|
+
}
|
|
1644
|
+
const current = ledger.stages[stage];
|
|
1645
|
+
if (current?.status === "pending-human")
|
|
1646
|
+
throw new Error(`${stage} is awaiting human approval. Approve or reject it before continuing.`);
|
|
1647
|
+
if (current?.status === "approved")
|
|
1648
|
+
throw new Error(`${stage} is already approved. Evidence changes require rerunning its gate.`);
|
|
1649
|
+
}
|
|
1650
|
+
function approvalArtifactPath(aiw, stage) {
|
|
1651
|
+
if (stage === "technical-design")
|
|
1652
|
+
return join(aiw, "generated/reports/technical-design.md");
|
|
1653
|
+
const paths = {
|
|
1654
|
+
brainstorming: "generated/specs/brainstorm.md",
|
|
1655
|
+
specification: "generated/specs/specification.md",
|
|
1656
|
+
plan: "generated/plans/implementation-plan.md",
|
|
1657
|
+
implementation: "generated/reports/implementation-report.md",
|
|
1658
|
+
verification: "generated/reports/verification-report.md",
|
|
1659
|
+
review: "generated/reports/code-review.md",
|
|
1660
|
+
traceability: "generated/artifacts/traceability.yml",
|
|
1661
|
+
};
|
|
1662
|
+
return resolve(aiw, paths[stage]);
|
|
1663
|
+
}
|
|
1393
1664
|
function parseNamedOptions(args, allowed) {
|
|
1394
1665
|
const entries = args.map((arg) => arg.match(/^--([a-z-]+)=(.+)$/)?.slice(1));
|
|
1395
1666
|
if (args.some((arg) => arg.indexOf("=") !== arg.lastIndexOf("=")) ||
|
|
@@ -1533,7 +1804,7 @@ function containsCredential(value, credential, visited = new Set()) {
|
|
|
1533
1804
|
visited.add(value);
|
|
1534
1805
|
return Object.values(value).some((entry) => containsCredential(entry, credential, visited));
|
|
1535
1806
|
}
|
|
1536
|
-
function syncSelectedResources(fs, root, aiw, profile, selected) {
|
|
1807
|
+
function syncSelectedResources(fs, root, aiw, profile, selected, apply = false, approvedFingerprint) {
|
|
1537
1808
|
const target = parseManifest(fs.read(join(aiw, "manifest.yml"))).target.active;
|
|
1538
1809
|
const resourceSelections = resourcesForCapabilities(selected);
|
|
1539
1810
|
const bundled = loadBundledResources(target);
|
|
@@ -1584,7 +1855,16 @@ function syncSelectedResources(fs, root, aiw, profile, selected) {
|
|
|
1584
1855
|
const inventory = parseOwnership(fs.read(ownershipPath));
|
|
1585
1856
|
const nextInventory = new Map(inventory.map((entry) => [entry.path, entry]));
|
|
1586
1857
|
const changes = [];
|
|
1858
|
+
const changeActions = new Map();
|
|
1859
|
+
const preserved = new Map();
|
|
1587
1860
|
const conflicts = [];
|
|
1861
|
+
const preserve = (path, type, content) => {
|
|
1862
|
+
preserved.set(path, {
|
|
1863
|
+
path,
|
|
1864
|
+
state: type ?? "unknown",
|
|
1865
|
+
...(content === undefined ? {} : { contentFingerprint: stageFingerprint(content) }),
|
|
1866
|
+
});
|
|
1867
|
+
};
|
|
1588
1868
|
for (const [path, { content, metadata }] of desired) {
|
|
1589
1869
|
assertSafeProjectPath(fs, root, path);
|
|
1590
1870
|
const relativePath = relative(root, path).split(sep).join("/");
|
|
@@ -1592,41 +1872,89 @@ function syncSelectedResources(fs, root, aiw, profile, selected) {
|
|
|
1592
1872
|
const owned = nextInventory.get(relativePath);
|
|
1593
1873
|
if (type === "missing") {
|
|
1594
1874
|
changes.push({ path, content: Buffer.from(content, "utf8") });
|
|
1875
|
+
changeActions.set(relativePath, "add");
|
|
1595
1876
|
nextInventory.set(relativePath, createOwnedFile(relativePath, content, metadata));
|
|
1596
1877
|
continue;
|
|
1597
1878
|
}
|
|
1598
1879
|
if (type !== "file" || !owned) {
|
|
1599
1880
|
conflicts.push(relativePath);
|
|
1881
|
+
preserve(relativePath, type);
|
|
1600
1882
|
continue;
|
|
1601
1883
|
}
|
|
1602
1884
|
const actual = fs.read(path);
|
|
1603
1885
|
if (createOwnedFile(relativePath, actual).checksum !== owned.checksum) {
|
|
1604
1886
|
conflicts.push(relativePath);
|
|
1887
|
+
preserve(relativePath, type, actual);
|
|
1605
1888
|
continue;
|
|
1606
1889
|
}
|
|
1607
|
-
if (actual !== content)
|
|
1890
|
+
if (actual !== content) {
|
|
1608
1891
|
changes.push({ path, content: Buffer.from(content, "utf8") });
|
|
1892
|
+
changeActions.set(relativePath, "update");
|
|
1893
|
+
}
|
|
1894
|
+
else
|
|
1895
|
+
preserve(relativePath, type, actual);
|
|
1609
1896
|
nextInventory.set(relativePath, createOwnedFile(relativePath, content, metadata));
|
|
1610
1897
|
}
|
|
1611
1898
|
for (const [path, content] of generatedFiles) {
|
|
1612
1899
|
assertSafeProjectPath(fs, root, path);
|
|
1613
1900
|
const type = fs.pathType?.(path);
|
|
1614
|
-
|
|
1901
|
+
const relativePath = relative(root, path).split(sep).join("/");
|
|
1902
|
+
if (type === "missing") {
|
|
1615
1903
|
changes.push({ path, content: Buffer.from(content, "utf8") });
|
|
1616
|
-
|
|
1904
|
+
changeActions.set(relativePath, "add");
|
|
1905
|
+
}
|
|
1906
|
+
else if (type !== "file" || fs.read(path) !== content) {
|
|
1617
1907
|
conflicts.push(relative(root, path).split(sep).join("/"));
|
|
1908
|
+
preserve(relativePath, type, type === "file" ? fs.read(path) : undefined);
|
|
1909
|
+
}
|
|
1910
|
+
else
|
|
1911
|
+
preserve(relativePath, type, fs.read(path));
|
|
1618
1912
|
}
|
|
1619
|
-
if (conflicts.length)
|
|
1620
|
-
return {
|
|
1621
|
-
error: `Sync preserved existing or modified project resources: ${[...new Set(conflicts)].sort().join(", ")}`,
|
|
1622
|
-
};
|
|
1623
1913
|
const nextOwnership = serializeOwnership([...nextInventory.values()]);
|
|
1624
|
-
if (fs.read(ownershipPath) !== nextOwnership)
|
|
1914
|
+
if (fs.read(ownershipPath) !== nextOwnership) {
|
|
1625
1915
|
changes.push({ path: ownershipPath, content: Buffer.from(nextOwnership, "utf8") });
|
|
1916
|
+
changeActions.set(relative(root, ownershipPath).split(sep).join("/"), "update");
|
|
1917
|
+
}
|
|
1918
|
+
const plan = {
|
|
1919
|
+
target,
|
|
1920
|
+
selected: [...new Set(selected)].sort(),
|
|
1921
|
+
changes: changes
|
|
1922
|
+
.map(({ path, content }) => ({
|
|
1923
|
+
path: relative(root, path).split(sep).join("/"),
|
|
1924
|
+
action: changeActions.get(relative(root, path).split(sep).join("/")) ?? "update",
|
|
1925
|
+
contentFingerprint: stageFingerprint(content ?? new Uint8Array()),
|
|
1926
|
+
}))
|
|
1927
|
+
.sort((a, b) => a.path.localeCompare(b.path)),
|
|
1928
|
+
preserved: [...preserved.values()].sort((a, b) => a.path.localeCompare(b.path)),
|
|
1929
|
+
conflicts: [...new Set(conflicts)].sort(),
|
|
1930
|
+
externalPending: selected.includes("react-best-practices")
|
|
1931
|
+
? [
|
|
1932
|
+
{
|
|
1933
|
+
id: "vercel-react-best-practices",
|
|
1934
|
+
source: "vercel-labs/agent-skills",
|
|
1935
|
+
permissions: ["network:external"],
|
|
1936
|
+
approval: "separate human decision required",
|
|
1937
|
+
},
|
|
1938
|
+
]
|
|
1939
|
+
: [],
|
|
1940
|
+
};
|
|
1941
|
+
const fingerprint = stageFingerprint(JSON.stringify(plan));
|
|
1942
|
+
const preview = `${JSON.stringify({ plan, fingerprint }, null, 2)}\n`;
|
|
1943
|
+
if (!apply)
|
|
1944
|
+
return { preview, fingerprint, conflicts: plan.conflicts };
|
|
1945
|
+
if (!approvedFingerprint)
|
|
1946
|
+
return { preview, fingerprint, conflicts: plan.conflicts };
|
|
1947
|
+
if (approvedFingerprint !== fingerprint)
|
|
1948
|
+
return { error: `The sync plan changed after review; current fingerprint is ${fingerprint}.` };
|
|
1949
|
+
if (plan.conflicts.length)
|
|
1950
|
+
return { error: `Sync plan has conflicts and made no changes: ${plan.conflicts.join(", ")}` };
|
|
1626
1951
|
if (changes.length)
|
|
1627
1952
|
applyProjectFileChanges(fs, root, changes);
|
|
1628
1953
|
return {
|
|
1629
1954
|
output: `Synchronized ${resourceSelections.length} selected bundled resources for ${target} and generated ${generatedFiles.size} project-specific files.`,
|
|
1955
|
+
preview,
|
|
1956
|
+
fingerprint,
|
|
1957
|
+
conflicts: plan.conflicts,
|
|
1630
1958
|
};
|
|
1631
1959
|
}
|
|
1632
1960
|
function personalizeBundledResource(type, id, content, profile) {
|
|
@@ -6,13 +6,13 @@ Make `/ai-init` a guided, evidence-backed onboarding flow. The initial `npx` ins
|
|
|
6
6
|
|
|
7
7
|
## User flow
|
|
8
8
|
|
|
9
|
-
1. **Model and consent.** Before repository analysis, recommend switching to a lower-cost model. Do not change model settings. Ask whether the user switched or wants to continue with the current model.
|
|
9
|
+
1. **Model and consent.** Before repository analysis, recommend switching to a lower-cost model. Do not change model settings. Ask whether the user switched or explicitly wants to continue with the current model. End the agent turn after asking and wait for a reply before scanning, launching subagents, or calling tools. Silence, ambiguity, or a preselected widget value is not consent.
|
|
10
10
|
2. **Parallel project analysis.** Use the host's native subagent mechanism to start independent read-only scans in parallel. Assign separate work to (a) technologies and package/workspace topology, (b) architecture, modules, and code patterns, and (c) tests, quality tools, and CI. The initiating agent is the orchestrator: it consolidates findings, resolves conflicts using repository evidence, and records paths for every material conclusion. If the host cannot start subagents, do the same scans directly and say so.
|
|
11
11
|
3. **Monorepo coverage.** Discover root and nested project manifests, workspace definitions, package boundaries, framework configs, tests, and module entry points. Analyze each workspace separately, then summarize shared tooling and cross-workspace relationships. Never infer that root dependencies apply to every workspace.
|
|
12
12
|
4. **Scan record.** Run the deterministic AI Workflow scan and combine it with the parallel analysis. Present technologies, architecture, patterns, modules, tests, quality commands, workspace topology, confidence, and source evidence. Do not include secret values. Ask for confirmation where conflicting or uncertain findings would affect installed guidance.
|
|
13
|
-
5. **Recommendations.** Match catalog capabilities to the consolidated project profile. Show
|
|
14
|
-
6. **Selection.**
|
|
15
|
-
7. **Install and personalize.** Install only
|
|
13
|
+
5. **Recommendations.** Match catalog capabilities to the consolidated project profile. Show concise tables grouped by resource category (skills, rules, agents, hooks, templates, and MCPs), with one row per item and a short, project-specific reason plus supporting evidence. Include resource types, required permissions, and project-specific configuration steps where relevant. Recommend an MCP only when repository evidence supports its use; disclose its publisher/source, capabilities, access, transport, and credential needs. Never invent credentials or silently enable an MCP.
|
|
14
|
+
6. **Selection.** After the recommendation tables, use the coding host's native selection widget when it supports one. First offer: install all recommendations, customize, or install none. No option may be preselected or inferred. Wait for an explicit submission; a dismissed widget, blank response, silence, or ambiguity must stop progress without selecting or installing anything. For customization, show selectable blocks grouped by resource type, then the individual recommended items in each selected block; retain each item's concise reason and evidence in the selection details. Include MCPs as a separate block. Keep dependencies visible and select required dependencies together. If the host has no selection widget, ask an equivalent numbered prompt, end the turn, and wait for an explicit all/custom/none reply.
|
|
15
|
+
7. **Install and personalize.** After an explicit non-empty selection, show the exact resources and prerequisite capabilities that will be installed, ask for confirmation, end the turn, and wait. Install only after that confirmation arrives in a later turn. For “all”, install every recommendation that does not require an unapproved external permission. A missing external permission must not block independent bundled resources; report the external item as pending and give the user the exact retry command. For custom selection, install only selected items and their declared dependencies. Personalize resources only when project evidence materially changes their instructions (for example, test commands, workspace/module layout, framework patterns, or architectural boundaries). Preserve existing files and secrets; report conflicts rather than overwriting. MCP setup must use the selected host's supported project-level configuration, preserve existing configuration, use environment-variable references for secrets, and request separate approval for permissions or external execution.
|
|
16
16
|
8. **Completion.** Report installed resources by block, project-specific changes, skipped recommendations, conflicts, MCP authentication/trust steps, and validation performed. Recommend restarting/reloading the coding-agent session so newly installed resources are discovered. Do not claim a restart is required when the host loads changes immediately; distinguish host behavior where known.
|
|
17
17
|
|
|
18
18
|
## Safety and compatibility
|
|
@@ -31,12 +31,17 @@ Make `/ai-init` a guided, evidence-backed onboarding flow. The initial `npx` ins
|
|
|
31
31
|
- **AIINIT-003:** On supported hosts, three independent read-only analysis agents start in parallel and the orchestrator consolidates their evidence; unsupported hosts use a disclosed sequential fallback.
|
|
32
32
|
- **AIINIT-004:** Scans include separate summaries for every detected monorepo workspace and do not attribute root dependencies to all packages.
|
|
33
33
|
- **AIINIT-005:** Recommendations identify matching evidence, resource blocks, dependencies, MCP sources, and permissions.
|
|
34
|
+
- **AIINIT-012:** Recommendations are displayed in concise tables grouped by category, and each item has a project-specific reason and supporting evidence before selection.
|
|
34
35
|
- **AIINIT-006:** The selection UI offers install-all, custom, and none; custom selections can choose individual items by skills, rules, agents, hooks, templates, and MCPs.
|
|
35
36
|
- **AIINIT-007:** Installation activates only selected resources and required dependencies, personalizes only context-sensitive content, and preserves unowned or edited files.
|
|
36
37
|
- **AIINIT-008:** MCP setup is target-aware, merges safely with existing configuration, never stores literal secrets, and requires separate permission approval.
|
|
37
38
|
- **AIINIT-009:** Completion lists changes and recommends a session restart/reload to discover installed resources.
|
|
38
39
|
- **AIINIT-010:** CLI tests cover mixed-stack monorepos, all/custom/none selection, permission gating, MCP config merging, personalization, conflicts, and completion output.
|
|
39
40
|
- **AIINIT-011:** Custom selections can choose individual resource references by block, while `--select=all` selects every recommended capability and `--select=` selects none.
|
|
41
|
+
- **AIINIT-013:** Sync previews exact additions, updates, preserved files, conflicts, and external permissions without writing; apply is bound to the approved plan fingerprint and is transactional. External skills and MCP actions require separate approvals, so network denial cannot partially apply local resources.
|
|
42
|
+
- **AIINIT-014:** The bootstrap pauses the agent turn at the model choice, recommendation selection, and final install confirmation; no scan or install proceeds from silence, ambiguity, cancellation, or a default/preselected choice.
|
|
43
|
+
- **AIINIT-015:** Every applicable SDD stage has a mandatory human approval checkpoint before the next stage; automated quality gates are explicitly distinct from human approval, and the agent must stop the turn until an explicit decision arrives.
|
|
44
|
+
- **AIINIT-016:** A versioned per-workflow approval ledger binds each human decision to the exact evidence fingerprint, blocks out-of-order commands, invalidates downstream approvals when evidence changes, and explains the next action in workflow status.
|
|
40
45
|
|
|
41
46
|
## Delivery slices
|
|
42
47
|
|
package/docs/architecture.md
CHANGED
|
@@ -43,6 +43,14 @@ Target files such as `.cursor/rules`, `.claude/skills`, or Codex skill directori
|
|
|
43
43
|
|
|
44
44
|
Project analysis is hybrid: deterministic detectors establish observable facts, while a scoped AI interpreter explains conventions and produces recommendations. AI output is never confirmed project state without user approval.
|
|
45
45
|
|
|
46
|
+
## Human workflow approvals
|
|
47
|
+
|
|
48
|
+
The SDD approval contract models the ordered stages brainstorming, specification, technical design, plan, implementation, verification, review, and traceability. Brainstorming and technical design are optional; skipping either requires a human-accepted reason. Every other stage must be approved. Automated quality-gate results are stored separately from human decisions and can only move an artifact to pending review, never directly to approved.
|
|
49
|
+
|
|
50
|
+
Each approval is scoped to one workflow ID and bound to a SHA-256 fingerprint of the exact artifact reviewed, so a later feature cannot reuse earlier approvals. A content change or rejection makes that stage and its descendants stale. The CLI persists the versioned ledger in `.aiw/approvals.yml`, blocks stage commands when prerequisites are unresolved, and keeps passing quality gates distinct from human approval. Approval records provide an audit trail but cannot prove the identity of a person who responded in a host conversation.
|
|
51
|
+
|
|
52
|
+
Every stage has a stable evidence path and an automated completeness gate. Design, implementation, review, and traceability use explicit report templates; a passing completeness check still leaves the stage pending until a human accepts the exact evidence.
|
|
53
|
+
|
|
46
54
|
## Modularity
|
|
47
55
|
|
|
48
56
|
The CLI is an entry point, not the business layer. Filesystem, package providers, scanners, recommendation engines, generators, validators, and adapters must be replaceable through interfaces and dependency injection.
|
package/docs/backlog.md
CHANGED
|
@@ -43,6 +43,7 @@ Priority levels: **P0** blocks the product or makes it unsafe; **P1** is require
|
|
|
43
43
|
- [x] **SDD-005** Implement verification skill. _Acceptance:_ missing evidence and untested requirements are reported.
|
|
44
44
|
- [x] **SDD-006** Implement requirement-to-evidence traceability graph. _Acceptance:_ requirement → decision → task → code → test → evidence is queryable.
|
|
45
45
|
- [x] **SDD-007** Add quality gates between workflow stages. _Acceptance:_ incomplete artifacts block progression with actionable feedback.
|
|
46
|
+
- [x] **SDD-008** Connect workflow recommendations and quality gates to the SDD sequence. _Acceptance:_ selected later stages include declared prerequisites; verification is recommended for supported project profiles and test runners; each gate checks the matching artifact; review instructions do not refer to a nonexistent CLI gate.
|
|
46
47
|
|
|
47
48
|
## Adapters and migration — P1/P2
|
|
48
49
|
|
package/package.json
CHANGED
package/resources/package.yaml
CHANGED
|
@@ -69,6 +69,8 @@ resources:
|
|
|
69
69
|
- { id: task, version: 0.1.0, path: templates/task.md }
|
|
70
70
|
- { id: test-plan, version: 0.1.0, path: templates/test-plan.md }
|
|
71
71
|
- { id: verification-report, version: 0.1.0, path: templates/verification-report.md }
|
|
72
|
+
- { id: implementation-evidence, version: 0.1.0, path: templates/implementation-evidence.md }
|
|
73
|
+
- { id: code-review, version: 0.1.0, path: templates/code-review.md }
|
|
72
74
|
- { id: security-review, version: 0.1.0, path: templates/security-review.md }
|
|
73
75
|
- { id: migration-report, version: 0.1.0, path: templates/migration-report.md }
|
|
74
76
|
provides:
|
|
@@ -7,3 +7,4 @@ description: Turn an early product idea into structured goals, assumptions, cons
|
|
|
7
7
|
|
|
8
8
|
Separate facts, hypotheses, decisions, and open questions. Identify users, desired outcomes, constraints, risks, and non-goals. Do not prematurely prescribe implementation. Produce a versioned brainstorm artifact in English.
|
|
9
9
|
|
|
10
|
+
Start an AI Workflow session for each feature with `npx --yes --package=@multileaf/ai-workflow -- aiw workflow start` unless one is already active. When the request is already clear, explain why brainstorming can be skipped, ask the human to accept the skip, stop, and record only an explicit acceptance with `aiw skip brainstorming --reason=<accepted reason>`. Otherwise create `.aiw/generated/specs/brainstorm.md`, run `aiw gate brainstorming`, present the exact artifact and a concise summary, and ask whether to proceed to requirements. Stop the turn and wait. Only after an explicit approval, record it with `aiw approve brainstorming`; do not start or scaffold requirements before that command succeeds. If changes are requested, first record `aiw reject brainstorming --reason=<feedback>`, then revise the artifact, rerun its gate, and ask again. Use `npx --yes --package=@multileaf/ai-workflow -- aiw <command>` when no global CLI is available.
|