@multileaf/ai-workflow 0.9.0 → 0.11.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 +34 -0
  2. package/README.md +12 -1
  3. package/dist/blocking-reasons.d.ts +67 -0
  4. package/dist/blocking-reasons.js +199 -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 +19 -2
  8. package/dist/human-approval.js +82 -9
  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 +280 -30
  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 +24 -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,40 @@ All notable changes to this project are documented in this file. The format foll
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.11.0] - 2026-10-31
8
+
9
+ ### Added
10
+
11
+ - `aiw workflow abandon --reason=<why>` archives an obsolete active workflow: the session (with `status: abandoned` and the recorded reason) and the approval ledger move under `.aiw/checkpoints/`, the active slot is freed, and a new workflow can start immediately. Reason is required and audited.
12
+ - Typed blocking reasons for workflow lifecycle: `workflow_still_active` (starting over an active workflow now names `aiw workflow complete` **or** `aiw workflow abandon --reason=...` as the way out — previously a prose-only dead end found during field testing), `workflow_not_active`, and `workflow_complete`.
13
+
14
+ ### Fixed
15
+
16
+ - Release automation now actually releases: the Auto-tag workflow creates the version tag **and invokes the Release workflow directly** (reusable-workflow call). Previously the tag was created through the API with the default `GITHUB_TOKEN`, whose events do not trigger other workflows — so merging a version bump silently published nothing. This swallowed the v0.9.1 release (never published to npm) and delayed v0.10.0 until a manual tag re-push.
17
+
18
+ ## [0.10.0] - 2026-10-30
19
+
20
+ ### Added
21
+
22
+ - 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.
23
+ - `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).
24
+ - 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.
25
+ - 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.
26
+ - 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.
27
+ - 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.
28
+ - 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.
29
+
30
+ ### Fixed
31
+
32
+ - `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.
33
+ - `aiw risk` without a subcommand now prints its own typed usage instead of the general command dump.
34
+
35
+ ## [0.9.1] - 2026-10-24
36
+
37
+ ### Fixed
38
+
39
+ - 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.
40
+
7
41
  ## [0.9.0] - 2026-10-21
8
42
 
9
43
  ## [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", "workflow_still_active", "workflow_not_active", "workflow_complete"];
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,199 @@
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
+ "workflow_still_active",
23
+ "workflow_not_active",
24
+ "workflow_complete",
25
+ ];
26
+ function command(commandText) {
27
+ return { kind: "command", command: commandText };
28
+ }
29
+ function action(description) {
30
+ return { kind: "action", description };
31
+ }
32
+ function terminal(humanDecision) {
33
+ return { kind: "terminal", humanDecision };
34
+ }
35
+ function firstStage(stages) {
36
+ return stages && stages.length > 0 ? stages[0] : undefined;
37
+ }
38
+ export const BLOCKING_REASON_INVENTORY = {
39
+ gate_evidence_missing: {
40
+ code: "gate_evidence_missing",
41
+ description: "The quality gate is blocked because the required evidence artifact is missing.",
42
+ continuation: (context) => context.stage && context.artifactPath
43
+ ? action(`Create the required artifact at ${context.artifactPath}, then run \`aiw gate ${context.stage}\`.`)
44
+ : terminal("Decide which stage artifact to author before gating."),
45
+ },
46
+ gate_incomplete: {
47
+ code: "gate_incomplete",
48
+ description: "The quality gate found completeness issues in the stage artifact.",
49
+ continuation: (context) => context.stage
50
+ ? action(`Fix the gate findings in the stage artifact${context.artifactPath ? ` (${context.artifactPath})` : ""}, then run \`aiw gate ${context.stage}\` again.`)
51
+ : terminal("Fix the gate findings, then rerun the stage gate."),
52
+ },
53
+ stage_prerequisite_pending: {
54
+ code: "stage_prerequisite_pending",
55
+ description: "A previous stage is awaiting human approval before this stage can start.",
56
+ continuation: (context) => {
57
+ const pending = firstStage(context.pendingStages);
58
+ return pending
59
+ ? command(`aiw approve ${pending}`)
60
+ : terminal("Review the pending previous stage with a human.");
61
+ },
62
+ },
63
+ stage_prerequisite_unresolved: {
64
+ code: "stage_prerequisite_unresolved",
65
+ description: "Previous stages need human approval or an accepted optional skip before this stage can start.",
66
+ continuation: (context) => {
67
+ const unresolved = firstStage(context.unresolvedStages);
68
+ return unresolved
69
+ ? command(`aiw approve ${unresolved}`)
70
+ : terminal("Resolve the previous stages with a human before continuing.");
71
+ },
72
+ },
73
+ stage_awaiting_approval: {
74
+ code: "stage_awaiting_approval",
75
+ description: "This stage is awaiting human approval; approve or reject it before continuing.",
76
+ continuation: (context) => context.stage
77
+ ? command(`aiw approve ${context.stage}`)
78
+ : terminal("A human must approve or reject the pending stage."),
79
+ },
80
+ stage_already_approved: {
81
+ code: "stage_already_approved",
82
+ description: "This stage is already approved; changed evidence requires rerunning its quality gate.",
83
+ continuation: (context) => context.stage
84
+ ? command(`aiw gate ${context.stage}`)
85
+ : terminal("A human must decide whether to rerun the gate for the changed evidence."),
86
+ },
87
+ approval_evidence_missing: {
88
+ code: "approval_evidence_missing",
89
+ description: "Approval cannot be recorded because the stage evidence artifact is missing.",
90
+ continuation: (context) => context.stage && context.artifactPath
91
+ ? action(`Create the required artifact at ${context.artifactPath}, then run \`aiw approve ${context.stage}\`.`)
92
+ : terminal("Decide which stage artifact to author before approving."),
93
+ },
94
+ approval_stale: {
95
+ code: "approval_stale",
96
+ description: "The artifact changed after review; its approval is stale and the gate must run again.",
97
+ continuation: (context) => context.stage
98
+ ? command(`aiw gate ${context.stage}`)
99
+ : terminal("A human must decide whether to rerun the gate for the changed artifact."),
100
+ },
101
+ approval_gate_missing: {
102
+ code: "approval_gate_missing",
103
+ description: "No passing quality gate is recorded for this stage; gate it before approving.",
104
+ continuation: (context) => context.stage
105
+ ? command(`aiw gate ${context.stage}`)
106
+ : terminal("A human must decide whether to gate the stage before approving."),
107
+ },
108
+ lane_escalation_required: {
109
+ code: "lane_escalation_required",
110
+ description: "Light-lane workflows do not author stage artifacts; escalate to the full lane to continue.",
111
+ continuation: () => action("Run `aiw task escalate --reason=<why scope grew>` to move this workflow to the full lane; recorded light-lane evidence is preserved."),
112
+ },
113
+ light_evidence_missing: {
114
+ code: "light_evidence_missing",
115
+ description: "A light-lane workflow cannot complete without at least one recorded validation entry.",
116
+ continuation: () => action("Run `aiw task evidence --validation=<command and result>` at least once, then `aiw workflow complete`."),
117
+ },
118
+ workflow_still_active: {
119
+ code: "workflow_still_active",
120
+ description: "A workflow is already active; finish or archive it before starting another.",
121
+ continuation: () => action("Run `aiw workflow complete` once every stage resolves, or archive an obsolete one with `aiw workflow abandon --reason=<why>`."),
122
+ },
123
+ workflow_not_active: {
124
+ code: "workflow_not_active",
125
+ description: "No SDD workflow is active for this command to operate on.",
126
+ continuation: () => command("aiw workflow start"),
127
+ },
128
+ workflow_complete: {
129
+ code: "workflow_complete",
130
+ description: "This workflow is already complete; start a new workflow to continue working.",
131
+ continuation: () => command("aiw workflow start"),
132
+ },
133
+ };
134
+ export function getBlockingReasonDefinition(code) {
135
+ const definition = BLOCKING_REASON_INVENTORY[code];
136
+ if (!definition)
137
+ throw new Error(`Unknown blocking reason code: ${code}`);
138
+ return definition;
139
+ }
140
+ export function describeBlockingReason(code, context) {
141
+ const definition = getBlockingReasonDefinition(code);
142
+ return {
143
+ code,
144
+ message: context.detail ?? definition.description,
145
+ continuation: definition.continuation(context),
146
+ };
147
+ }
148
+ /** Error thrown by blocking CLI paths; carries the typed result for `--json` output. */
149
+ export class BlockingError extends Error {
150
+ result;
151
+ constructor(result) {
152
+ super(formatBlockingOutput(result));
153
+ this.name = "BlockingError";
154
+ this.result = result;
155
+ }
156
+ }
157
+ export function formatBlockingOutput(result) {
158
+ const prefix = `${result.message} [reason: ${result.code}]`;
159
+ switch (result.continuation.kind) {
160
+ case "command":
161
+ return `${prefix} Next: run \`${result.continuation.command}\`.`;
162
+ case "action":
163
+ return `${prefix} Next: ${result.continuation.description}`;
164
+ case "terminal":
165
+ return `${prefix} Terminal: no automatic continuation. ${result.continuation.humanDecision}`;
166
+ }
167
+ }
168
+ export function blockingResultToJson(result) {
169
+ switch (result.continuation.kind) {
170
+ case "command":
171
+ return { reason_code: result.code, next_command: result.continuation.command };
172
+ case "action":
173
+ return { reason_code: result.code, next_action: result.continuation.description };
174
+ case "terminal":
175
+ return { reason_code: result.code, terminal: true };
176
+ }
177
+ }
178
+ const CONTINUATION_KIND_LABEL = {
179
+ command: "runnable command",
180
+ action: "explicit action",
181
+ terminal: "terminal (human decision)",
182
+ };
183
+ /** Markdown continuation inventory for docs generation and CI parity checks. */
184
+ export function exportContinuationInventoryMarkdown() {
185
+ const rows = BLOCKING_REASON_CODES.map((code) => {
186
+ const definition = getBlockingReasonDefinition(code);
187
+ return `| \`${code}\` | ${definition.description} | ${CONTINUATION_KIND_LABEL[commandKind(code)]} |`;
188
+ });
189
+ return ["| Reason code | Description | Continuation |", "| --- | --- | --- |", ...rows].join("\n");
190
+ }
191
+ function commandKind(code) {
192
+ const sample = describeBlockingReason(code, {
193
+ stage: "specification",
194
+ pendingStages: ["brainstorming"],
195
+ unresolvedStages: ["brainstorming"],
196
+ artifactPath: ".aiw/generated/specs/specification.md",
197
+ });
198
+ return sample.continuation.kind;
199
+ }
@@ -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,22 +13,39 @@ 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;
19
22
  workflowId: string;
20
- status: "active" | "complete";
23
+ status: "active" | "complete" | "abandoned";
21
24
  startedAt: string;
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;
35
+ /** Set when the workflow is archived via `aiw workflow abandon`. */
36
+ abandonedReason?: string;
25
37
  };
38
+ /** The lane that currently governs stage commands: escalation moves light → full. */
39
+ export declare function effectiveLane(session: WorkflowSession): "full" | "light";
26
40
  export declare function validateTaskTicket(ticket: string): void;
27
41
  export type ApprovalStageDefinition = {
28
42
  optional: boolean;
29
43
  requiresQualityGate: boolean;
30
44
  };
31
- export declare function createApprovalLedger(workflowId: string): ApprovalLedger;
45
+ export declare function createApprovalLedger(workflowId: string, options?: {
46
+ lane?: "light";
47
+ laneReason?: string;
48
+ }): ApprovalLedger;
32
49
  export declare function serializeWorkflowSession(session: WorkflowSession): string;
33
50
  export declare function parseWorkflowSession(content: string): WorkflowSession;
34
51
  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,24 @@ const SESSION_KEYS = new Set([
34
38
  "updatedAt",
35
39
  "ticket",
36
40
  "title",
41
+ "lane",
42
+ "laneReason",
43
+ "laneEscalatedAt",
44
+ "laneEscalationReason",
45
+ "abandonedReason",
37
46
  ]);
38
- export function createApprovalLedger(workflowId) {
47
+ export function createApprovalLedger(workflowId, options = {}) {
39
48
  assertWorkflowId(workflowId);
40
- return { schema: 1, workflowId, stages: {} };
49
+ if (options.lane === "light" && !options.laneReason)
50
+ throw new Error("Light-lane ledgers require a recorded lane reason.");
51
+ return {
52
+ schema: 1,
53
+ workflowId,
54
+ stages: {},
55
+ ...(options.lane === "light" && options.laneReason
56
+ ? { lane: "light", laneReason: options.laneReason }
57
+ : {}),
58
+ };
41
59
  }
42
60
  export function serializeWorkflowSession(session) {
43
61
  validateWorkflowSession(session);
@@ -55,8 +73,8 @@ export function parseWorkflowSession(content) {
55
73
  throw new Error(`Workflow session has unknown field: ${key}.`);
56
74
  if (typeof value.workflowId !== "string")
57
75
  throw new Error("Workflow session requires a workflowId.");
58
- if (value.status !== "active" && value.status !== "complete")
59
- throw new Error("Workflow session status must be active or complete.");
76
+ if (value.status !== "active" && value.status !== "complete" && value.status !== "abandoned")
77
+ throw new Error("Workflow session status must be active, complete, or abandoned.");
60
78
  if (typeof value.startedAt !== "string" || typeof value.updatedAt !== "string")
61
79
  throw new Error("Workflow session requires start and update timestamps.");
62
80
  if (value.ticket !== undefined && typeof value.ticket !== "string")
@@ -65,6 +83,23 @@ export function parseWorkflowSession(content) {
65
83
  throw new Error("Workflow session title must be a string.");
66
84
  if (typeof value.ticket === "string")
67
85
  validateTaskTicket(value.ticket);
86
+ const lane = value.lane === undefined ? undefined : value.lane;
87
+ if (lane !== undefined && lane !== "light")
88
+ throw new Error("Workflow session lane must be light when present.");
89
+ const laneReason = value.laneReason;
90
+ if (laneReason !== undefined && typeof laneReason !== "string")
91
+ throw new Error("Workflow session lane reason must be a string.");
92
+ if (lane === "light" && !laneReason)
93
+ throw new Error("Light-lane sessions require a lane reason.");
94
+ const laneEscalatedAt = value.laneEscalatedAt;
95
+ if (laneEscalatedAt !== undefined && typeof laneEscalatedAt !== "string")
96
+ throw new Error("Workflow session lane escalation timestamp must be a string.");
97
+ const laneEscalationReason = value.laneEscalationReason;
98
+ if (laneEscalationReason !== undefined && typeof laneEscalationReason !== "string")
99
+ throw new Error("Workflow session lane escalation reason must be a string.");
100
+ const abandonedReason = value.abandonedReason;
101
+ if (abandonedReason !== undefined && typeof abandonedReason !== "string")
102
+ throw new Error("Workflow session abandonment reason must be a string.");
68
103
  const session = {
69
104
  schema: 1,
70
105
  workflowId: value.workflowId,
@@ -73,6 +108,11 @@ export function parseWorkflowSession(content) {
73
108
  updatedAt: value.updatedAt,
74
109
  ...(value.ticket === undefined ? {} : { ticket: value.ticket }),
75
110
  ...(value.title === undefined ? {} : { title: value.title }),
111
+ ...(lane === undefined ? {} : { lane }),
112
+ ...(laneReason === undefined ? {} : { laneReason }),
113
+ ...(laneEscalatedAt === undefined ? {} : { laneEscalatedAt }),
114
+ ...(laneEscalationReason === undefined ? {} : { laneEscalationReason }),
115
+ ...(abandonedReason === undefined ? {} : { abandonedReason }),
76
116
  };
77
117
  validateWorkflowSession(session);
78
118
  return session;
@@ -168,7 +208,14 @@ export function invalidateStageAndDescendants(ledger, stage, updatedAt) {
168
208
  export function serializeApprovalLedger(ledger) {
169
209
  validateLedger(ledger);
170
210
  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 });
211
+ return stringify({
212
+ schema: 1,
213
+ workflowId: ledger.workflowId,
214
+ stages,
215
+ ...(ledger.lane === "light" && ledger.laneReason
216
+ ? { lane: ledger.lane, laneReason: ledger.laneReason }
217
+ : {}),
218
+ }, { lineWidth: 0 });
172
219
  }
173
220
  export function parseApprovalLedger(content) {
174
221
  const document = parseDocument(content, { uniqueKeys: true, strict: true });
@@ -191,7 +238,14 @@ export function parseApprovalLedger(content) {
191
238
  throw new Error(`Approval ledger has unknown workflow stage: ${stageName}.`);
192
239
  stages[stageName] = parseStageRecord(stageName, record);
193
240
  }
194
- const ledger = { schema: 1, workflowId: value.workflowId, stages };
241
+ const ledger = {
242
+ schema: 1,
243
+ workflowId: value.workflowId,
244
+ stages,
245
+ ...(value.lane === "light" && typeof value.laneReason === "string"
246
+ ? { lane: "light", laneReason: value.laneReason }
247
+ : {}),
248
+ };
195
249
  validateLedger(ledger);
196
250
  return ledger;
197
251
  }
@@ -217,10 +271,25 @@ function validateWorkflowSession(session) {
217
271
  if (session.schema !== 1)
218
272
  throw new Error("Workflow session schema must be version 1.");
219
273
  assertWorkflowId(session.workflowId);
220
- if (session.status !== "active" && session.status !== "complete")
221
- throw new Error("Workflow session status must be active or complete.");
274
+ if (session.status !== "active" &&
275
+ session.status !== "complete" &&
276
+ session.status !== "abandoned")
277
+ throw new Error("Workflow session status must be active, complete, or abandoned.");
222
278
  assertTimestamp(session.startedAt);
223
279
  assertTimestamp(session.updatedAt);
280
+ if (session.lane !== undefined && session.lane !== "light")
281
+ throw new Error("Workflow session lane must be light when present.");
282
+ if (session.lane === "light" && !session.laneReason)
283
+ throw new Error("Light-lane sessions require a lane reason.");
284
+ if (session.laneEscalatedAt !== undefined) {
285
+ assertTimestamp(session.laneEscalatedAt);
286
+ if (session.lane !== "light")
287
+ throw new Error("Lane escalation is only meaningful on light-lane sessions.");
288
+ if (!session.laneEscalationReason)
289
+ throw new Error("Lane escalation requires a reason.");
290
+ }
291
+ if (session.status === "abandoned" && !session.abandonedReason)
292
+ throw new Error("Abandoned sessions require an abandonment reason.");
224
293
  }
225
294
  function withStage(ledger, stage, record) {
226
295
  return {
@@ -287,6 +356,10 @@ function validateLedger(ledger) {
287
356
  if (ledger.schema !== 1 || !isRecord(ledger.stages))
288
357
  throw new Error("Approval ledger schema must be version 1 with a stages mapping.");
289
358
  assertWorkflowId(ledger.workflowId);
359
+ if (ledger.lane !== undefined && ledger.lane !== "light")
360
+ throw new Error("Approval ledger lane must be light when present.");
361
+ if (ledger.lane === "light" && !ledger.laneReason)
362
+ throw new Error("Light-lane ledgers require a lane reason.");
290
363
  for (const [stage, record] of Object.entries(ledger.stages)) {
291
364
  if (!isApprovalStage(stage))
292
365
  throw new Error(`Approval ledger has unknown workflow stage: ${stage}.`);