@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 CHANGED
@@ -4,6 +4,19 @@ All notable changes to this project are documented in this file. The format foll
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.2.0] - 2026-09-13
8
+
9
+ ### Added
10
+
11
+ - Persisted human approval gates for every SDD stage, with explicit optional-stage skips and invalidation when reviewed evidence changes.
12
+ - Reviewable, fingerprint-bound installation previews with conflict detection and rollback on failed syncs.
13
+ - Reusable implementation evidence and code review templates, plus completeness checks for every workflow stage.
14
+
15
+ ### Changed
16
+
17
+ - Require an active workflow, ordered stage evidence, a passing quality gate, and explicit human approval before advancing SDD work.
18
+ - Align generated bootstrap and stage skills with the enforced approval and installation flows.
19
+
7
20
  ## [0.1.5] - 2026-09-12
8
21
 
9
22
  ### Fixed
package/README.md CHANGED
@@ -126,27 +126,35 @@ flowchart TD
126
126
 
127
127
  G -->|Yes| H[Brainstorm]
128
128
  H --> I[(generated/specs/brainstorm.md)]
129
- I --> J{Brainstorm gate}
129
+ I --> J{Brainstorm quality gate}
130
130
  J -->|Incomplete| H
131
- J -->|Complete| K[Write specification]
131
+ J -->|Complete| JH{Human approves brainstorm?}
132
+ JH -->|Revise| H
133
+ JH -->|Approve| K[Write specification]
132
134
  G -->|No| K
133
135
 
134
136
  K --> L[(generated/specs/specification.md)]
135
- L --> M{Specification gate}
137
+ L --> M{Specification quality gate}
136
138
  M -->|Incomplete| K
137
- M -->|Complete| N{Architecture decision needed?}
139
+ M -->|Complete| MH{Human approves requirements?}
140
+ MH -->|Revise| K
141
+ MH -->|Approve| N{Architecture decision needed?}
138
142
 
139
143
  N -->|Yes| O[Create ADR]
140
144
  O --> P[(.context/adrs/ADR-NNN-title.md)]
141
145
  P --> Q[Update ADR index]
142
146
  Q --> R[(.context/adrs/INDEX.md)]
143
- N -->|No| S[Create implementation plan]
144
- R --> S
147
+ R --> RH{Human approves design?}
148
+ RH -->|Revise| O
149
+ RH -->|Approve| S[Create implementation plan]
150
+ N -->|No| S
145
151
 
146
152
  S --> T[(generated/plans/implementation-plan.md)]
147
- T --> U{Plan gate}
153
+ T --> U{Plan quality gate}
148
154
  U -->|Incomplete| S
149
- U -->|Ready| V[Implement tasks with TDD]
155
+ U -->|Ready| UH{Human approves plan?}
156
+ UH -->|Revise| S
157
+ UH -->|Approve| V[Implement tasks with TDD]
150
158
 
151
159
  V --> W[Write code and tests]
152
160
  W --> X[Run task validation commands]
@@ -155,17 +163,24 @@ flowchart TD
155
163
  Y -->|Yes| Z[Record completion evidence]
156
164
 
157
165
  Z --> AA[(generated/artifacts and checkpoint evidence)]
158
- AA --> AB[Write verification report]
159
- AB --> AC{Verification gate}
166
+ AA --> AAH{Human accepts implementation?}
167
+ AAH -->|Revise| V
168
+ AAH -->|Approve| AB[Write verification report]
169
+ AB --> AC{Verification quality gate}
160
170
  AC -->|Missing evidence| V
161
- AC -->|Complete| AD[Build traceability graph]
162
-
163
- AD --> AE[Requirement → ADR → Task → Code → Test → Evidence]
164
- AE --> AF[(generated artifacts and trace output)]
165
- AF --> AG[Run code-review skill when installed]
171
+ AC -->|Complete| ACH{Human approves verification?}
172
+ ACH -->|Revise| V
173
+ ACH -->|Approve| AG[Run code-review skill when installed]
166
174
  AG --> AH{Review findings?}
167
175
  AH -->|Changes required| V
168
- AH -->|Clear| AI[Run repository self-validation]
176
+ AH -->|Clear| AHR{Human accepts review outcome?}
177
+ AHR -->|Address findings| V
178
+ AHR -->|Approve| AD[Build final traceability graph]
179
+ AD --> AE[Requirement → ADR → Task → Code → Test → Evidence]
180
+ AE --> AF[(generated artifacts and trace output)]
181
+ AF --> AFH{Human approves traceability?}
182
+ AFH -->|Revise| AD
183
+ AFH -->|Approve| AI[Run repository self-validation]
169
184
  AI --> AJ[(.aiw/checkpoints/self-validation-TICKET.yml)]
170
185
  AJ --> AK[Commit and independent integration validation]
171
186
 
@@ -185,24 +200,37 @@ npx @multileaf/ai-workflow recommend
185
200
  # In CI, provide comma-separated recommendation IDs detected for that project.
186
201
  npx @multileaf/ai-workflow recommend --select=typescript-quality,verification
187
202
 
188
- # Create structured product artifacts.
203
+ # Track a feature from its first artifact through final human approval.
204
+ npx @multileaf/ai-workflow workflow start
189
205
  npx @multileaf/ai-workflow brainstorm --title="Feature name"
206
+ npx @multileaf/ai-workflow gate brainstorming
207
+ # Present evidence and wait for a human reply before each approve command.
208
+ npx @multileaf/ai-workflow approve brainstorming
190
209
  npx @multileaf/ai-workflow spec --title="Feature requirements"
210
+ npx @multileaf/ai-workflow gate specification
211
+ npx @multileaf/ai-workflow approve specification
191
212
  npx @multileaf/ai-workflow adr --id=001 --title="Technical decision"
213
+ npx @multileaf/ai-workflow gate technical-design
214
+ npx @multileaf/ai-workflow approve technical-design
192
215
  npx @multileaf/ai-workflow plan --title="Implementation plan"
193
-
194
- # Optional, after creating a brainstorm artifact for an unclear request:
195
- npx @multileaf/ai-workflow gate brainstorming
196
- # After filling the specification and plan scaffolds shown above:
197
- npx @multileaf/ai-workflow gate specification
198
216
  npx @multileaf/ai-workflow gate plan
199
- # After writing .aiw/generated/reports/verification-report.md from the verification report template:
217
+ npx @multileaf/ai-workflow approve plan
218
+ # After implementation, write implementation-report.md and pass its gate.
219
+ npx @multileaf/ai-workflow gate implementation
220
+ npx @multileaf/ai-workflow approve implementation
221
+ # Write verification-report.md and code-review.md before their gates.
200
222
  npx @multileaf/ai-workflow gate verification
223
+ npx @multileaf/ai-workflow approve verification
224
+ npx @multileaf/ai-workflow gate review
225
+ npx @multileaf/ai-workflow approve review
201
226
  npx @multileaf/ai-workflow verify
202
227
  npx @multileaf/ai-workflow trace
228
+ npx @multileaf/ai-workflow gate traceability
229
+ npx @multileaf/ai-workflow approve traceability
230
+ npx @multileaf/ai-workflow workflow complete
203
231
  ```
204
232
 
205
- Scaffold commands write deterministic files at fixed paths and a repeated invocation replaces that scaffold. Complete and review each artifact before advancing. Requirements use stable identifiers and Given/When/Then acceptance criteria. Plans link tasks to requirements, tests, risks, dependencies, and expected evidence. Each `gate` validates the artifact named by its stage. `verify` checks that the specification has acceptance criteria and the plan links requirements to validation; it does not execute project test commands. `trace` records declared links between requirements, decisions, tasks, code, tests, and evidence. Code review is performed with the `code-review` skill when installed; there is no separate review gate command.
233
+ Scaffold commands write deterministic files at fixed paths and a repeated invocation replaces that scaffold. Each applicable workflow stage requires two separate checks: its automated quality gate (when available), then explicit human approval. After presenting the artifact or evidence, the agent must stop and wait for the human before invoking the next skill, command, scaffold, or implementation action. A passing `aiw gate` validates artifact completeness only; it never records or implies human approval. Requirements use stable identifiers and Given/When/Then acceptance criteria. Plans link tasks to requirements, tests, risks, dependencies, and expected evidence. `verify` checks that the specification has acceptance criteria and the plan links requirements to validation; it does not execute project test commands. `trace` records declared links between requirements, decisions, tasks, code, tests, and evidence. Code review is performed with the `code-review` skill when installed; there is no separate review gate command, but the human must accept the review outcome before completion.
206
234
 
207
235
  ## Project intelligence
208
236
 
package/dist/adapter.js CHANGED
@@ -40,24 +40,32 @@ This skill is the bootstrap for AI Workflow. The installer intentionally install
40
40
 
41
41
  ## First-run setup
42
42
 
43
- 1. Before scanning, recommend that the user switch to a cheaper model for this onboarding. Ask whether they switched or prefer to continue with the current model; never change model settings yourself. Explain that analysis is read-only and optional resources will not be installed without their selection.
43
+ ### Mandatory interaction checkpoints
44
+
45
+ This setup is gated by explicit user replies. A question is a blocking checkpoint, not narration: ask it, end your current response/turn immediately, and do no further analysis, tool calls, commands, or file changes until the user replies in a later turn. Never answer your own question, infer an answer from silence, or continue because a widget has a preselected value. If the host cannot pause for an answer, stop and ask the user to continue the setup after replying.
46
+
47
+ 1. Before scanning, recommend that the user switch to a cheaper model. Ask whether they switched or explicitly want to continue with the current model; never change model settings yourself. Explain that analysis is read-only and optional resources will not be installed without their selection. Then stop the turn and wait. Do not launch analysis agents or run \`aiw scan\` until the user replies. If the reply is unclear, ask again and stop; proceed only after an explicit answer to continue (including an explicit choice to keep the current model).
44
48
  2. Use the host's native subagent mechanism to launch these read-only analyses in parallel: (a) technologies, root and nested manifests, workspaces, and monorepo boundaries; (b) architecture, modules, entry points, and recurring code patterns; (c) tests, quality commands, CI, and developer tools. Keep the main agent as orchestrator. Wait for all results, verify important claims against cited project files, reconcile disagreements, and consolidate one evidence-backed project profile. Do not inspect secrets, ignored files, dependency directories such as \`node_modules\`, or generated output; inspect declared dependencies in project manifests. If subagents are unavailable, perform the same analyses directly and disclose the fallback.
45
49
  3. Run \`npx --yes --package=@multileaf/ai-workflow -- aiw scan\` and read \`.aiw/profile.yml\`. Treat this deterministic scan as another evidence source, not as a replacement for workspace-level analysis. Summarize each workspace separately; do not assume root dependencies or commands apply to every module. Ask the user to confirm uncertain or conflicting facts before using them as project policy. Add confirmed architecture and recurring patterns to the matching entries in the \`project_modules\` JSON array in \`.aiw/profile.yml\`, including file-path evidence; create a root-level entry when only the root project exists. Preserve existing detected fields and never store secrets or source-code excerpts there.
46
50
  4. Run \`npx --yes --package=@multileaf/ai-workflow -- aiw recommend\` and read \`.aiw/recommendations.yml\`. Combine catalog results with the consolidated project profile. Recommend matching skills, rules, agents, hooks, templates, and MCP servers. For every MCP, disclose its publisher/source, purpose, capabilities, permissions, transport, and credential needs; verify them against current official host and server documentation. Recommend only integrations supported by project evidence and never invent credentials.
47
- 5. Present recommendations as concise tables grouped by category: skills, rules, agents, hooks, templates, and MCPs. Include one row per recommended item and columns for the item, a short project-specific reason for the recommendation, and relevant evidence. For MCP rows, also summarize purpose, source, permissions, transport, and credential needs (use a separate table if needed). Do not show a flat list without category headings or generic reasons such as “useful for this project.” After the tables, use the host's native selection widget to offer three choices: install all recommendations, customize, or install none. If the user customizes, show selectable category blocks and individual items, keeping each item's short reason and declared prerequisites visible. Read \`requires\` from \`.aiw/recommendations.yml\`; explain that choosing a stage auto-selects its prerequisite capabilities and their resources. If the host lacks a multi-select widget, use a numbered all/custom/none prompt and grouped checklists. MCP selection does not grant permission to execute the server or use external access; ask separately for those permissions.
48
- 6. Record all recommendations with \`npx --yes --package=@multileaf/ai-workflow -- aiw recommend --select=all\`; for customized item-level choices, pass the selected resource references such as \`skills/verification,rules/tdd-policy,agents/test-engineer\`. Use \`npx --yes --package=@multileaf/ai-workflow -- aiw recommend --select=\` when none are chosen. If any AI Workflow resources are selected, run \`npx --yes --package=@multileaf/ai-workflow -- aiw sync\` without extra permissions first. It installs selected bundled resources and reports any external resource that is still pending approval. Only if the user separately approves external access, rerun with \`--allow=network:external\` to install that pending external resource. Install only selected resources and declared dependencies. Tailor instructions only where project evidence changes their usefulness (such as test commands, workspace boundaries, framework patterns, or module architecture). Preserve existing files, configuration, and secrets; report conflicts rather than overwriting them.
51
+ 5. Present recommendations as concise tables grouped by category: skills, rules, agents, hooks, templates, and MCPs. Include one row per recommended item and columns for the item, a short project-specific reason for the recommendation, and relevant evidence. For MCP rows, also summarize purpose, source, permissions, transport, and credential needs (use a separate table if needed). Do not show a flat list without category headings or generic reasons such as “useful for this project.” After the tables, use the host's native selection widget to offer three explicit choices: install all recommendations, customize, or install none. The widget must start with no choice selected; never accept a default or preselected value as the user's answer. If the user customizes, show selectable category blocks and individual items, keeping each item's short reason and declared prerequisites visible. Read \`requires\` from \`.aiw/recommendations.yml\`; explain that choosing a stage auto-selects its prerequisite capabilities and their resources. If the host lacks a multi-select widget, ask a numbered all/custom/none question and stop the turn to wait for the reply. Do not run a selection command or install anything until a reply/explicit widget submission arrives in a later turn. A dismissed widget, empty answer, silence, or ambiguous answer means no consent; ask again or stop without installing. MCP selection does not grant permission to execute the server or use external access; ask separately for those permissions.
52
+ 6. Only after receiving the user's explicit all/custom/none choice, record it: use \`npx --yes --package=@multileaf/ai-workflow -- aiw recommend --select=all\` for an explicit all choice; for customized item-level choices, pass only the selected resource references such as \`skills/verification,rules/tdd-policy,agents/test-engineer\`; use \`npx --yes --package=@multileaf/ai-workflow -- aiw recommend --select=\` for an explicit none choice. Before syncing a non-empty selection, show the exact selected resources and their prerequisites. Run \`npx --yes --package=@multileaf/ai-workflow -- aiw sync --preview\` and show the complete plan, including additions, updates, preserved files, conflicts, and any external permissions. Ask the user to approve this exact plan and stop the turn. Run sync only after that confirmation arrives in a later turn. Only after a later explicit approval, rerun preview if selection or project files changed, then run \`npx --yes --package=@multileaf/ai-workflow -- aiw sync --approve-plan=<fingerprint>\` using the fingerprint from the approved preview. A changed plan or conflict must stop without writes. Remote skills are not installed by sync: ask separately for network access and installation approval, then use \`npx --yes --package=@multileaf/ai-workflow -- aiw skills install --source=vercel-labs/agent-skills --skill=vercel-react-best-practices --allow=network:external\`. MCP configuration, server execution, and credentials each require separate consent. Install only confirmed selected resources and declared dependencies. Tailor instructions only where project evidence changes their usefulness (such as test commands, workspace boundaries, framework patterns, or module architecture). Preserve existing files, configuration, and secrets; report conflicts rather than overwriting them.
49
53
  7. For selected MCPs, add only the selected servers to the host's supported project configuration. Check current official host and server documentation for the configuration format, merge existing settings without replacing unrelated entries, and use environment-variable references instead of literal credentials. Do not run server install/start/authentication commands without separate approval. If the target requires user-level changes or safe configuration cannot be validated, give the user exact manual steps and report the MCP as pending rather than installed.
50
- 8. Report installed resources by block, project-specific changes, skipped recommendations, MCP trust/authentication steps, and conflicts. Recommend restarting or reloading the coding-agent session so it discovers newly installed resources; follow known host behavior if it reloads changes automatically. Introduce only the gates supported by installed resources: brainstorming → specification → architecture decision when needed → implementation plan → implementation with tests → verification → traceability → review.
54
+ 8. Report installed resources by block, project-specific changes, skipped recommendations, MCP trust/authentication steps, and conflicts. Recommend restarting or reloading the coding-agent session so it discovers newly installed resources; follow known host behavior if it reloads changes automatically. Introduce only the gates supported by installed resources: brainstorming → specification → architecture decision when needed → implementation plan → implementation with tests → verification → code review → traceability.
51
55
 
52
56
  If the user declines all recommendations, leave the project with only the bootstrap skill and scan state. Do not treat scanning or recommendation as consent to install.
53
57
 
54
58
  ## For each feature request after setup
55
59
 
56
- - Use the brainstorming skill when scope, users, constraints, or assumptions are unclear; create its artifact with \`npx --yes --package=@multileaf/ai-workflow -- aiw brainstorm\`, then validate it with \`npx --yes --package=@multileaf/ai-workflow -- aiw gate brainstorming\`.
57
- - Create a specification with stable requirement IDs and Given/When/Then acceptance criteria; pass \`npx --yes --package=@multileaf/ai-workflow -- aiw gate specification\` before planning.
58
- - For changes with architecture impact, use technical design, record consequential choices as ADRs with \`npx --yes --package=@multileaf/ai-workflow -- aiw adr\`, and link those decisions to the requirements. Then create an implementation plan with \`npx --yes --package=@multileaf/ai-workflow -- aiw plan\`, linking tasks to requirements, tests, risks, and validation commands; pass \`npx --yes --package=@multileaf/ai-workflow -- aiw gate plan\` before coding.
59
- - When TDD resources are installed and the project has a test workflow, follow the TDD policy; otherwise use the project's existing validation methods. Follow installed project quality rules and use specialist agents only when they are installed and useful.
60
- - Run the declared checks, write \`.aiw/generated/reports/verification-report.md\` using the verification-report template, and pass \`npx --yes --package=@multileaf/ai-workflow -- aiw gate verification\`. Then run \`npx --yes --package=@multileaf/ai-workflow -- aiw verify\` and \`npx --yes --package=@multileaf/ai-workflow -- aiw trace\`. Use the code-review skill when installed and record its findings; AI Workflow does not provide a separate review gate. Do not claim completion without evidence.
60
+ ### Mandatory human gates between stages
61
+
62
+ For every CLI command below, use \`npx --yes --package=@multileaf/ai-workflow -- aiw <command>\`; do not assume a global \`aiw\` binary exists. Start each feature request with \`npx --yes --package=@multileaf/ai-workflow -- aiw workflow start\` and check progress with \`npx --yes --package=@multileaf/ai-workflow -- aiw workflow status\`. Complete it with \`npx --yes --package=@multileaf/ai-workflow -- aiw workflow complete\` only after all stages resolve. The \`aiw gate <stage>\` command checks evidence and records a pending review; a passing result is not human approval. At every stage boundary—brainstorming, specification, technical design, plan, implementation, verification, code review, and traceability—present the exact artifact/evidence, ask whether the user approves proceeding, end the turn, and wait for an explicit reply in a later turn. On approval, record it with \`npx --yes --package=@multileaf/ai-workflow -- aiw approve <stage>\`; if the user asks for changes, record the rejection first with \`npx --yes --package=@multileaf/ai-workflow -- aiw reject <stage> --reason=<feedback>\`, then revise and rerun the gate. Do not invoke the next skill, create its scaffold, run its commands, or edit its artifacts before approval is recorded or the pending stage is rejected. Silence, an ambiguous reply, an automated gate pass, or earlier approval never counts. Brainstorming and technical design may be skipped only after explaining why and receiving explicit human acceptance; record each skip with \`npx --yes --package=@multileaf/ai-workflow -- aiw skip <stage> --reason=<accepted reason>\`.
63
+
64
+ - Use the brainstorming skill when scope, users, constraints, or assumptions are unclear; create its artifact with \`npx --yes --package=@multileaf/ai-workflow -- aiw brainstorm\`, validate it with \`npx --yes --package=@multileaf/ai-workflow -- aiw gate brainstorming\`, present it, wait, then record the reply with \`npx --yes --package=@multileaf/ai-workflow -- aiw approve brainstorming\` before specification. If unnecessary, ask to skip and record explicit acceptance with \`npx --yes --package=@multileaf/ai-workflow -- aiw skip brainstorming --reason=...\`.
65
+ - Create a specification with stable requirement IDs and Given/When/Then acceptance criteria; pass \`npx --yes --package=@multileaf/ai-workflow -- aiw gate specification\`, present it, wait, and record approval before design or planning.
66
+ - For changes with architecture impact, write \`.aiw/generated/reports/technical-design.md\` using the technical-design template, record consequential choices as ADRs, pass \`npx --yes --package=@multileaf/ai-workflow -- aiw gate technical-design\`, and obtain/record approval before planning. If design is unnecessary, obtain explicit acceptance and record a reasoned skip. Then create an implementation plan with \`npx --yes --package=@multileaf/ai-workflow -- aiw plan\`, pass \`npx --yes --package=@multileaf/ai-workflow -- aiw gate plan\`, present it, wait, and record approval before coding.
67
+ - When TDD resources are installed and the project has a test workflow, follow the TDD policy; otherwise use the project's existing validation methods. Follow installed project quality rules and use specialist agents only when useful. After implementation, write \`.aiw/generated/reports/implementation-report.md\` with changed files, tests, validation, and deviations; pass \`npx --yes --package=@multileaf/ai-workflow -- aiw gate implementation\`, present the diff/evidence, wait, and record approval before verification.
68
+ - Run declared checks, write \`.aiw/generated/reports/verification-report.md\`, pass \`npx --yes --package=@multileaf/ai-workflow -- aiw gate verification\`, run \`npx --yes --package=@multileaf/ai-workflow -- aiw verify\`, present results, wait, and record approval before review. Write \`.aiw/generated/reports/code-review.md\` with findings/checks/risks/decision, pass \`npx --yes --package=@multileaf/ai-workflow -- aiw gate review\`, present findings, wait, and record approval before \`npx --yes --package=@multileaf/ai-workflow -- aiw trace\`. Pass \`npx --yes --package=@multileaf/ai-workflow -- aiw gate traceability\`, present the final graph, wait, record approval, then complete the workflow. Do not claim completion without evidence.
61
69
 
62
70
  ## Language policy
63
71
 
@@ -0,0 +1,51 @@
1
+ import { type HumanApprovalStage } from "./types.js";
2
+ export declare const APPROVAL_STAGES: readonly ["brainstorming", "specification", "technical-design", "plan", "implementation", "verification", "review", "traceability"];
3
+ export type ApprovalStatus = "draft" | "pending-human" | "approved" | "rejected" | "skipped" | "stale";
4
+ export type QualityGateResult = "not-run" | "passed" | "failed" | "not-applicable";
5
+ export type StageApprovalRecord = {
6
+ status: ApprovalStatus;
7
+ qualityGate: QualityGateResult;
8
+ artifactHash?: string;
9
+ reason?: string;
10
+ updatedAt: string;
11
+ };
12
+ export type ApprovalLedger = {
13
+ schema: 1;
14
+ workflowId: string;
15
+ stages: Partial<Record<HumanApprovalStage, StageApprovalRecord>>;
16
+ };
17
+ export type WorkflowSession = {
18
+ schema: 1;
19
+ workflowId: string;
20
+ status: "active" | "complete";
21
+ startedAt: string;
22
+ updatedAt: string;
23
+ };
24
+ export type ApprovalStageDefinition = {
25
+ optional: boolean;
26
+ requiresQualityGate: boolean;
27
+ };
28
+ export declare function createApprovalLedger(workflowId: string): ApprovalLedger;
29
+ export declare function serializeWorkflowSession(session: WorkflowSession): string;
30
+ export declare function parseWorkflowSession(content: string): WorkflowSession;
31
+ export declare function stageDefinition(stage: HumanApprovalStage): ApprovalStageDefinition;
32
+ export declare function stageFingerprint(content: string | Uint8Array): string;
33
+ export declare function getUnresolvedPrerequisites(ledger: ApprovalLedger, stage: HumanApprovalStage): HumanApprovalStage[];
34
+ export declare function beginStageApproval(ledger: ApprovalLedger, stage: HumanApprovalStage, input: {
35
+ artifactHash: string;
36
+ qualityGate: QualityGateResult;
37
+ updatedAt: string;
38
+ }): ApprovalLedger;
39
+ export declare function approveStage(ledger: ApprovalLedger, stage: HumanApprovalStage, input: {
40
+ artifactHash: string;
41
+ updatedAt: string;
42
+ }): ApprovalLedger;
43
+ export declare function rejectStage(ledger: ApprovalLedger, stage: HumanApprovalStage, reason: string, updatedAt: string): ApprovalLedger;
44
+ export declare function skipOptionalStage(ledger: ApprovalLedger, stage: HumanApprovalStage, input: {
45
+ reason: string;
46
+ humanAccepted: boolean;
47
+ updatedAt: string;
48
+ }): ApprovalLedger;
49
+ export declare function invalidateStageAndDescendants(ledger: ApprovalLedger, stage: HumanApprovalStage, updatedAt: string): ApprovalLedger;
50
+ export declare function serializeApprovalLedger(ledger: ApprovalLedger): string;
51
+ export declare function parseApprovalLedger(content: string): ApprovalLedger;
@@ -0,0 +1,286 @@
1
+ import { createHash } from "node:crypto";
2
+ import { parseDocument, stringify } from "yaml";
3
+ import { HUMAN_APPROVAL_STAGES } from "./types.js";
4
+ export const APPROVAL_STAGES = HUMAN_APPROVAL_STAGES;
5
+ const STAGE_DEFINITIONS = {
6
+ brainstorming: { optional: true, requiresQualityGate: true },
7
+ specification: { optional: false, requiresQualityGate: true },
8
+ "technical-design": { optional: true, requiresQualityGate: true },
9
+ plan: { optional: false, requiresQualityGate: true },
10
+ implementation: { optional: false, requiresQualityGate: true },
11
+ verification: { optional: false, requiresQualityGate: true },
12
+ review: { optional: false, requiresQualityGate: true },
13
+ traceability: { optional: false, requiresQualityGate: true },
14
+ };
15
+ const APPROVAL_STATUSES = ["draft", "pending-human", "approved", "rejected", "skipped", "stale"];
16
+ const QUALITY_GATE_RESULTS = ["not-run", "passed", "failed", "not-applicable"];
17
+ const APPROVAL_RECORD_KEYS = new Set([
18
+ "status",
19
+ "qualityGate",
20
+ "artifactHash",
21
+ "reason",
22
+ "updatedAt",
23
+ ]);
24
+ const LEDGER_KEYS = new Set(["schema", "workflowId", "stages"]);
25
+ const SESSION_KEYS = new Set(["schema", "workflowId", "status", "startedAt", "updatedAt"]);
26
+ export function createApprovalLedger(workflowId) {
27
+ assertWorkflowId(workflowId);
28
+ return { schema: 1, workflowId, stages: {} };
29
+ }
30
+ export function serializeWorkflowSession(session) {
31
+ validateWorkflowSession(session);
32
+ return stringify(session, { lineWidth: 0 });
33
+ }
34
+ export function parseWorkflowSession(content) {
35
+ const document = parseDocument(content, { uniqueKeys: true, strict: true });
36
+ if (document.errors.length)
37
+ throw new Error("Workflow session contains invalid YAML.");
38
+ const value = document.toJS();
39
+ if (!isRecord(value) || value.schema !== 1)
40
+ throw new Error("Workflow session schema must be version 1.");
41
+ for (const key of Object.keys(value))
42
+ if (!SESSION_KEYS.has(key))
43
+ throw new Error(`Workflow session has unknown field: ${key}.`);
44
+ if (typeof value.workflowId !== "string")
45
+ throw new Error("Workflow session requires a workflowId.");
46
+ if (value.status !== "active" && value.status !== "complete")
47
+ throw new Error("Workflow session status must be active or complete.");
48
+ if (typeof value.startedAt !== "string" || typeof value.updatedAt !== "string")
49
+ throw new Error("Workflow session requires start and update timestamps.");
50
+ const session = {
51
+ schema: 1,
52
+ workflowId: value.workflowId,
53
+ status: value.status,
54
+ startedAt: value.startedAt,
55
+ updatedAt: value.updatedAt,
56
+ };
57
+ validateWorkflowSession(session);
58
+ return session;
59
+ }
60
+ export function stageDefinition(stage) {
61
+ return STAGE_DEFINITIONS[stage];
62
+ }
63
+ export function stageFingerprint(content) {
64
+ return `sha256-${createHash("sha256").update(content).digest("hex")}`;
65
+ }
66
+ export function getUnresolvedPrerequisites(ledger, stage) {
67
+ const stageIndex = APPROVAL_STAGES.indexOf(stage);
68
+ return APPROVAL_STAGES.slice(0, stageIndex).filter((prerequisite) => {
69
+ const status = ledger.stages[prerequisite]?.status;
70
+ return status !== "approved" && status !== "skipped";
71
+ });
72
+ }
73
+ export function beginStageApproval(ledger, stage, input) {
74
+ assertHash(input.artifactHash);
75
+ assertTimestamp(input.updatedAt);
76
+ assertPrerequisitesResolved(ledger, stage);
77
+ const definition = stageDefinition(stage);
78
+ if (definition.requiresQualityGate && input.qualityGate !== "passed")
79
+ throw new Error(`The ${stage} quality gate must pass before human review.`);
80
+ if (!definition.requiresQualityGate && input.qualityGate !== "not-applicable")
81
+ throw new Error(`The ${stage} stage has no automated quality gate; use not-applicable.`);
82
+ const existing = ledger.stages[stage];
83
+ if (existing?.status === "approved" && existing.artifactHash === input.artifactHash)
84
+ return ledger;
85
+ const reset = existing && (existing.artifactHash !== input.artifactHash || existing.status === "skipped")
86
+ ? invalidateStageAndDescendants(ledger, stage, input.updatedAt)
87
+ : ledger;
88
+ return withStage(reset, stage, {
89
+ status: "pending-human",
90
+ qualityGate: input.qualityGate,
91
+ artifactHash: input.artifactHash,
92
+ updatedAt: input.updatedAt,
93
+ });
94
+ }
95
+ export function approveStage(ledger, stage, input) {
96
+ assertHash(input.artifactHash);
97
+ assertTimestamp(input.updatedAt);
98
+ assertPrerequisitesResolved(ledger, stage);
99
+ const current = ledger.stages[stage];
100
+ if (!current || current.status !== "pending-human")
101
+ throw new Error(`The ${stage} stage is not pending human approval.`);
102
+ if (current.artifactHash !== input.artifactHash)
103
+ throw new Error(`The ${stage} artifact changed after review; request approval again.`);
104
+ return withStage(ledger, stage, { ...current, status: "approved", updatedAt: input.updatedAt });
105
+ }
106
+ export function rejectStage(ledger, stage, reason, updatedAt) {
107
+ assertTimestamp(updatedAt);
108
+ if (!ledger.stages[stage] || ledger.stages[stage].status !== "pending-human")
109
+ throw new Error(`The ${stage} stage is not pending human approval.`);
110
+ if (!reason.trim())
111
+ throw new Error("A reason is required when rejecting a stage.");
112
+ const invalidated = invalidateStageAndDescendants(ledger, stage, updatedAt);
113
+ return withStage(invalidated, stage, {
114
+ ...ledger.stages[stage],
115
+ status: "rejected",
116
+ reason: reason.trim(),
117
+ updatedAt,
118
+ });
119
+ }
120
+ export function skipOptionalStage(ledger, stage, input) {
121
+ assertTimestamp(input.updatedAt);
122
+ if (!stageDefinition(stage).optional)
123
+ throw new Error(`The ${stage} stage is required and cannot be skipped.`);
124
+ if (!input.humanAccepted)
125
+ throw new Error(`Explicit human acceptance is required to skip ${stage}.`);
126
+ if (!input.reason.trim())
127
+ throw new Error(`A reason is required to skip ${stage}.`);
128
+ assertPrerequisitesResolved(ledger, stage);
129
+ const invalidated = invalidateStageAndDescendants(ledger, stage, input.updatedAt);
130
+ return withStage(invalidated, stage, {
131
+ status: "skipped",
132
+ qualityGate: "not-applicable",
133
+ reason: input.reason.trim(),
134
+ updatedAt: input.updatedAt,
135
+ });
136
+ }
137
+ export function invalidateStageAndDescendants(ledger, stage, updatedAt) {
138
+ assertTimestamp(updatedAt);
139
+ const stageIndex = APPROVAL_STAGES.indexOf(stage);
140
+ const stages = { ...ledger.stages };
141
+ for (const affected of APPROVAL_STAGES.slice(stageIndex)) {
142
+ const record = stages[affected];
143
+ if (record && record.status !== "draft")
144
+ stages[affected] = { ...record, status: "stale", updatedAt };
145
+ }
146
+ return { schema: 1, workflowId: ledger.workflowId, stages };
147
+ }
148
+ export function serializeApprovalLedger(ledger) {
149
+ validateLedger(ledger);
150
+ const stages = Object.fromEntries(APPROVAL_STAGES.flatMap((stage) => ledger.stages[stage] ? [[stage, ledger.stages[stage]]] : []));
151
+ return stringify({ schema: 1, workflowId: ledger.workflowId, stages }, { lineWidth: 0 });
152
+ }
153
+ export function parseApprovalLedger(content) {
154
+ const document = parseDocument(content, { uniqueKeys: true, strict: true });
155
+ if (document.errors.length)
156
+ throw new Error("Approval ledger contains invalid YAML.");
157
+ const value = document.toJS();
158
+ if (!isRecord(value) || value.schema !== 1)
159
+ throw new Error("Approval ledger schema must be version 1.");
160
+ for (const key of Object.keys(value))
161
+ if (!LEDGER_KEYS.has(key))
162
+ throw new Error(`Approval ledger has unknown field: ${key}.`);
163
+ if (typeof value.workflowId !== "string")
164
+ throw new Error("Approval ledger requires a workflowId.");
165
+ assertWorkflowId(value.workflowId);
166
+ if (!isRecord(value.stages))
167
+ throw new Error("Approval ledger stages must be a mapping.");
168
+ const stages = {};
169
+ for (const [stageName, record] of Object.entries(value.stages)) {
170
+ if (!isApprovalStage(stageName))
171
+ throw new Error(`Approval ledger has unknown workflow stage: ${stageName}.`);
172
+ stages[stageName] = parseStageRecord(stageName, record);
173
+ }
174
+ const ledger = { schema: 1, workflowId: value.workflowId, stages };
175
+ validateLedger(ledger);
176
+ return ledger;
177
+ }
178
+ function assertPrerequisitesResolved(ledger, stage) {
179
+ const unresolved = getUnresolvedPrerequisites(ledger, stage);
180
+ if (unresolved.length)
181
+ throw new Error(`The ${stage} stage is blocked until these stages are approved or explicitly skipped: ${unresolved.join(", ")}.`);
182
+ }
183
+ function assertHash(value) {
184
+ if (!/^sha256-[a-f0-9]{64}$/.test(value))
185
+ throw new Error("Stage artifact fingerprint must be a SHA-256 hash.");
186
+ }
187
+ function assertTimestamp(value) {
188
+ if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?(?:Z|[+-]\d{2}:\d{2})$/.test(value) ||
189
+ !Number.isFinite(Date.parse(value)))
190
+ throw new Error("Approval timestamp must be a valid date-time.");
191
+ }
192
+ function assertWorkflowId(value) {
193
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(value))
194
+ throw new Error("Workflow ID must be 1–64 characters using letters, numbers, dot, underscore, or hyphen.");
195
+ }
196
+ function validateWorkflowSession(session) {
197
+ if (session.schema !== 1)
198
+ throw new Error("Workflow session schema must be version 1.");
199
+ assertWorkflowId(session.workflowId);
200
+ if (session.status !== "active" && session.status !== "complete")
201
+ throw new Error("Workflow session status must be active or complete.");
202
+ assertTimestamp(session.startedAt);
203
+ assertTimestamp(session.updatedAt);
204
+ }
205
+ function withStage(ledger, stage, record) {
206
+ return {
207
+ schema: 1,
208
+ workflowId: ledger.workflowId,
209
+ stages: { ...ledger.stages, [stage]: record },
210
+ };
211
+ }
212
+ function isApprovalStage(value) {
213
+ return APPROVAL_STAGES.includes(value);
214
+ }
215
+ function isRecord(value) {
216
+ return typeof value === "object" && value !== null && !Array.isArray(value);
217
+ }
218
+ function parseStageRecord(stage, value) {
219
+ if (!isRecord(value))
220
+ throw new Error(`Approval record for ${stage} must be a mapping.`);
221
+ for (const key of Object.keys(value))
222
+ if (!APPROVAL_RECORD_KEYS.has(key))
223
+ throw new Error(`Approval record for ${stage} has unknown field: ${key}.`);
224
+ const { status, qualityGate, artifactHash, reason, updatedAt } = value;
225
+ if (typeof status !== "string" || !APPROVAL_STATUSES.includes(status))
226
+ throw new Error(`Approval record for ${stage} has an invalid status.`);
227
+ if (typeof qualityGate !== "string" || !QUALITY_GATE_RESULTS.includes(qualityGate))
228
+ throw new Error(`Approval record for ${stage} has an invalid quality gate result.`);
229
+ if (typeof updatedAt !== "string")
230
+ throw new Error(`Approval record for ${stage} requires an updatedAt timestamp.`);
231
+ assertTimestamp(updatedAt);
232
+ if (artifactHash !== undefined) {
233
+ if (typeof artifactHash !== "string")
234
+ throw new Error(`Approval record for ${stage} has an invalid artifact fingerprint.`);
235
+ assertHash(artifactHash);
236
+ }
237
+ if (reason !== undefined && typeof reason !== "string")
238
+ throw new Error(`Approval record for ${stage} has an invalid reason.`);
239
+ const record = {
240
+ status: status,
241
+ qualityGate: qualityGate,
242
+ updatedAt,
243
+ ...(artifactHash === undefined ? {} : { artifactHash }),
244
+ ...(reason === undefined ? {} : { reason }),
245
+ };
246
+ if (record.status === "pending-human" && !record.artifactHash)
247
+ throw new Error(`Pending approval for ${stage} requires an artifact fingerprint.`);
248
+ if (record.status === "approved" && !record.artifactHash)
249
+ throw new Error(`Approved stage ${stage} requires an artifact fingerprint.`);
250
+ if ((record.status === "pending-human" || record.status === "approved") &&
251
+ stageDefinition(stage).requiresQualityGate &&
252
+ record.qualityGate !== "passed")
253
+ throw new Error(`Human-reviewed stage ${stage} must have a passing quality gate.`);
254
+ if (!stageDefinition(stage).requiresQualityGate && record.qualityGate !== "not-applicable")
255
+ throw new Error(`Stage ${stage} has no automated quality gate and must use not-applicable.`);
256
+ if (record.status === "skipped") {
257
+ if (!stageDefinition(stage).optional)
258
+ throw new Error(`Required stage ${stage} cannot be skipped.`);
259
+ if (!record.reason?.trim())
260
+ throw new Error(`Skipped stage ${stage} requires a reason.`);
261
+ }
262
+ if (record.status === "rejected" && !record.reason?.trim())
263
+ throw new Error(`Rejected stage ${stage} requires a reason.`);
264
+ return record;
265
+ }
266
+ function validateLedger(ledger) {
267
+ if (ledger.schema !== 1 || !isRecord(ledger.stages))
268
+ throw new Error("Approval ledger schema must be version 1 with a stages mapping.");
269
+ assertWorkflowId(ledger.workflowId);
270
+ for (const [stage, record] of Object.entries(ledger.stages)) {
271
+ if (!isApprovalStage(stage))
272
+ throw new Error(`Approval ledger has unknown workflow stage: ${stage}.`);
273
+ parseStageRecord(stage, record);
274
+ }
275
+ for (const stage of APPROVAL_STAGES) {
276
+ const status = ledger.stages[stage]?.status;
277
+ if (status === "pending-human" ||
278
+ status === "approved" ||
279
+ status === "rejected" ||
280
+ status === "skipped") {
281
+ const unresolved = getUnresolvedPrerequisites(ledger, stage);
282
+ if (unresolved.length)
283
+ throw new Error(`Approval ledger has out-of-order decision for ${stage}; unresolved stages: ${unresolved.join(", ")}.`);
284
+ }
285
+ }
286
+ }
@@ -1,2 +1,2 @@
1
- export type QualityGateStage = "brainstorming" | "specification" | "plan" | "verification";
1
+ export type QualityGateStage = "brainstorming" | "specification" | "technical-design" | "plan" | "implementation" | "verification" | "review" | "traceability";
2
2
  export declare function evaluateQualityGate(stage: QualityGateStage, content: string): string[];
@@ -1,3 +1,4 @@
1
+ import { parseDocument } from "yaml";
1
2
  function section(content, heading) {
2
3
  const match = content.match(new RegExp(`^## ${heading}\\s*$([\\s\\S]*?)(?=^## |(?![\\s\\S]))`, "im"));
3
4
  return match?.[1].trim();
@@ -29,6 +30,15 @@ export function evaluateQualityGate(stage, content) {
29
30
  issues.push("Requirement acceptance criteria must use Given, When, and Then.");
30
31
  return issues;
31
32
  }
33
+ if (stage === "technical-design")
34
+ return requireSections(content, "Technical Design", [
35
+ "Context",
36
+ "Proposed Design",
37
+ "Alternatives",
38
+ "Interfaces",
39
+ "Risks",
40
+ "Validation",
41
+ ]);
32
42
  if (stage === "plan") {
33
43
  const fields = ["Requirement", "Code", "Tests", "Validation", "Evidence"];
34
44
  const issues = !/TASK-\d+/.test(content)
@@ -39,11 +49,53 @@ export function evaluateQualityGate(stage, content) {
39
49
  issues.push(`Implementation plan task must define ${field}.`);
40
50
  return issues;
41
51
  }
42
- return requireSections(content, "Verification Report", [
43
- "Requirements Checked",
44
- "Checks Passed",
45
- "Missing Evidence",
46
- "Residual Risks",
47
- "Decision",
48
- ]);
52
+ if (stage === "implementation")
53
+ return requireSections(content, "Implementation Evidence", [
54
+ "Changed Files",
55
+ "Tests",
56
+ "Validation",
57
+ "Deviations",
58
+ ]);
59
+ if (stage === "verification")
60
+ return requireSections(content, "Verification Report", [
61
+ "Requirements Checked",
62
+ "Checks Passed",
63
+ "Missing Evidence",
64
+ "Residual Risks",
65
+ "Decision",
66
+ ]);
67
+ if (stage === "review")
68
+ return requireSections(content, "Code Review", [
69
+ "Scope",
70
+ "Findings",
71
+ "Checks",
72
+ "Residual Risks",
73
+ "Decision",
74
+ ]);
75
+ const document = parseDocument(content, { uniqueKeys: true, strict: true });
76
+ if (document.errors.length)
77
+ return ["Traceability evidence must be valid YAML."];
78
+ const value = document.toJS();
79
+ if (!isRecord(value) || value.schema !== 1 || !Array.isArray(value.links))
80
+ return ["Traceability evidence must define schema: 1 and a links list."];
81
+ if (value.links.length === 0)
82
+ return ["Traceability evidence must link at least one requirement."];
83
+ const fields = ["requirement", "tasks", "code", "tests", "evidence"];
84
+ return value.links.flatMap((link, index) => {
85
+ if (!isRecord(link))
86
+ return [`Traceability link ${index + 1} must be a mapping.`];
87
+ return fields.flatMap((field) => {
88
+ const fieldValue = link[field];
89
+ return field === "requirement"
90
+ ? typeof fieldValue === "string" && /^REQ-\d+$/.test(fieldValue)
91
+ ? []
92
+ : [`Traceability link ${index + 1} requires a valid requirement ID.`]
93
+ : Array.isArray(fieldValue) && fieldValue.length > 0
94
+ ? []
95
+ : [`Traceability link ${index + 1} requires non-empty ${field} evidence.`];
96
+ });
97
+ });
98
+ }
99
+ function isRecord(value) {
100
+ return typeof value === "object" && value !== null && !Array.isArray(value);
49
101
  }