@multileaf/ai-workflow 0.1.5 → 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 +13 -0
- package/README.md +52 -24
- package/dist/adapter.js +17 -9
- 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 +59 -7
- package/dist/types.d.ts +2 -0
- package/dist/types.js +10 -0
- package/dist/workflow.js +347 -47
- package/docs/ai-init-workflow-spec.md +7 -4
- package/docs/architecture.md +8 -0
- package/package.json +1 -1
- package/resources/package.yaml +2 -0
- package/resources/skills/brainstorming.md +1 -1
- package/resources/skills/code-review.md +1 -1
- package/resources/skills/implementation-planning.md +1 -1
- package/resources/skills/requirements-specification.md +1 -1
- package/resources/skills/tdd-development.md +1 -1
- package/resources/skills/technical-design.md +1 -1
- package/resources/skills/verification.md +1 -1
- package/resources/templates/code-review.md +21 -0
- package/resources/templates/implementation-evidence.md +17 -0
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
|
+
];
|
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." };
|
|
@@ -750,32 +885,48 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
750
885
|
const selectedArg = args.find((arg) => arg.startsWith("--select="))?.split("=", 2)[1];
|
|
751
886
|
const selected = selectedArg?.split(",") ?? (command === "sync" ? readSelectedRecommendations(fs, aiw) : []);
|
|
752
887
|
if (command === "sync") {
|
|
753
|
-
const
|
|
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);
|
|
754
897
|
if (result.error)
|
|
755
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
|
+
};
|
|
756
916
|
const messages = [result.output ?? "Selected bundled resources synchronized."];
|
|
757
917
|
if (selected.includes("react-best-practices")) {
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
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
|
+
});
|
|
761
925
|
}
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
enforceConfiguredOrganizationPolicy(fs, aiw, {
|
|
765
|
-
provider: "vercel-skills",
|
|
766
|
-
source: "vercel-labs/agent-skills",
|
|
767
|
-
permissions: ["network:external"],
|
|
768
|
-
});
|
|
769
|
-
const target = parseManifest(fs.read(join(aiw, "manifest.yml"))).target.active;
|
|
770
|
-
await installVercelSkill(services.externalSkills ?? nodeCommandExecutor, "vercel-labs/agent-skills", "vercel-react-best-practices", target);
|
|
771
|
-
writeVercelSkillLock(fs, root, aiw, "vercel-labs/agent-skills", "vercel-react-best-practices");
|
|
772
|
-
messages.push("Remote React best-practices skill installed.");
|
|
773
|
-
}
|
|
774
|
-
catch (error) {
|
|
775
|
-
const reason = error instanceof Error ? error.message : String(error);
|
|
776
|
-
messages.push(`Remote skill pending: ${reason}`);
|
|
777
|
-
}
|
|
926
|
+
catch (error) {
|
|
927
|
+
pendingReason = error instanceof Error ? error.message : String(error);
|
|
778
928
|
}
|
|
929
|
+
messages.push(`Remote skill pending: ${pendingReason}. Install it separately with aiw skills install.`);
|
|
779
930
|
}
|
|
780
931
|
return { exitCode: 0, output: messages.join(" ") };
|
|
781
932
|
}
|
|
@@ -818,7 +969,14 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
818
969
|
try {
|
|
819
970
|
const manifest = fs.read(join(aiw, "manifest.yml"));
|
|
820
971
|
parseManifest(manifest);
|
|
821
|
-
|
|
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
|
+
};
|
|
822
980
|
}
|
|
823
981
|
catch (error) {
|
|
824
982
|
return { exitCode: 1, error: error instanceof Error ? error.message : String(error) };
|
|
@@ -946,6 +1104,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
946
1104
|
if (command === "brainstorm") {
|
|
947
1105
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
948
1106
|
return { exitCode: 1, error: "Run `aiw install` first." };
|
|
1107
|
+
assertStageReady(aiw, fs, "brainstorming");
|
|
949
1108
|
const title = args.find((arg) => arg.startsWith("--title="))?.slice(8) || "Untitled Brainstorm";
|
|
950
1109
|
const path = join(aiw, "generated/specs/brainstorm.md");
|
|
951
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`);
|
|
@@ -954,6 +1113,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
954
1113
|
if (command === "spec") {
|
|
955
1114
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
956
1115
|
return { exitCode: 1, error: "Run `aiw install` first." };
|
|
1116
|
+
assertStageReady(aiw, fs, "specification");
|
|
957
1117
|
const title = args.find((arg) => arg.startsWith("--title="))?.slice(8) || "Untitled Specification";
|
|
958
1118
|
const path = join(aiw, "generated/specs/specification.md");
|
|
959
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`);
|
|
@@ -962,6 +1122,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
962
1122
|
if (command === "adr") {
|
|
963
1123
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
964
1124
|
return { exitCode: 1, error: "Run `aiw install` first." };
|
|
1125
|
+
assertStageReady(aiw, fs, "technical-design");
|
|
965
1126
|
const id = args.find((arg) => arg.startsWith("--id="))?.slice(5) || "001";
|
|
966
1127
|
const title = args.find((arg) => arg.startsWith("--title="))?.slice(8) || "Untitled Decision";
|
|
967
1128
|
const path = join(root, ".context/adrs", `ADR-${id}-${title.toLowerCase().replaceAll(" ", "-")}.md`);
|
|
@@ -971,6 +1132,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
971
1132
|
if (command === "plan") {
|
|
972
1133
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
973
1134
|
return { exitCode: 1, error: "Run `aiw install` first." };
|
|
1135
|
+
assertStageReady(aiw, fs, "plan");
|
|
974
1136
|
const title = args.find((arg) => arg.startsWith("--title="))?.slice(8) || "Untitled Implementation Plan";
|
|
975
1137
|
const path = join(aiw, "generated/plans/implementation-plan.md");
|
|
976
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`);
|
|
@@ -979,6 +1141,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
979
1141
|
if (command === "verify") {
|
|
980
1142
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
981
1143
|
return { exitCode: 1, error: "Run `aiw install` first." };
|
|
1144
|
+
assertStageReady(aiw, fs, "verification");
|
|
982
1145
|
const specPath = join(aiw, "generated/specs/specification.md");
|
|
983
1146
|
const planPath = join(aiw, "generated/plans/implementation-plan.md");
|
|
984
1147
|
const issues = [];
|
|
@@ -1011,6 +1174,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
1011
1174
|
if (command === "trace") {
|
|
1012
1175
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
1013
1176
|
return { exitCode: 1, error: "Run `aiw install` first." };
|
|
1177
|
+
assertStageReady(aiw, fs, "traceability");
|
|
1014
1178
|
const specPath = join(aiw, "generated/specs/specification.md");
|
|
1015
1179
|
const planPath = join(aiw, "generated/plans/implementation-plan.md");
|
|
1016
1180
|
if (!fs.exists(specPath) || !fs.exists(planPath))
|
|
@@ -1042,25 +1206,36 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
1042
1206
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
1043
1207
|
return { exitCode: 1, error: "Run `aiw install` first." };
|
|
1044
1208
|
const stage = args[1];
|
|
1045
|
-
|
|
1046
|
-
brainstorming: "generated/specs/brainstorm.md",
|
|
1047
|
-
specification: "generated/specs/specification.md",
|
|
1048
|
-
plan: "generated/plans/implementation-plan.md",
|
|
1049
|
-
verification: "generated/reports/verification-report.md",
|
|
1050
|
-
};
|
|
1051
|
-
if (!stage || !Object.hasOwn(requirements, stage))
|
|
1209
|
+
if (!stage || !APPROVAL_STAGES.includes(stage))
|
|
1052
1210
|
return {
|
|
1053
1211
|
exitCode: 1,
|
|
1054
|
-
error:
|
|
1212
|
+
error: `Usage: aiw gate <${APPROVAL_STAGES.join("|")}>`,
|
|
1055
1213
|
};
|
|
1056
|
-
const
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
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}.`,
|
|
1238
|
+
};
|
|
1064
1239
|
}
|
|
1065
1240
|
if (command === "validate") {
|
|
1066
1241
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
@@ -1411,13 +1586,81 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
1411
1586
|
}
|
|
1412
1587
|
return {
|
|
1413
1588
|
exitCode: 0,
|
|
1414
|
-
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]",
|
|
1415
1590
|
};
|
|
1416
1591
|
}
|
|
1417
1592
|
catch (error) {
|
|
1418
1593
|
return { exitCode: 1, error: error instanceof Error ? error.message : String(error) };
|
|
1419
1594
|
}
|
|
1420
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
|
+
}
|
|
1421
1664
|
function parseNamedOptions(args, allowed) {
|
|
1422
1665
|
const entries = args.map((arg) => arg.match(/^--([a-z-]+)=(.+)$/)?.slice(1));
|
|
1423
1666
|
if (args.some((arg) => arg.indexOf("=") !== arg.lastIndexOf("=")) ||
|
|
@@ -1561,7 +1804,7 @@ function containsCredential(value, credential, visited = new Set()) {
|
|
|
1561
1804
|
visited.add(value);
|
|
1562
1805
|
return Object.values(value).some((entry) => containsCredential(entry, credential, visited));
|
|
1563
1806
|
}
|
|
1564
|
-
function syncSelectedResources(fs, root, aiw, profile, selected) {
|
|
1807
|
+
function syncSelectedResources(fs, root, aiw, profile, selected, apply = false, approvedFingerprint) {
|
|
1565
1808
|
const target = parseManifest(fs.read(join(aiw, "manifest.yml"))).target.active;
|
|
1566
1809
|
const resourceSelections = resourcesForCapabilities(selected);
|
|
1567
1810
|
const bundled = loadBundledResources(target);
|
|
@@ -1612,7 +1855,16 @@ function syncSelectedResources(fs, root, aiw, profile, selected) {
|
|
|
1612
1855
|
const inventory = parseOwnership(fs.read(ownershipPath));
|
|
1613
1856
|
const nextInventory = new Map(inventory.map((entry) => [entry.path, entry]));
|
|
1614
1857
|
const changes = [];
|
|
1858
|
+
const changeActions = new Map();
|
|
1859
|
+
const preserved = new Map();
|
|
1615
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
|
+
};
|
|
1616
1868
|
for (const [path, { content, metadata }] of desired) {
|
|
1617
1869
|
assertSafeProjectPath(fs, root, path);
|
|
1618
1870
|
const relativePath = relative(root, path).split(sep).join("/");
|
|
@@ -1620,41 +1872,89 @@ function syncSelectedResources(fs, root, aiw, profile, selected) {
|
|
|
1620
1872
|
const owned = nextInventory.get(relativePath);
|
|
1621
1873
|
if (type === "missing") {
|
|
1622
1874
|
changes.push({ path, content: Buffer.from(content, "utf8") });
|
|
1875
|
+
changeActions.set(relativePath, "add");
|
|
1623
1876
|
nextInventory.set(relativePath, createOwnedFile(relativePath, content, metadata));
|
|
1624
1877
|
continue;
|
|
1625
1878
|
}
|
|
1626
1879
|
if (type !== "file" || !owned) {
|
|
1627
1880
|
conflicts.push(relativePath);
|
|
1881
|
+
preserve(relativePath, type);
|
|
1628
1882
|
continue;
|
|
1629
1883
|
}
|
|
1630
1884
|
const actual = fs.read(path);
|
|
1631
1885
|
if (createOwnedFile(relativePath, actual).checksum !== owned.checksum) {
|
|
1632
1886
|
conflicts.push(relativePath);
|
|
1887
|
+
preserve(relativePath, type, actual);
|
|
1633
1888
|
continue;
|
|
1634
1889
|
}
|
|
1635
|
-
if (actual !== content)
|
|
1890
|
+
if (actual !== content) {
|
|
1636
1891
|
changes.push({ path, content: Buffer.from(content, "utf8") });
|
|
1892
|
+
changeActions.set(relativePath, "update");
|
|
1893
|
+
}
|
|
1894
|
+
else
|
|
1895
|
+
preserve(relativePath, type, actual);
|
|
1637
1896
|
nextInventory.set(relativePath, createOwnedFile(relativePath, content, metadata));
|
|
1638
1897
|
}
|
|
1639
1898
|
for (const [path, content] of generatedFiles) {
|
|
1640
1899
|
assertSafeProjectPath(fs, root, path);
|
|
1641
1900
|
const type = fs.pathType?.(path);
|
|
1642
|
-
|
|
1901
|
+
const relativePath = relative(root, path).split(sep).join("/");
|
|
1902
|
+
if (type === "missing") {
|
|
1643
1903
|
changes.push({ path, content: Buffer.from(content, "utf8") });
|
|
1644
|
-
|
|
1904
|
+
changeActions.set(relativePath, "add");
|
|
1905
|
+
}
|
|
1906
|
+
else if (type !== "file" || fs.read(path) !== content) {
|
|
1645
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));
|
|
1646
1912
|
}
|
|
1647
|
-
if (conflicts.length)
|
|
1648
|
-
return {
|
|
1649
|
-
error: `Sync preserved existing or modified project resources: ${[...new Set(conflicts)].sort().join(", ")}`,
|
|
1650
|
-
};
|
|
1651
1913
|
const nextOwnership = serializeOwnership([...nextInventory.values()]);
|
|
1652
|
-
if (fs.read(ownershipPath) !== nextOwnership)
|
|
1914
|
+
if (fs.read(ownershipPath) !== nextOwnership) {
|
|
1653
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(", ")}` };
|
|
1654
1951
|
if (changes.length)
|
|
1655
1952
|
applyProjectFileChanges(fs, root, changes);
|
|
1656
1953
|
return {
|
|
1657
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,
|
|
1658
1958
|
};
|
|
1659
1959
|
}
|
|
1660
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
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. 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,
|
|
15
|
-
7. **Install and personalize.** Install only
|
|
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
|
|
@@ -38,7 +38,10 @@ Make `/ai-init` a guided, evidence-backed onboarding flow. The initial `npx` ins
|
|
|
38
38
|
- **AIINIT-009:** Completion lists changes and recommends a session restart/reload to discover installed resources.
|
|
39
39
|
- **AIINIT-010:** CLI tests cover mixed-stack monorepos, all/custom/none selection, permission gating, MCP config merging, personalization, conflicts, and completion output.
|
|
40
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:**
|
|
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.
|
|
42
45
|
|
|
43
46
|
## Delivery slices
|
|
44
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.
|