@multileaf/ai-workflow 0.9.0 → 0.10.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.
Files changed (38) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +12 -1
  3. package/dist/blocking-reasons.d.ts +67 -0
  4. package/dist/blocking-reasons.js +181 -0
  5. package/dist/command-evidence.d.ts +32 -0
  6. package/dist/command-evidence.js +61 -0
  7. package/dist/human-approval.d.ts +16 -1
  8. package/dist/human-approval.js +69 -5
  9. package/dist/recommendations.js +16 -1
  10. package/dist/risk-assess.d.ts +50 -0
  11. package/dist/risk-assess.js +273 -0
  12. package/dist/workflow.js +233 -24
  13. package/docs/README.md +2 -0
  14. package/docs/architecture.md +50 -0
  15. package/docs/backlog.md +13 -0
  16. package/docs/continuation-inventory.md +21 -0
  17. package/docs/decision-model-evaluation.md +4 -0
  18. package/docs/opportunities/OPP-001-proportional-ceremony-light-lane.md +62 -0
  19. package/docs/opportunities/OPP-002-risk-assess-command.md +70 -0
  20. package/docs/opportunities/OPP-003-adversarial-review-package.md +63 -0
  21. package/docs/opportunities/OPP-004-exact-continuation-commands.md +59 -0
  22. package/docs/opportunities/OPP-005-friction-benchmark.md +62 -0
  23. package/docs/opportunities/OPP-006-emission-usability-test-policy.md +57 -0
  24. package/docs/opportunities/OPP-007-decision-findings-triage-scope-watch.md +54 -0
  25. package/docs/opportunities/OPP-008-embedding-covering-tests-judge-context.md +52 -0
  26. package/docs/opportunities/README.md +33 -0
  27. package/docs/product-requirements.md +3 -0
  28. package/docs/product-vision.md +1 -0
  29. package/docs/quality.md +10 -0
  30. package/docs/release-process.md +9 -1
  31. package/docs/semantic-mapping.md +4 -0
  32. package/package.json +1 -1
  33. package/resources/package.yaml +3 -0
  34. package/resources/skills/code-review.md +2 -0
  35. package/resources/skills/review-judge-a.md +24 -0
  36. package/resources/skills/review-judge-b.md +24 -0
  37. package/resources/skills/review-refuter.md +28 -0
  38. package/resources/skills/ticket.md +2 -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.10.0] - 2026-10-30
8
+
9
+ ### Added
10
+
11
+ - Typed blocking reasons with exact continuations (OPP-004): every workflow, gate, and approval blocking exit emits a stable reason code plus either an exact runnable `aiw` continuation or an explicit terminal statement. With `--json`, blocks return machine-readable `reason_code` and `next_command`/`next_action`/`terminal` fields. `docs/continuation-inventory.md` maps every code and is kept in parity with the registry by a CI test.
12
+ - `aiw risk assess` (OPP-002): read-only, content-based risk classification for working-tree or `--base-ref` changes. Six high-risk categories (data/irreversible, security, consumed contracts, concurrency, delivery/environment, untested regression) classify by what changed, never by size: documentation-only diffs are `passive`; size never decides. Deterministic and offline by default; `--escalate-item 1-6 --escalate-reason` raises and never lowers a tier; `risk_due: yes` names an exact runnable next command (`aiw.risk-assessment/v1` JSON envelope).
13
+ - Light lane (OPP-001): a human-chosen `aiw task start --lane=light --lane-reason=...` for small, understood work — the resume test decides (resumable from the request plus `git diff`). No durable spec/plan artifacts; validation evidence is recorded via `aiw task evidence` and completion requires at least one entry; the lane and rationale are written into the session and approval ledger for audit. Stage commands block with `lane_escalation_required`; `aiw task escalate --reason` moves the workflow to the full lane (one-way, evidence preserved). The full lane remains the default and unchanged.
14
+ - Emission ≠ usability policy and command ratchet (OPP-006): messages naming an `aiw` command must have a test that executes it. A ratchet test executes every command named by the generated skills and READMEs and fails on unknown commands.
15
+ - Friction benchmark (OPP-005): `scripts/friction-bench.mjs` drives packaged-CLI journeys and classifies every non-success exit as in-band / out-of-band / dead-end, executing printed continuations to verify they resolve. CI enforces a zero-dead-end budget; the release gate re-verifies budget and determinism. Baseline: in-band 5, out-of-band 5, dead-end 0.
16
+ - Adversarial review skills (OPP-003): `review-judge-a`, `review-judge-b`, and `review-refuter` ship in the self-hosting package. Two blind judges review independently; severe findings require a reproducing command or self-proving `file:line` citations and otherwise degrade to advisory; only corroborated severe findings block. Refuter command execution is permission-gated (default: evidence reading with an explicit no-command-executed marker, isolated directory, no network). Human acceptance of the review outcome is unchanged.
17
+ - Workflow proportionality documentation: [ADR-001](.context/adrs/ADR-001-odd-rdd-proportionality-scope.md) records the ODD/RDD analysis — six implemented opportunities plus advisory-only model-integration opportunities (OPP-007/008) deliberately deferred until field exposure.
18
+
19
+ ### Fixed
20
+
21
+ - `aiw risk assess` no longer truncates paths of tracked-modified files (the porcelain fixed-width column was destroyed by line trimming) and no longer flags `.aiw/` managed state (for example `lock.yml`) as concurrency risk.
22
+ - `aiw risk` without a subcommand now prints its own typed usage instead of the general command dump.
23
+
24
+ ## [0.9.1] - 2026-10-24
25
+
26
+ ### Fixed
27
+
28
+ - Workflow capabilities are always recommended: the seven SDD capabilities (brainstorming, ticket-driven-development, requirements-specification, implementation-planning, technical-design, code-review, verification) are marked universal and are recommended for any project, including scans with no detected signals. Previously a project without languages, frameworks, package manager, tests, linter, or CI received zero recommendations; stack-dependent capabilities still require their evidence.
29
+
7
30
  ## [0.9.0] - 2026-10-21
8
31
 
9
32
  ## [0.8.0] - 2026-10-20
package/README.md CHANGED
@@ -14,6 +14,7 @@ AI-assisted development often relies on implicit prompts, duplicated configurati
14
14
  - skills, rules, agents, hooks, and templates are versioned resources;
15
15
  - target-specific files are generated through adapters;
16
16
  - quality gates and checkpoints provide completion evidence;
17
+ - ceremony scales with the consequence of the change — risk-based lanes instead of uniform ceremony;
17
18
  - context is filtered and loaded only when needed to reduce token usage;
18
19
  - package-declared sensitive capabilities are evaluated by policy boundaries.
19
20
 
@@ -114,7 +115,7 @@ flowchart TD
114
115
 
115
116
  ## The SDD workflow
116
117
 
117
- AI Workflow supports an end-to-end specification-driven development cycle:
118
+ AI Workflow supports an end-to-end specification-driven development cycle. Ceremony is proportional: `aiw risk assess` classifies each change by content, and small, understood work may run in a human-chosen light lane (evidence entries instead of durable artifacts) while substantial work runs the staged sequence below. The current end-to-end flow, both lanes, and the CI enforcement layer are diagrammed in [Architecture — Current flow](docs/architecture.md#current-flow).
118
119
 
119
120
  ```mermaid
120
121
  flowchart TD
@@ -205,6 +206,16 @@ npx @multileaf/ai-workflow recommend --select=typescript-quality,verification
205
206
  # Track a feature from its first artifact through final human approval.
206
207
  npx @multileaf/ai-workflow workflow start
207
208
 
209
+ # Small, understood change? The light lane skips durable spec/plan artifacts
210
+ # (human-chosen via the resume test) and records validation evidence instead.
211
+ npx @multileaf/ai-workflow task start --ticket=TEAM-7 --lane=light --lane-reason="small fix, resumable from request"
212
+ npx @multileaf/ai-workflow task evidence --validation="npm test: passed"
213
+ npx @multileaf/ai-workflow workflow complete
214
+ # If scope grows: aiw task escalate --reason=...
215
+
216
+ # Classify the risk of current changes (read-only; content, not size).
217
+ npx @multileaf/ai-workflow risk assess [--base-ref=<ref>] [--escalate-item=1-6 --escalate-reason=text] [--json]
218
+
208
219
  # Or start directly from a ticket / already-clear requirement (no brainstorm needed).
209
220
  npx @multileaf/ai-workflow task start --ticket=PROJ-142 --title="Add export endpoint"
210
221
  npx @multileaf/ai-workflow task status
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Typed reason codes and exact continuations for every blocking CLI exit.
3
+ *
4
+ * Contract (OPP-004, REQ-OPP-004): a blocking output must carry a stable
5
+ * reason code plus either an exact runnable continuation or an explicit
6
+ * terminal statement. A message may name a command only when running that
7
+ * command resolves the block it was printed for. See ADR-001 and
8
+ * docs/opportunities/OPP-004-exact-continuation-commands.md.
9
+ */
10
+ export declare const BLOCKING_REASON_CODES: readonly ["gate_evidence_missing", "gate_incomplete", "stage_prerequisite_pending", "stage_prerequisite_unresolved", "stage_awaiting_approval", "stage_already_approved", "approval_evidence_missing", "approval_stale", "approval_gate_missing", "lane_escalation_required", "light_evidence_missing"];
11
+ export type BlockingReasonCode = (typeof BLOCKING_REASON_CODES)[number];
12
+ /** How the user or agent gets past a block. Exactly one of three kinds. */
13
+ export type BlockingContinuation =
14
+ /** An exact runnable `aiw` command with every argument resolved. */
15
+ {
16
+ kind: "command";
17
+ command: string;
18
+ }
19
+ /** A concrete non-command step (for example, authoring an artifact). */
20
+ | {
21
+ kind: "action";
22
+ description: string;
23
+ }
24
+ /** No in-line continuation exists; a human decision is required. */
25
+ | {
26
+ kind: "terminal";
27
+ humanDecision: string;
28
+ };
29
+ /** Resolved context used by continuation producers. */
30
+ export type BlockingContext = {
31
+ stage?: string;
32
+ pendingStages?: string[];
33
+ unresolvedStages?: string[];
34
+ artifactPath?: string;
35
+ gateIssues?: string[];
36
+ /** Context-specific human message preferred over the generic definition text. */
37
+ detail?: string;
38
+ };
39
+ export type BlockingReasonDefinition = {
40
+ code: BlockingReasonCode;
41
+ description: string;
42
+ continuation: (context: BlockingContext) => BlockingContinuation;
43
+ };
44
+ export declare const BLOCKING_REASON_INVENTORY: Readonly<Record<BlockingReasonCode, BlockingReasonDefinition>>;
45
+ export declare function getBlockingReasonDefinition(code: string): BlockingReasonDefinition;
46
+ export type BlockingResult = {
47
+ code: BlockingReasonCode;
48
+ /** Explanation of the block without continuation instructions. */
49
+ message: string;
50
+ continuation: BlockingContinuation;
51
+ };
52
+ export declare function describeBlockingReason(code: BlockingReasonCode, context: BlockingContext): BlockingResult;
53
+ /** Error thrown by blocking CLI paths; carries the typed result for `--json` output. */
54
+ export declare class BlockingError extends Error {
55
+ readonly result: BlockingResult;
56
+ constructor(result: BlockingResult);
57
+ }
58
+ export declare function formatBlockingOutput(result: BlockingResult): string;
59
+ export type BlockingJson = {
60
+ reason_code: BlockingReasonCode;
61
+ next_command?: string;
62
+ next_action?: string;
63
+ terminal?: boolean;
64
+ };
65
+ export declare function blockingResultToJson(result: BlockingResult): BlockingJson;
66
+ /** Markdown continuation inventory for docs generation and CI parity checks. */
67
+ export declare function exportContinuationInventoryMarkdown(): string;
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Typed reason codes and exact continuations for every blocking CLI exit.
3
+ *
4
+ * Contract (OPP-004, REQ-OPP-004): a blocking output must carry a stable
5
+ * reason code plus either an exact runnable continuation or an explicit
6
+ * terminal statement. A message may name a command only when running that
7
+ * command resolves the block it was printed for. See ADR-001 and
8
+ * docs/opportunities/OPP-004-exact-continuation-commands.md.
9
+ */
10
+ export const BLOCKING_REASON_CODES = [
11
+ "gate_evidence_missing",
12
+ "gate_incomplete",
13
+ "stage_prerequisite_pending",
14
+ "stage_prerequisite_unresolved",
15
+ "stage_awaiting_approval",
16
+ "stage_already_approved",
17
+ "approval_evidence_missing",
18
+ "approval_stale",
19
+ "approval_gate_missing",
20
+ "lane_escalation_required",
21
+ "light_evidence_missing",
22
+ ];
23
+ function command(commandText) {
24
+ return { kind: "command", command: commandText };
25
+ }
26
+ function action(description) {
27
+ return { kind: "action", description };
28
+ }
29
+ function terminal(humanDecision) {
30
+ return { kind: "terminal", humanDecision };
31
+ }
32
+ function firstStage(stages) {
33
+ return stages && stages.length > 0 ? stages[0] : undefined;
34
+ }
35
+ export const BLOCKING_REASON_INVENTORY = {
36
+ gate_evidence_missing: {
37
+ code: "gate_evidence_missing",
38
+ description: "The quality gate is blocked because the required evidence artifact is missing.",
39
+ continuation: (context) => context.stage && context.artifactPath
40
+ ? action(`Create the required artifact at ${context.artifactPath}, then run \`aiw gate ${context.stage}\`.`)
41
+ : terminal("Decide which stage artifact to author before gating."),
42
+ },
43
+ gate_incomplete: {
44
+ code: "gate_incomplete",
45
+ description: "The quality gate found completeness issues in the stage artifact.",
46
+ continuation: (context) => context.stage
47
+ ? action(`Fix the gate findings in the stage artifact${context.artifactPath ? ` (${context.artifactPath})` : ""}, then run \`aiw gate ${context.stage}\` again.`)
48
+ : terminal("Fix the gate findings, then rerun the stage gate."),
49
+ },
50
+ stage_prerequisite_pending: {
51
+ code: "stage_prerequisite_pending",
52
+ description: "A previous stage is awaiting human approval before this stage can start.",
53
+ continuation: (context) => {
54
+ const pending = firstStage(context.pendingStages);
55
+ return pending
56
+ ? command(`aiw approve ${pending}`)
57
+ : terminal("Review the pending previous stage with a human.");
58
+ },
59
+ },
60
+ stage_prerequisite_unresolved: {
61
+ code: "stage_prerequisite_unresolved",
62
+ description: "Previous stages need human approval or an accepted optional skip before this stage can start.",
63
+ continuation: (context) => {
64
+ const unresolved = firstStage(context.unresolvedStages);
65
+ return unresolved
66
+ ? command(`aiw approve ${unresolved}`)
67
+ : terminal("Resolve the previous stages with a human before continuing.");
68
+ },
69
+ },
70
+ stage_awaiting_approval: {
71
+ code: "stage_awaiting_approval",
72
+ description: "This stage is awaiting human approval; approve or reject it before continuing.",
73
+ continuation: (context) => context.stage
74
+ ? command(`aiw approve ${context.stage}`)
75
+ : terminal("A human must approve or reject the pending stage."),
76
+ },
77
+ stage_already_approved: {
78
+ code: "stage_already_approved",
79
+ description: "This stage is already approved; changed evidence requires rerunning its quality gate.",
80
+ continuation: (context) => context.stage
81
+ ? command(`aiw gate ${context.stage}`)
82
+ : terminal("A human must decide whether to rerun the gate for the changed evidence."),
83
+ },
84
+ approval_evidence_missing: {
85
+ code: "approval_evidence_missing",
86
+ description: "Approval cannot be recorded because the stage evidence artifact is missing.",
87
+ continuation: (context) => context.stage && context.artifactPath
88
+ ? action(`Create the required artifact at ${context.artifactPath}, then run \`aiw approve ${context.stage}\`.`)
89
+ : terminal("Decide which stage artifact to author before approving."),
90
+ },
91
+ approval_stale: {
92
+ code: "approval_stale",
93
+ description: "The artifact changed after review; its approval is stale and the gate must run again.",
94
+ continuation: (context) => context.stage
95
+ ? command(`aiw gate ${context.stage}`)
96
+ : terminal("A human must decide whether to rerun the gate for the changed artifact."),
97
+ },
98
+ approval_gate_missing: {
99
+ code: "approval_gate_missing",
100
+ description: "No passing quality gate is recorded for this stage; gate it before approving.",
101
+ continuation: (context) => context.stage
102
+ ? command(`aiw gate ${context.stage}`)
103
+ : terminal("A human must decide whether to gate the stage before approving."),
104
+ },
105
+ lane_escalation_required: {
106
+ code: "lane_escalation_required",
107
+ description: "Light-lane workflows do not author stage artifacts; escalate to the full lane to continue.",
108
+ continuation: () => action("Run `aiw task escalate --reason=<why scope grew>` to move this workflow to the full lane; recorded light-lane evidence is preserved."),
109
+ },
110
+ light_evidence_missing: {
111
+ code: "light_evidence_missing",
112
+ description: "A light-lane workflow cannot complete without at least one recorded validation entry.",
113
+ continuation: () => action("Run `aiw task evidence --validation=<command and result>` at least once, then `aiw workflow complete`."),
114
+ },
115
+ };
116
+ export function getBlockingReasonDefinition(code) {
117
+ const definition = BLOCKING_REASON_INVENTORY[code];
118
+ if (!definition)
119
+ throw new Error(`Unknown blocking reason code: ${code}`);
120
+ return definition;
121
+ }
122
+ export function describeBlockingReason(code, context) {
123
+ const definition = getBlockingReasonDefinition(code);
124
+ return {
125
+ code,
126
+ message: context.detail ?? definition.description,
127
+ continuation: definition.continuation(context),
128
+ };
129
+ }
130
+ /** Error thrown by blocking CLI paths; carries the typed result for `--json` output. */
131
+ export class BlockingError extends Error {
132
+ result;
133
+ constructor(result) {
134
+ super(formatBlockingOutput(result));
135
+ this.name = "BlockingError";
136
+ this.result = result;
137
+ }
138
+ }
139
+ export function formatBlockingOutput(result) {
140
+ const prefix = `${result.message} [reason: ${result.code}]`;
141
+ switch (result.continuation.kind) {
142
+ case "command":
143
+ return `${prefix} Next: run \`${result.continuation.command}\`.`;
144
+ case "action":
145
+ return `${prefix} Next: ${result.continuation.description}`;
146
+ case "terminal":
147
+ return `${prefix} Terminal: no automatic continuation. ${result.continuation.humanDecision}`;
148
+ }
149
+ }
150
+ export function blockingResultToJson(result) {
151
+ switch (result.continuation.kind) {
152
+ case "command":
153
+ return { reason_code: result.code, next_command: result.continuation.command };
154
+ case "action":
155
+ return { reason_code: result.code, next_action: result.continuation.description };
156
+ case "terminal":
157
+ return { reason_code: result.code, terminal: true };
158
+ }
159
+ }
160
+ const CONTINUATION_KIND_LABEL = {
161
+ command: "runnable command",
162
+ action: "explicit action",
163
+ terminal: "terminal (human decision)",
164
+ };
165
+ /** Markdown continuation inventory for docs generation and CI parity checks. */
166
+ export function exportContinuationInventoryMarkdown() {
167
+ const rows = BLOCKING_REASON_CODES.map((code) => {
168
+ const definition = getBlockingReasonDefinition(code);
169
+ return `| \`${code}\` | ${definition.description} | ${CONTINUATION_KIND_LABEL[commandKind(code)]} |`;
170
+ });
171
+ return ["| Reason code | Description | Continuation |", "| --- | --- | --- |", ...rows].join("\n");
172
+ }
173
+ function commandKind(code) {
174
+ const sample = describeBlockingReason(code, {
175
+ stage: "specification",
176
+ pendingStages: ["brainstorming"],
177
+ unresolvedStages: ["brainstorming"],
178
+ artifactPath: ".aiw/generated/specs/specification.md",
179
+ });
180
+ return sample.continuation.kind;
181
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Emission ≠ usability enforcement (OPP-006, REQ-OPP-006).
3
+ *
4
+ * A test that verifies a message was emitted does not verify it is usable.
5
+ * Any user-facing artifact (docs, generated skills, blocking output) that
6
+ * names an `aiw` command must be covered by a test that proves the command
7
+ * is real — the source lesson: a shipped message named a command nobody had
8
+ * ever run, for months, with green coverage.
9
+ *
10
+ * This helper extracts `aiw ...` and `npx @multileaf/ai-workflow ...`
11
+ * invocations from artifact text so tests can execute them.
12
+ */
13
+ export type CommandMention = {
14
+ /** Normalized invocation: command and subcommand words only, no flags. */
15
+ command: string;
16
+ /** Source label for diagnostics. */
17
+ source: string;
18
+ };
19
+ /** Extract normalized command mentions (command + optional subcommand, no flags). */
20
+ export declare function extractAiwCommands(text: string, source?: string): CommandMention[];
21
+ /**
22
+ * Informational mentions excluded from execution evidence. Every entry needs
23
+ * a justification; entries are exact normalized commands.
24
+ */
25
+ export declare const INFORMATIONAL_ALLOWLIST: readonly {
26
+ command: string;
27
+ justification: string;
28
+ }[];
29
+ /** First token (the command word) used for recognition checks. */
30
+ export declare function commandWord(mention: CommandMention): string;
31
+ /** Signature of the general usage dump printed for unknown commands. */
32
+ export declare const UNKNOWN_COMMAND_SIGNATURE = "aiw install [--target target] | workflow";
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Emission ≠ usability enforcement (OPP-006, REQ-OPP-006).
3
+ *
4
+ * A test that verifies a message was emitted does not verify it is usable.
5
+ * Any user-facing artifact (docs, generated skills, blocking output) that
6
+ * names an `aiw` command must be covered by a test that proves the command
7
+ * is real — the source lesson: a shipped message named a command nobody had
8
+ * ever run, for months, with green coverage.
9
+ *
10
+ * This helper extracts `aiw ...` and `npx @multileaf/ai-workflow ...`
11
+ * invocations from artifact text so tests can execute them.
12
+ */
13
+ const INVOCATION_PATTERN = /(^|`|\n)(?:aiw|npx @multileaf\/ai-workflow) ([a-z][a-z-]*)(?: ([a-z][a-z-]*))?/g;
14
+ /** English words that must never be captured as a subcommand. */
15
+ const STOP_WORDS = new Set([
16
+ "and",
17
+ "or",
18
+ "the",
19
+ "a",
20
+ "to",
21
+ "then",
22
+ "is",
23
+ "in",
24
+ "with",
25
+ "before",
26
+ "after",
27
+ "you",
28
+ "see",
29
+ "for",
30
+ "that",
31
+ "helps",
32
+ "tool",
33
+ ]);
34
+ /** Extract normalized command mentions (command + optional subcommand, no flags). */
35
+ export function extractAiwCommands(text, source = "text") {
36
+ const mentions = [];
37
+ for (const match of text.matchAll(INVOCATION_PATTERN)) {
38
+ const subcommand = match[3] && !STOP_WORDS.has(match[3]) ? match[3] : undefined;
39
+ const command = subcommand ? `${match[2]} ${subcommand}` : match[2];
40
+ if (!mentions.some((mention) => mention.command === command && mention.source === source)) {
41
+ mentions.push({ command, source });
42
+ }
43
+ }
44
+ return mentions;
45
+ }
46
+ /**
47
+ * Informational mentions excluded from execution evidence. Every entry needs
48
+ * a justification; entries are exact normalized commands.
49
+ */
50
+ export const INFORMATIONAL_ALLOWLIST = [
51
+ {
52
+ command: "aiw install",
53
+ justification: "Executed by the install e2e suite; README form includes --target variants.",
54
+ },
55
+ ];
56
+ /** First token (the command word) used for recognition checks. */
57
+ export function commandWord(mention) {
58
+ return mention.command.split(" ")[0];
59
+ }
60
+ /** Signature of the general usage dump printed for unknown commands. */
61
+ export const UNKNOWN_COMMAND_SIGNATURE = "aiw install [--target target] | workflow";
@@ -13,6 +13,9 @@ export type ApprovalLedger = {
13
13
  schema: 1;
14
14
  workflowId: string;
15
15
  stages: Partial<Record<HumanApprovalStage, StageApprovalRecord>>;
16
+ /** Light-lane provenance so audits see the rationale instead of missing approvals. */
17
+ lane?: "light";
18
+ laneReason?: string;
16
19
  };
17
20
  export type WorkflowSession = {
18
21
  schema: 1;
@@ -22,13 +25,25 @@ export type WorkflowSession = {
22
25
  updatedAt: string;
23
26
  ticket?: string;
24
27
  title?: string;
28
+ /** Light lane marker; absent means the full lane (REQ-OPP-001). */
29
+ lane?: "light";
30
+ /** Recorded rationale for using the light lane (the resume-test call). */
31
+ laneReason?: string;
32
+ /** Presence means the workflow escalated to the full lane (effective lane: full). */
33
+ laneEscalatedAt?: string;
34
+ laneEscalationReason?: string;
25
35
  };
36
+ /** The lane that currently governs stage commands: escalation moves light → full. */
37
+ export declare function effectiveLane(session: WorkflowSession): "full" | "light";
26
38
  export declare function validateTaskTicket(ticket: string): void;
27
39
  export type ApprovalStageDefinition = {
28
40
  optional: boolean;
29
41
  requiresQualityGate: boolean;
30
42
  };
31
- export declare function createApprovalLedger(workflowId: string): ApprovalLedger;
43
+ export declare function createApprovalLedger(workflowId: string, options?: {
44
+ lane?: "light";
45
+ laneReason?: string;
46
+ }): ApprovalLedger;
32
47
  export declare function serializeWorkflowSession(session: WorkflowSession): string;
33
48
  export declare function parseWorkflowSession(content: string): WorkflowSession;
34
49
  export declare function stageDefinition(stage: HumanApprovalStage): ApprovalStageDefinition;
@@ -2,6 +2,10 @@ import { createHash } from "node:crypto";
2
2
  import { parseDocument, stringify } from "yaml";
3
3
  import { HUMAN_APPROVAL_STAGES } from "./types.js";
4
4
  export const APPROVAL_STAGES = HUMAN_APPROVAL_STAGES;
5
+ /** The lane that currently governs stage commands: escalation moves light → full. */
6
+ export function effectiveLane(session) {
7
+ return session.lane === "light" && !session.laneEscalatedAt ? "light" : "full";
8
+ }
5
9
  export function validateTaskTicket(ticket) {
6
10
  if (!/^[A-Z][A-Z0-9_]*-\d+$/.test(ticket))
7
11
  throw new Error("Task ticket must match TEAM-123, for example PROJ-142.");
@@ -25,7 +29,7 @@ const APPROVAL_RECORD_KEYS = new Set([
25
29
  "reason",
26
30
  "updatedAt",
27
31
  ]);
28
- const LEDGER_KEYS = new Set(["schema", "workflowId", "stages"]);
32
+ const LEDGER_KEYS = new Set(["schema", "workflowId", "stages", "lane", "laneReason"]);
29
33
  const SESSION_KEYS = new Set([
30
34
  "schema",
31
35
  "workflowId",
@@ -34,10 +38,23 @@ const SESSION_KEYS = new Set([
34
38
  "updatedAt",
35
39
  "ticket",
36
40
  "title",
41
+ "lane",
42
+ "laneReason",
43
+ "laneEscalatedAt",
44
+ "laneEscalationReason",
37
45
  ]);
38
- export function createApprovalLedger(workflowId) {
46
+ export function createApprovalLedger(workflowId, options = {}) {
39
47
  assertWorkflowId(workflowId);
40
- return { schema: 1, workflowId, stages: {} };
48
+ if (options.lane === "light" && !options.laneReason)
49
+ throw new Error("Light-lane ledgers require a recorded lane reason.");
50
+ return {
51
+ schema: 1,
52
+ workflowId,
53
+ stages: {},
54
+ ...(options.lane === "light" && options.laneReason
55
+ ? { lane: "light", laneReason: options.laneReason }
56
+ : {}),
57
+ };
41
58
  }
42
59
  export function serializeWorkflowSession(session) {
43
60
  validateWorkflowSession(session);
@@ -65,6 +82,20 @@ export function parseWorkflowSession(content) {
65
82
  throw new Error("Workflow session title must be a string.");
66
83
  if (typeof value.ticket === "string")
67
84
  validateTaskTicket(value.ticket);
85
+ const lane = value.lane === undefined ? undefined : value.lane;
86
+ if (lane !== undefined && lane !== "light")
87
+ throw new Error("Workflow session lane must be light when present.");
88
+ const laneReason = value.laneReason;
89
+ if (laneReason !== undefined && typeof laneReason !== "string")
90
+ throw new Error("Workflow session lane reason must be a string.");
91
+ if (lane === "light" && !laneReason)
92
+ throw new Error("Light-lane sessions require a lane reason.");
93
+ const laneEscalatedAt = value.laneEscalatedAt;
94
+ if (laneEscalatedAt !== undefined && typeof laneEscalatedAt !== "string")
95
+ throw new Error("Workflow session lane escalation timestamp must be a string.");
96
+ const laneEscalationReason = value.laneEscalationReason;
97
+ if (laneEscalationReason !== undefined && typeof laneEscalationReason !== "string")
98
+ throw new Error("Workflow session lane escalation reason must be a string.");
68
99
  const session = {
69
100
  schema: 1,
70
101
  workflowId: value.workflowId,
@@ -73,6 +104,10 @@ export function parseWorkflowSession(content) {
73
104
  updatedAt: value.updatedAt,
74
105
  ...(value.ticket === undefined ? {} : { ticket: value.ticket }),
75
106
  ...(value.title === undefined ? {} : { title: value.title }),
107
+ ...(lane === undefined ? {} : { lane }),
108
+ ...(laneReason === undefined ? {} : { laneReason }),
109
+ ...(laneEscalatedAt === undefined ? {} : { laneEscalatedAt }),
110
+ ...(laneEscalationReason === undefined ? {} : { laneEscalationReason }),
76
111
  };
77
112
  validateWorkflowSession(session);
78
113
  return session;
@@ -168,7 +203,14 @@ export function invalidateStageAndDescendants(ledger, stage, updatedAt) {
168
203
  export function serializeApprovalLedger(ledger) {
169
204
  validateLedger(ledger);
170
205
  const stages = Object.fromEntries(APPROVAL_STAGES.flatMap((stage) => ledger.stages[stage] ? [[stage, ledger.stages[stage]]] : []));
171
- return stringify({ schema: 1, workflowId: ledger.workflowId, stages }, { lineWidth: 0 });
206
+ return stringify({
207
+ schema: 1,
208
+ workflowId: ledger.workflowId,
209
+ stages,
210
+ ...(ledger.lane === "light" && ledger.laneReason
211
+ ? { lane: ledger.lane, laneReason: ledger.laneReason }
212
+ : {}),
213
+ }, { lineWidth: 0 });
172
214
  }
173
215
  export function parseApprovalLedger(content) {
174
216
  const document = parseDocument(content, { uniqueKeys: true, strict: true });
@@ -191,7 +233,14 @@ export function parseApprovalLedger(content) {
191
233
  throw new Error(`Approval ledger has unknown workflow stage: ${stageName}.`);
192
234
  stages[stageName] = parseStageRecord(stageName, record);
193
235
  }
194
- const ledger = { schema: 1, workflowId: value.workflowId, stages };
236
+ const ledger = {
237
+ schema: 1,
238
+ workflowId: value.workflowId,
239
+ stages,
240
+ ...(value.lane === "light" && typeof value.laneReason === "string"
241
+ ? { lane: "light", laneReason: value.laneReason }
242
+ : {}),
243
+ };
195
244
  validateLedger(ledger);
196
245
  return ledger;
197
246
  }
@@ -221,6 +270,17 @@ function validateWorkflowSession(session) {
221
270
  throw new Error("Workflow session status must be active or complete.");
222
271
  assertTimestamp(session.startedAt);
223
272
  assertTimestamp(session.updatedAt);
273
+ if (session.lane !== undefined && session.lane !== "light")
274
+ throw new Error("Workflow session lane must be light when present.");
275
+ if (session.lane === "light" && !session.laneReason)
276
+ throw new Error("Light-lane sessions require a lane reason.");
277
+ if (session.laneEscalatedAt !== undefined) {
278
+ assertTimestamp(session.laneEscalatedAt);
279
+ if (session.lane !== "light")
280
+ throw new Error("Lane escalation is only meaningful on light-lane sessions.");
281
+ if (!session.laneEscalationReason)
282
+ throw new Error("Lane escalation requires a reason.");
283
+ }
224
284
  }
225
285
  function withStage(ledger, stage, record) {
226
286
  return {
@@ -287,6 +347,10 @@ function validateLedger(ledger) {
287
347
  if (ledger.schema !== 1 || !isRecord(ledger.stages))
288
348
  throw new Error("Approval ledger schema must be version 1 with a stages mapping.");
289
349
  assertWorkflowId(ledger.workflowId);
350
+ if (ledger.lane !== undefined && ledger.lane !== "light")
351
+ throw new Error("Approval ledger lane must be light when present.");
352
+ if (ledger.lane === "light" && !ledger.laneReason)
353
+ throw new Error("Light-lane ledgers require a lane reason.");
290
354
  for (const [stage, record] of Object.entries(ledger.stages)) {
291
355
  if (!isApprovalStage(stage))
292
356
  throw new Error(`Approval ledger has unknown workflow stage: ${stage}.`);
@@ -3,6 +3,7 @@ const CATALOG = [
3
3
  {
4
4
  id: "brainstorming",
5
5
  provider: "multileaf",
6
+ universal: true,
6
7
  rationale: "A project-specific brainstorming flow helps clarify goals and constraints before implementation.",
7
8
  permissions: [],
8
9
  resources: local([
@@ -17,6 +18,7 @@ const CATALOG = [
17
18
  {
18
19
  id: "ticket-driven-development",
19
20
  provider: "multileaf",
21
+ universal: true,
20
22
  rationale: "Work often arrives as a ticket or an already-clear requirement; this entry point starts the SDD flow from the ticket without a brainstorming phase.",
21
23
  permissions: [],
22
24
  resources: local([
@@ -30,6 +32,7 @@ const CATALOG = [
30
32
  {
31
33
  id: "requirements-specification",
32
34
  provider: "multileaf",
35
+ universal: true,
33
36
  rationale: "The project can use explicit requirements and acceptance criteria before changes are planned.",
34
37
  permissions: [],
35
38
  resources: local([
@@ -44,6 +47,7 @@ const CATALOG = [
44
47
  {
45
48
  id: "implementation-planning",
46
49
  provider: "multileaf",
50
+ universal: true,
47
51
  rationale: "A traceable implementation plan can connect project requirements, tests, risks, and validation.",
48
52
  permissions: [],
49
53
  resources: local([
@@ -59,6 +63,7 @@ const CATALOG = [
59
63
  {
60
64
  id: "technical-design",
61
65
  provider: "multileaf",
66
+ universal: true,
62
67
  rationale: "Architecture changes benefit from explicit alternatives, trade-offs, and decision records.",
63
68
  permissions: [],
64
69
  resources: local([
@@ -74,6 +79,7 @@ const CATALOG = [
74
79
  {
75
80
  id: "code-review",
76
81
  provider: "multileaf",
82
+ universal: true,
77
83
  rationale: "A consistent review checklist helps validate changes against this repository's conventions.",
78
84
  permissions: [],
79
85
  resources: local([
@@ -164,6 +170,7 @@ const CATALOG = [
164
170
  {
165
171
  id: "verification",
166
172
  provider: "multileaf",
173
+ universal: true,
167
174
  rationale: "Verification guidance can use the project's detected test runner and quality commands.",
168
175
  permissions: [],
169
176
  resources: local([
@@ -241,7 +248,15 @@ const CATALOG = [
241
248
  ];
242
249
  export function recommendCapabilities(profile) {
243
250
  return CATALOG.flatMap((capability) => {
244
- const match = capability.matches(profile);
251
+ const match = capability.matches(profile) ??
252
+ (capability.universal
253
+ ? {
254
+ evidence: [
255
+ "Workflow capability: applies to any project, with or without detected signals.",
256
+ ],
257
+ confidence: 0.9,
258
+ }
259
+ : undefined);
245
260
  return match
246
261
  ? [
247
262
  {