@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/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,29 @@ 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
|
+
|
|
20
|
+
## [0.1.5] - 2026-09-12
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- Connect SDD capability recommendations through explicit prerequisites so custom selections include required earlier workflow stages.
|
|
25
|
+
- Recommend verification for supported project profiles and test runners, including test commands discovered in monorepo workspaces.
|
|
26
|
+
- Align brainstorming, specification, planning, and verification gates with their matching artifacts; remove references to a nonexistent review gate command.
|
|
27
|
+
- Clarify skill handoffs across brainstorming, requirements, design, planning, implementation, verification, and code review.
|
|
28
|
+
- Correct the project profile path referenced by the project initialization skill.
|
|
29
|
+
|
|
7
30
|
## [0.1.4] - 2026-09-12
|
|
8
31
|
|
|
9
32
|
### Added
|
|
@@ -66,7 +89,8 @@ All notable changes to this project are documented in this file. The format foll
|
|
|
66
89
|
|
|
67
90
|
- Path confinement, symlink protection, explicit permission gates, secret redaction, strict parsers, atomic migrations, and immutable execution checkpoints.
|
|
68
91
|
|
|
69
|
-
[Unreleased]: https://github.com/MultiLeaf/aiw/compare/v0.1.
|
|
92
|
+
[Unreleased]: https://github.com/MultiLeaf/aiw/compare/v0.1.5...HEAD
|
|
93
|
+
[0.1.5]: https://github.com/MultiLeaf/aiw/compare/v0.1.4...v0.1.5
|
|
70
94
|
[0.1.4]: https://github.com/MultiLeaf/aiw/compare/v0.1.3...v0.1.4
|
|
71
95
|
[0.1.3]: https://github.com/MultiLeaf/aiw/compare/v0.1.2...v0.1.3
|
|
72
96
|
[0.1.2]: https://github.com/MultiLeaf/aiw/compare/v0.1.1...v0.1.2
|
package/README.md
CHANGED
|
@@ -122,49 +122,67 @@ flowchart TD
|
|
|
122
122
|
D --> E[(.aiw/recommendations.yml)]
|
|
123
123
|
E --> F{User confirms scope}
|
|
124
124
|
F -->|Revise| B
|
|
125
|
-
F -->|Proceed| G
|
|
126
|
-
|
|
127
|
-
G
|
|
128
|
-
H --> I
|
|
129
|
-
I
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
M
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
125
|
+
F -->|Proceed| G{Clarification needed?}
|
|
126
|
+
|
|
127
|
+
G -->|Yes| H[Brainstorm]
|
|
128
|
+
H --> I[(generated/specs/brainstorm.md)]
|
|
129
|
+
I --> J{Brainstorm quality gate}
|
|
130
|
+
J -->|Incomplete| H
|
|
131
|
+
J -->|Complete| JH{Human approves brainstorm?}
|
|
132
|
+
JH -->|Revise| H
|
|
133
|
+
JH -->|Approve| K[Write specification]
|
|
134
|
+
G -->|No| K
|
|
135
|
+
|
|
136
|
+
K --> L[(generated/specs/specification.md)]
|
|
137
|
+
L --> M{Specification quality gate}
|
|
138
|
+
M -->|Incomplete| K
|
|
139
|
+
M -->|Complete| MH{Human approves requirements?}
|
|
140
|
+
MH -->|Revise| K
|
|
141
|
+
MH -->|Approve| N{Architecture decision needed?}
|
|
142
|
+
|
|
143
|
+
N -->|Yes| O[Create ADR]
|
|
144
|
+
O --> P[(.context/adrs/ADR-NNN-title.md)]
|
|
145
|
+
P --> Q[Update ADR index]
|
|
146
|
+
Q --> R[(.context/adrs/INDEX.md)]
|
|
147
|
+
R --> RH{Human approves design?}
|
|
148
|
+
RH -->|Revise| O
|
|
149
|
+
RH -->|Approve| S[Create implementation plan]
|
|
150
|
+
N -->|No| S
|
|
151
|
+
|
|
152
|
+
S --> T[(generated/plans/implementation-plan.md)]
|
|
153
|
+
T --> U{Plan quality gate}
|
|
154
|
+
U -->|Incomplete| S
|
|
155
|
+
U -->|Ready| UH{Human approves plan?}
|
|
156
|
+
UH -->|Revise| S
|
|
157
|
+
UH -->|Approve| V[Implement tasks with TDD]
|
|
158
|
+
|
|
159
|
+
V --> W[Write code and tests]
|
|
160
|
+
W --> X[Run task validation commands]
|
|
161
|
+
X --> Y{Validation passes?}
|
|
162
|
+
Y -->|No| V
|
|
163
|
+
Y -->|Yes| Z[Record completion evidence]
|
|
164
|
+
|
|
165
|
+
Z --> AA[(generated/artifacts and checkpoint evidence)]
|
|
166
|
+
AA --> AAH{Human accepts implementation?}
|
|
167
|
+
AAH -->|Revise| V
|
|
168
|
+
AAH -->|Approve| AB[Write verification report]
|
|
169
|
+
AB --> AC{Verification quality gate}
|
|
170
|
+
AC -->|Missing evidence| V
|
|
171
|
+
AC -->|Complete| ACH{Human approves verification?}
|
|
172
|
+
ACH -->|Revise| V
|
|
173
|
+
ACH -->|Approve| AG[Run code-review skill when installed]
|
|
174
|
+
AG --> AH{Review findings?}
|
|
175
|
+
AH -->|Changes required| V
|
|
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]
|
|
184
|
+
AI --> AJ[(.aiw/checkpoints/self-validation-TICKET.yml)]
|
|
185
|
+
AJ --> AK[Commit and independent integration validation]
|
|
168
186
|
|
|
169
187
|
B -. command history and evidence .-> LOGS[(.aiw/checkpoints/)]
|
|
170
188
|
D -. command history and evidence .-> LOGS
|
|
@@ -180,21 +198,39 @@ npx @multileaf/ai-workflow scan
|
|
|
180
198
|
npx @multileaf/ai-workflow recommend
|
|
181
199
|
|
|
182
200
|
# In CI, provide comma-separated recommendation IDs detected for that project.
|
|
183
|
-
npx @multileaf/ai-workflow recommend --select=typescript-quality,
|
|
201
|
+
npx @multileaf/ai-workflow recommend --select=typescript-quality,verification
|
|
184
202
|
|
|
185
|
-
#
|
|
203
|
+
# Track a feature from its first artifact through final human approval.
|
|
204
|
+
npx @multileaf/ai-workflow workflow start
|
|
186
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
|
|
187
209
|
npx @multileaf/ai-workflow spec --title="Feature requirements"
|
|
210
|
+
npx @multileaf/ai-workflow gate specification
|
|
211
|
+
npx @multileaf/ai-workflow approve specification
|
|
188
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
|
|
189
215
|
npx @multileaf/ai-workflow plan --title="Implementation plan"
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
216
|
+
npx @multileaf/ai-workflow gate plan
|
|
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.
|
|
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
|
|
193
226
|
npx @multileaf/ai-workflow verify
|
|
194
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
|
|
195
231
|
```
|
|
196
232
|
|
|
197
|
-
Scaffold commands write deterministic files at fixed paths and a repeated invocation replaces that scaffold.
|
|
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.
|
|
198
234
|
|
|
199
235
|
## Project intelligence
|
|
200
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
|
-
|
|
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.
|
|
48
|
-
6.
|
|
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 →
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
-
|
|
59
|
-
|
|
60
|
-
-
|
|
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
|
|
package/dist/generator.js
CHANGED
|
@@ -2,18 +2,28 @@ import { resolveGeneratedPath } from "./drift.js";
|
|
|
2
2
|
export function generateProjectResources(profile, selected, write) {
|
|
3
3
|
const linterCommand = safeInlineValue(profile.quality.linter?.command);
|
|
4
4
|
const formatterCommand = safeInlineValue(profile.quality.formatter?.command);
|
|
5
|
-
const
|
|
5
|
+
const testCommands = [
|
|
6
|
+
...(profile.testing?.command ? [{ scope: "project", command: profile.testing.command }] : []),
|
|
7
|
+
...(profile.modules ?? []).flatMap((module) => module.testing?.command
|
|
8
|
+
? [{ scope: `workspace ${module.path}`, command: module.testing.command }]
|
|
9
|
+
: []),
|
|
10
|
+
].map(({ scope, command }) => ({ scope, command: safeInlineValue(command) }));
|
|
11
|
+
const uniqueTestCommands = [
|
|
12
|
+
...new Map(testCommands
|
|
13
|
+
.filter(({ command }) => command)
|
|
14
|
+
.map(({ scope, command }) => [`${scope}:${command}`, { scope, command }])).values(),
|
|
15
|
+
];
|
|
6
16
|
if (selected.includes("typescript-quality") || selected.includes("rules/project-quality"))
|
|
7
17
|
write(resolveGeneratedPath("rules/project-quality.md"), `# Project Quality Rules\n\n- Use ${profile.runtime.languages.map((language) => (language === "typescript" ? "TypeScript" : language)).join(", ")} with strict typing.\n${linterCommand ? `- Run \`${linterCommand}\` before completion.\n` : ""}${formatterCommand ? `- Run \`${formatterCommand}\` before review.\n` : ""}- Keep all generated AI Workflow artifacts in English.\n`);
|
|
8
18
|
if (selected.includes("tdd-development") || selected.includes("rules/tdd-project-policy"))
|
|
9
|
-
write(resolveGeneratedPath("rules/tdd-project-policy.md"), `# TDD Project Policy\n\n${
|
|
19
|
+
write(resolveGeneratedPath("rules/tdd-project-policy.md"), `# TDD Project Policy\n\n${uniqueTestCommands.map(({ scope, command }) => `- Run the ${scope} test command \`${command}\` for verification.`).join("\n")}${uniqueTestCommands.length ? "\n" : ""}- Write a failing behavioral test before implementation.\n- Refactor only after the test is green.\n`);
|
|
10
20
|
if (selected.includes("nextjs-development") ||
|
|
11
21
|
selected.includes("react-best-practices") ||
|
|
12
22
|
selected.includes("rules/architecture-policy"))
|
|
13
23
|
write(resolveGeneratedPath("agents/project-context.md"), `# Project Context\n\nThis project uses ${profile.frameworks.map((framework) => (framework === "nextjs" ? "Next.js" : framework)).join(", ")}. Follow the detected project structure and existing framework conventions.\n`);
|
|
14
24
|
if (selected.length > 0) {
|
|
15
25
|
write(resolveGeneratedPath("context/project-profile.md"), `# Confirmed Project Profile\n\n- Languages: ${profile.runtime.languages.join(", ") || "none detected"}\n- Frameworks: ${profile.frameworks.join(", ") || "none detected"}\n- Package manager: ${profile.packageManager}\n- All generated content is in English.\n`);
|
|
16
|
-
write(resolveGeneratedPath("hooks/quality-check.md"), `# Quality Check Hook\n\nRun the available project quality commands before completion.${linterCommand ? `\n\n- Linter: \`${linterCommand}\`` : ""}${formatterCommand ? `\n- Formatter: \`${formatterCommand}\`` : ""}${
|
|
26
|
+
write(resolveGeneratedPath("hooks/quality-check.md"), `# Quality Check Hook\n\nRun the available project quality commands before completion.${linterCommand ? `\n\n- Linter: \`${linterCommand}\`` : ""}${formatterCommand ? `\n- Formatter: \`${formatterCommand}\`` : ""}${uniqueTestCommands.map(({ scope, command }) => `\n- ${scope} tests: \`${command}\``).join("")}\n`);
|
|
17
27
|
}
|
|
18
28
|
}
|
|
19
29
|
function safeInlineValue(value) {
|
|
@@ -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
|
+
}
|