@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/dist/workflow.js CHANGED
@@ -50,7 +50,8 @@ import { composeContext, CONTEXT_LAYERS, readContextStore, writeContextLayer, }
50
50
  import { loadContextDelta, retrieveTaskContext } from "./context-retrieval.js";
51
51
  import { createSummaryCache, isSummaryFresh, parseSummaryCache, serializeSummaryCache, } from "./context-cache.js";
52
52
  import { parseTokenUsage, recordTokenUsage, serializeTokenUsage, summarizeUsageByModel, } from "./token-usage.js";
53
- import { APPROVAL_STAGES, assertWorkflowId, validateTaskTicket, approveStage, beginStageApproval, createApprovalLedger, getUnresolvedPrerequisites, invalidateStageAndDescendants, parseApprovalLedger, parseWorkflowSession, rejectStage, serializeApprovalLedger, serializeWorkflowSession, skipOptionalStage, stageDefinition, stageFingerprint, } from "./human-approval.js";
53
+ import { APPROVAL_STAGES, assertWorkflowId, validateTaskTicket, approveStage, beginStageApproval, createApprovalLedger, getUnresolvedPrerequisites, invalidateStageAndDescendants, parseApprovalLedger, parseWorkflowSession, rejectStage, serializeApprovalLedger, serializeWorkflowSession, skipOptionalStage, stageDefinition, stageFingerprint, effectiveLane, } from "./human-approval.js";
54
+ import { parse as parseYaml, stringify } from "yaml";
54
55
  import { measureContextQuality, serializeContextQuality } from "./context-quality.js";
55
56
  import { collectContextBench, compareContextBench, parseContextBench, serializeContextBench, } from "./context-benchmark.js";
56
57
  import { materializeParityTask, parseParityTask, runParitySuite, serializeParitySuite, TASK_ASSEMBLER, } from "./task-parity.js";
@@ -64,6 +65,8 @@ import { parseTraceabilityGraph } from "./traceability.js";
64
65
  import { serializeAdapterCapabilities } from "./adapter-contract.js";
65
66
  import { buildTraceabilityGraph, serializeTraceabilityGraph } from "./traceability.js";
66
67
  import { evaluateQualityGate } from "./quality-gates.js";
68
+ import { BlockingError, blockingResultToJson, describeBlockingReason } from "./blocking-reasons.js";
69
+ import { runRiskAssess } from "./risk-assess.js";
67
70
  import { parseMigrationCheckpoint, planNeutralResourceMigration, readResourceBytes, serializeMigrationPreview, serializeMigrationCheckpoint, validateNeutralResourceDocument, writeResourceBytes, } from "./migration.js";
68
71
  import { parsePrivateRegistries, parseTeamPreset, serializePrivateRegistries, serializeTeamPreset, fetchPrivateRegistryClient, } from "./team-configuration.js";
69
72
  import { enforcePolicyGate, gitPolicyTracking } from "./policy-gate.js";
@@ -110,8 +113,15 @@ function beginWorkflowSession(fs, aiw, options) {
110
113
  updatedAt: now,
111
114
  ...(options.ticket ? { ticket: options.ticket } : {}),
112
115
  ...(options.title ? { title: options.title } : {}),
116
+ ...(options.lane === "light" && options.laneReason
117
+ ? { lane: "light", laneReason: options.laneReason }
118
+ : {}),
113
119
  };
114
- const ledger = createApprovalLedger(workflowId);
120
+ const ledger = createApprovalLedger(workflowId, {
121
+ ...(options.lane === "light" && options.laneReason
122
+ ? { lane: "light", laneReason: options.laneReason }
123
+ : {}),
124
+ });
115
125
  fs.write(ledgerPath, serializeApprovalLedger(ledger));
116
126
  fs.write(sessionPath, serializeWorkflowSession(session));
117
127
  return { exitCode: 0, output: `Workflow started: ${workflowId}.` };
@@ -124,14 +134,35 @@ export async function runCommand(args, root, fs, services = {}) {
124
134
  if (command === "task") {
125
135
  if (!fs.exists(join(aiw, "manifest.yml")))
126
136
  return { exitCode: 1, error: "Run `aiw install` first." };
127
- const usage = "Usage: aiw task start --ticket=TEAM-123 [--title=text] [--id=workflow-id] | aiw task status";
137
+ const usage = "Usage: aiw task start --ticket=TEAM-123 [--title=text] [--id=workflow-id] [--lane=light --lane-reason=text] | aiw task status | aiw task escalate --reason=text | aiw task evidence --validation=text";
128
138
  const action = args[1];
129
139
  const sessionPath = join(aiw, "workflow.yml");
130
140
  if (action === "start") {
131
- const options = parseNamedOptions(args.slice(2), ["ticket", "title", "id"]);
141
+ const options = parseNamedOptions(args.slice(2), [
142
+ "ticket",
143
+ "title",
144
+ "id",
145
+ "lane",
146
+ "lane-reason",
147
+ ]);
132
148
  if (!options?.ticket)
133
149
  return { exitCode: 1, error: usage };
134
150
  validateTaskTicket(options.ticket);
151
+ if (options.lane !== undefined && options.lane !== "light")
152
+ return { exitCode: 1, error: "Only --lane=light exists; omit it for the full lane." };
153
+ const laneReason = options["lane-reason"];
154
+ if (options.lane === "light") {
155
+ if (!laneReason)
156
+ return {
157
+ exitCode: 1,
158
+ error: "--lane=light requires --lane-reason: the resume-test call, recorded for audit.",
159
+ };
160
+ if (laneReason.length > 500 || /[\r\n]/.test(laneReason))
161
+ return {
162
+ exitCode: 1,
163
+ error: "--lane-reason must be a single line of at most 500 bytes.",
164
+ };
165
+ }
135
166
  if (options.id !== undefined) {
136
167
  try {
137
168
  assertWorkflowId(options.id);
@@ -140,20 +171,104 @@ export async function runCommand(args, root, fs, services = {}) {
140
171
  return { exitCode: 1, error: "Workflow ID must be a UUID." };
141
172
  }
142
173
  }
174
+ const lane = options.lane === "light" ? "light" : undefined;
143
175
  const result = beginWorkflowSession(fs, aiw, {
144
176
  ...(options.id ? { id: options.id } : {}),
145
177
  ticket: options.ticket,
146
178
  ...(options.title ? { title: options.title } : {}),
179
+ ...(lane ? { lane, laneReason: laneReason } : {}),
147
180
  });
148
181
  if (result.exitCode !== 0)
149
182
  return result;
150
183
  const title = options.title ? ` titled "${options.title}"` : "";
184
+ if (lane)
185
+ return {
186
+ exitCode: 0,
187
+ output: `Task started for ticket ${options.ticket}${title} (light lane): ${result.output}\n` +
188
+ "Resume test: this work is small when it is understood, risk is contained, and the work could be resumed from your request plus `git diff`.\n" +
189
+ `Rationale recorded: ${laneReason}\n` +
190
+ 'No durable spec/plan artifacts are required. Record validation with `aiw task evidence --validation="<command and result>"`, then complete with `aiw workflow complete`. If scope grows, escalate with `aiw task escalate --reason=...`.',
191
+ };
151
192
  return {
152
193
  exitCode: 0,
153
194
  output: `Task started for ticket ${options.ticket}${title}: ${result.output}\n` +
154
195
  `Next steps: ask the user whether the requirement is clear enough to skip brainstorming (record an accepted skip with \`aiw skip brainstorming --reason="Requirement provided by ticket ${options.ticket}"\`), then create the specification with \`aiw spec --ticket=${options.ticket} --title=...\`.`,
155
196
  };
156
197
  }
198
+ if (action === "escalate") {
199
+ const options = parseNamedOptions(args.slice(2), ["reason"]);
200
+ if (!options?.reason)
201
+ return { exitCode: 1, error: "Usage: aiw task escalate --reason=why-scope-grew" };
202
+ if (!fs.exists(sessionPath))
203
+ return { exitCode: 1, error: "No SDD workflow is active. Start one first." };
204
+ const { session } = readWorkflowState(aiw, fs);
205
+ if (session.lane !== "light")
206
+ return {
207
+ exitCode: 1,
208
+ error: "Only light-lane workflows escalate; this one already runs the full lane.",
209
+ };
210
+ if (session.laneEscalatedAt)
211
+ return {
212
+ exitCode: 1,
213
+ error: `Workflow ${session.workflowId} already escalated to the full lane.`,
214
+ };
215
+ const now = new Date().toISOString();
216
+ fs.write(sessionPath, serializeWorkflowSession({
217
+ ...session,
218
+ laneEscalatedAt: now,
219
+ laneEscalationReason: options.reason,
220
+ updatedAt: now,
221
+ }));
222
+ const evidencePath = join(aiw, `checkpoints/task-light-${session.workflowId}.yml`);
223
+ return {
224
+ exitCode: 0,
225
+ output: `Workflow ${session.workflowId} escalated to the full lane.\n` +
226
+ (fs.exists(evidencePath)
227
+ ? `Light-lane evidence preserved and linked: ${evidencePath}\n`
228
+ : "No light-lane evidence had been recorded yet.\n") +
229
+ "Stages now run the full approval sequence from brainstorming.",
230
+ };
231
+ }
232
+ if (action === "evidence") {
233
+ const options = parseNamedOptions(args.slice(2), ["validation"]);
234
+ if (!options?.validation)
235
+ return {
236
+ exitCode: 1,
237
+ error: 'Usage: aiw task evidence --validation="<command and its observed result>"',
238
+ };
239
+ if (!fs.exists(sessionPath))
240
+ return { exitCode: 1, error: "No SDD workflow is active. Start one first." };
241
+ const { session } = readWorkflowState(aiw, fs);
242
+ if (effectiveLane(session) !== "light")
243
+ return {
244
+ exitCode: 1,
245
+ error: "Evidence entries are a light-lane command; full-lane workflows record stage artifacts and gates.",
246
+ };
247
+ const evidencePath = join(aiw, `checkpoints/task-light-${session.workflowId}.yml`);
248
+ const existing = fs.exists(evidencePath)
249
+ ? parseLightEvidence(fs.read(evidencePath))
250
+ : {
251
+ schema: 1,
252
+ workflowId: session.workflowId,
253
+ ...(session.ticket ? { ticket: session.ticket } : {}),
254
+ lane: "light",
255
+ ...(session.laneReason ? { laneReason: session.laneReason } : {}),
256
+ entries: [],
257
+ };
258
+ const updated = {
259
+ ...existing,
260
+ entries: [
261
+ ...existing.entries,
262
+ { note: options.validation, recordedAt: new Date().toISOString() },
263
+ ],
264
+ };
265
+ fs.mkdir(join(aiw, "checkpoints"));
266
+ fs.write(evidencePath, stringify({ ...updated }, { lineWidth: 0 }));
267
+ return {
268
+ exitCode: 0,
269
+ output: `Validation evidence recorded (${updated.entries.length} entr${updated.entries.length === 1 ? "y" : "ies"}): ${options.validation}`,
270
+ };
271
+ }
157
272
  if (action === "status") {
158
273
  if (!fs.exists(sessionPath))
159
274
  return {
@@ -161,14 +276,24 @@ export async function runCommand(args, root, fs, services = {}) {
161
276
  output: "No SDD workflow is active. Start one with `aiw task start --ticket=TEAM-123`.",
162
277
  };
163
278
  const { session, ledger } = readWorkflowState(aiw, fs, false);
279
+ const lane = session.lane === "light"
280
+ ? [
281
+ `Lane: light — ${session.laneEscalatedAt ? `escalated to the full lane (${session.laneEscalationReason})` : session.laneReason}`,
282
+ ]
283
+ : [];
164
284
  const lines = [
165
285
  ...(session.ticket
166
286
  ? [`Ticket: ${session.ticket}${session.title ? ` — ${session.title}` : ""}`]
167
287
  : []),
288
+ ...lane,
168
289
  `Workflow: ${session.workflowId} (${session.status})`,
169
290
  "Stages:",
170
291
  ...APPROVAL_STAGES.map((stage) => `- ${stage}: ${ledger.stages[stage]?.status ?? "not-started"}`),
171
292
  ];
293
+ if (session.status === "active" && effectiveLane(session) === "light") {
294
+ lines.push('Next: record validation with `aiw task evidence --validation="<command and result>"`, then `aiw workflow complete`. If scope grows: `aiw task escalate --reason=...`.');
295
+ return { exitCode: 0, output: lines.join("\n") };
296
+ }
172
297
  const next = APPROVAL_STAGES.find((stage) => (ledger.stages[stage]?.status ?? "not-started") === "not-started");
173
298
  if (session.status === "active" && next)
174
299
  lines.push(`Next: ${next === "brainstorming" && session.ticket ? `decide brainstorm or record an accepted skip (ticket ${session.ticket} already provides the requirement)` : `advance ${next} (gate, approval)`}.`);
@@ -199,6 +324,25 @@ export async function runCommand(args, root, fs, services = {}) {
199
324
  }
200
325
  if (action === "complete") {
201
326
  const { session, ledger } = readActiveWorkflow(aiw, fs);
327
+ if (effectiveLane(session) === "light") {
328
+ const evidencePath = join(aiw, `checkpoints/task-light-${session.workflowId}.yml`);
329
+ const hasEntries = fs.exists(evidencePath)
330
+ ? parseLightEvidence(fs.read(evidencePath)).entries.length > 0
331
+ : false;
332
+ if (!hasEntries)
333
+ throw new BlockingError(describeBlockingReason("light_evidence_missing", {
334
+ detail: `Light-lane workflow ${session.workflowId} cannot complete without recorded validation evidence.`,
335
+ }));
336
+ fs.write(sessionPath, serializeWorkflowSession({
337
+ ...session,
338
+ status: "complete",
339
+ updatedAt: new Date().toISOString(),
340
+ }));
341
+ return {
342
+ exitCode: 0,
343
+ output: `Workflow completed (light lane): ${session.workflowId}. Evidence: ${evidencePath}`,
344
+ };
345
+ }
202
346
  const unresolved = APPROVAL_STAGES.filter((stage) => ledger.stages[stage]?.status !== "approved" &&
203
347
  ledger.stages[stage]?.status !== "skipped");
204
348
  if (unresolved.length)
@@ -222,10 +366,15 @@ export async function runCommand(args, root, fs, services = {}) {
222
366
  const stage = args[1];
223
367
  if (!stage || !APPROVAL_STAGES.includes(stage))
224
368
  return { exitCode: 1, error: `Usage: aiw ${command} <workflow-stage> [options]` };
225
- const { ledger } = readActiveWorkflow(aiw, fs);
369
+ const rest = args.slice(2).filter((argument) => argument !== "--json");
370
+ const { session, ledger } = readActiveWorkflow(aiw, fs);
371
+ if (effectiveLane(session) === "light")
372
+ throw new BlockingError(describeBlockingReason("lane_escalation_required", {
373
+ detail: `Light-lane workflow ${session.workflowId} does not approve stages; escalate to the full lane to continue.`,
374
+ }));
226
375
  const workflowStage = stage;
227
376
  if (command === "skip") {
228
- const options = parseNamedOptions(args.slice(2), ["reason"]);
377
+ const options = parseNamedOptions(rest, ["reason"]);
229
378
  if (!options?.reason)
230
379
  return {
231
380
  exitCode: 1,
@@ -241,19 +390,28 @@ export async function runCommand(args, root, fs, services = {}) {
241
390
  }
242
391
  const artifact = approvalArtifactPath(aiw, workflowStage);
243
392
  if (!artifact || !fs.exists(artifact))
244
- return { exitCode: 1, error: `Approval evidence is missing for ${workflowStage}.` };
393
+ throw new BlockingError(describeBlockingReason("approval_evidence_missing", {
394
+ stage: workflowStage,
395
+ artifactPath: artifact ?? undefined,
396
+ detail: `Approval evidence is missing for ${workflowStage}.`,
397
+ }));
245
398
  const artifactHash = stageFingerprint(fs.read(artifact));
246
399
  if (command === "approve") {
247
400
  if (ledger.stages[workflowStage]?.artifactHash !== artifactHash) {
248
401
  const invalidated = invalidateStageAndDescendants(ledger, workflowStage, new Date().toISOString());
249
402
  fs.write(join(aiw, "approvals.yml"), serializeApprovalLedger(invalidated));
250
403
  const existing = ledger.stages[workflowStage];
251
- return {
252
- exitCode: 1,
253
- error: existing
254
- ? `The ${workflowStage} artifact changed after review; its approval is stale. Rerun the quality gate and request approval again.`
255
- : `No passing quality gate recorded for ${workflowStage}. Run aiw gate ${workflowStage} before approving.`,
256
- };
404
+ if (existing)
405
+ throw new BlockingError(describeBlockingReason("approval_stale", {
406
+ stage: workflowStage,
407
+ artifactPath: artifact,
408
+ detail: `The ${workflowStage} artifact changed after review; its approval is stale. Rerun the quality gate and request approval again.`,
409
+ }));
410
+ throw new BlockingError(describeBlockingReason("approval_gate_missing", {
411
+ stage: workflowStage,
412
+ artifactPath: artifact,
413
+ detail: `No passing quality gate recorded for ${workflowStage}. Run aiw gate ${workflowStage} before approving.`,
414
+ }));
257
415
  }
258
416
  const updated = approveStage(ledger, workflowStage, {
259
417
  artifactHash,
@@ -262,7 +420,7 @@ export async function runCommand(args, root, fs, services = {}) {
262
420
  fs.write(join(aiw, "approvals.yml"), serializeApprovalLedger(updated));
263
421
  return { exitCode: 0, output: `Human approval recorded for ${workflowStage}.` };
264
422
  }
265
- const options = parseNamedOptions(args.slice(2), ["reason"]);
423
+ const options = parseNamedOptions(rest, ["reason"]);
266
424
  if (!options?.reason)
267
425
  return { exitCode: 1, error: "Usage: aiw reject <stage> --reason=human-feedback" };
268
426
  const updated = rejectStage(ledger, workflowStage, options.reason, new Date().toISOString());
@@ -1749,6 +1907,11 @@ export async function runCommand(args, root, fs, services = {}) {
1749
1907
  fs.write(path, serialized);
1750
1908
  return { exitCode: 0, output: `Traceability graph created: ${path}` };
1751
1909
  }
1910
+ if (command === "risk") {
1911
+ if (args[1] !== "assess")
1912
+ return { exitCode: 1, error: "Usage: aiw risk assess [--base-ref=ref] [--json]" };
1913
+ return runRiskAssess(args.slice(2), root, fs);
1914
+ }
1752
1915
  if (command === "gate") {
1753
1916
  if (!fs.exists(join(aiw, "manifest.yml")))
1754
1917
  return { exitCode: 1, error: "Run `aiw install` first." };
@@ -1762,15 +1925,21 @@ export async function runCommand(args, root, fs, services = {}) {
1762
1925
  assertStageReady(aiw, fs, workflowStage);
1763
1926
  const artifactPath = approvalArtifactPath(aiw, workflowStage);
1764
1927
  if (!artifactPath || !fs.exists(artifactPath))
1765
- return {
1766
- exitCode: 1,
1767
- error: `Quality gate blocked: required evidence is missing for ${stage}.`,
1768
- };
1928
+ throw new BlockingError(describeBlockingReason("gate_evidence_missing", {
1929
+ stage,
1930
+ artifactPath: artifactPath ?? undefined,
1931
+ detail: `Quality gate blocked: required evidence is missing for ${stage}.`,
1932
+ }));
1769
1933
  const hasQualityGate = stageDefinition(workflowStage).requiresQualityGate;
1770
1934
  if (hasQualityGate) {
1771
1935
  const issues = evaluateQualityGate(stage, fs.read(artifactPath));
1772
1936
  if (issues.length)
1773
- return { exitCode: 1, error: `Quality gate blocked: ${issues.join(" ")}` };
1937
+ throw new BlockingError(describeBlockingReason("gate_incomplete", {
1938
+ stage,
1939
+ artifactPath,
1940
+ gateIssues: issues,
1941
+ detail: `Quality gate blocked: ${issues.join(" ")}`,
1942
+ }));
1774
1943
  }
1775
1944
  const { ledger } = readActiveWorkflow(aiw, fs);
1776
1945
  const updated = beginStageApproval(ledger, workflowStage, {
@@ -2519,6 +2688,9 @@ export async function runCommand(args, root, fs, services = {}) {
2519
2688
  };
2520
2689
  }
2521
2690
  catch (error) {
2691
+ if (error instanceof BlockingError && args.includes("--json")) {
2692
+ return { exitCode: 1, output: JSON.stringify(blockingResultToJson(error.result)) };
2693
+ }
2522
2694
  return { exitCode: 1, error: error instanceof Error ? error.message : String(error) };
2523
2695
  }
2524
2696
  }
@@ -2561,20 +2733,57 @@ function workflowNextAction(ledger) {
2561
2733
  return `Next action: revise ${next}, rerun its gate, and request human approval.`;
2562
2734
  return `Next action: prepare ${next} evidence.`;
2563
2735
  }
2736
+ function parseLightEvidence(content) {
2737
+ const value = parseYaml(content);
2738
+ if (!value || typeof value !== "object" || value.schema !== 1)
2739
+ throw new Error("Light-lane evidence checkpoint must be schema version 1.");
2740
+ const record = value;
2741
+ const entries = Array.isArray(record.entries) ? record.entries : [];
2742
+ return {
2743
+ schema: 1,
2744
+ workflowId: String(record.workflowId ?? ""),
2745
+ ...(record.ticket !== undefined ? { ticket: String(record.ticket) } : {}),
2746
+ lane: "light",
2747
+ ...(record.laneReason !== undefined ? { laneReason: String(record.laneReason) } : {}),
2748
+ entries: entries.map((entry) => ({
2749
+ note: String(entry.note ?? ""),
2750
+ recordedAt: String(entry.recordedAt ?? ""),
2751
+ })),
2752
+ };
2753
+ }
2564
2754
  function assertStageReady(aiw, fs, stage) {
2565
- const { ledger } = readActiveWorkflow(aiw, fs);
2755
+ const { session, ledger } = readActiveWorkflow(aiw, fs);
2756
+ if (effectiveLane(session) === "light")
2757
+ throw new BlockingError(describeBlockingReason("lane_escalation_required", {
2758
+ stage,
2759
+ detail: `Light-lane workflow ${session.workflowId} does not author stage artifacts; escalate to the full lane to continue.`,
2760
+ }));
2566
2761
  const unresolved = getUnresolvedPrerequisites(ledger, stage);
2567
2762
  if (unresolved.length) {
2568
2763
  const pending = unresolved.filter((prerequisite) => ledger.stages[prerequisite]?.status === "pending-human");
2569
2764
  if (pending.length)
2570
- throw new Error(`Cannot enter ${stage}; previous stage(s) are awaiting human approval: ${pending.join(", ")}.`);
2571
- throw new Error(`Cannot enter ${stage}; previous stages require human approval or an accepted optional skip: ${unresolved.join(", ")}.`);
2765
+ throw new BlockingError(describeBlockingReason("stage_prerequisite_pending", {
2766
+ stage,
2767
+ pendingStages: pending,
2768
+ detail: `Cannot enter ${stage}; previous stage(s) are awaiting human approval: ${pending.join(", ")}.`,
2769
+ }));
2770
+ throw new BlockingError(describeBlockingReason("stage_prerequisite_unresolved", {
2771
+ stage,
2772
+ unresolvedStages: unresolved,
2773
+ detail: `Cannot enter ${stage}; previous stages require human approval or an accepted optional skip: ${unresolved.join(", ")}.`,
2774
+ }));
2572
2775
  }
2573
2776
  const current = ledger.stages[stage];
2574
2777
  if (current?.status === "pending-human")
2575
- throw new Error(`${stage} is awaiting human approval. Approve or reject it before continuing.`);
2778
+ throw new BlockingError(describeBlockingReason("stage_awaiting_approval", {
2779
+ stage,
2780
+ detail: `${stage} is awaiting human approval. Approve or reject it before continuing.`,
2781
+ }));
2576
2782
  if (current?.status === "approved")
2577
- throw new Error(`${stage} is already approved. Evidence changes require rerunning its gate.`);
2783
+ throw new BlockingError(describeBlockingReason("stage_already_approved", {
2784
+ stage,
2785
+ detail: `${stage} is already approved. Evidence changes require rerunning its gate.`,
2786
+ }));
2578
2787
  }
2579
2788
  function approvalArtifactPath(aiw, stage) {
2580
2789
  if (stage === "technical-design")
package/docs/README.md CHANGED
@@ -22,6 +22,8 @@ AI Workflow is a portable, spec-driven development system for AI coding agents.
22
22
  - [Plugin authoring](plugin-authoring.md)
23
23
  - [Release process](release-process.md)
24
24
  - [Release backlog](release-backlog.md)
25
+ - [Continuation inventory](continuation-inventory.md)
26
+ - [Workflow proportionality opportunities](opportunities/README.md) (ODD/RDD analysis, ADR-001)
25
27
  - [Backlog](backlog.md)
26
28
 
27
29
  All generated AI Workflow artifacts are written in English unless the user explicitly requests another language.
@@ -45,6 +45,56 @@ Project analysis is hybrid: deterministic detectors establish observable facts,
45
45
 
46
46
  ## Human workflow approvals
47
47
 
48
+ Workflows run in one of two lanes. The **full lane** (default, and the only non-interactive default) is the staged approval sequence below. The **light lane** (`aiw task start --lane=light --lane-reason=...`, human-chosen via the resume test — work is small when it could be resumed from the request plus `git diff`) skips durable spec/plan artifacts: validation evidence is recorded per entry (`aiw task evidence`), completion requires at least one entry, and the lane plus rationale are written into the session and approval ledger for audit. Stage commands in the light lane block with a `lane_escalation_required` reason until `aiw task escalate --reason=...` moves the workflow to the full lane; recorded evidence is preserved and linked. Escalation is one-way; the full lane then runs the complete approval sequence.
49
+
50
+ ## Current flow
51
+
52
+ ```mermaid
53
+ flowchart TD
54
+ REQ["Change request / ticket"] --> RA["aiw risk assess<br/>read-only · content-based · deterministic"]
55
+ RA -->|"passive — docs-only"| LANE
56
+ RA -->|"medium — plain code"| LANE
57
+ RA -->|"high — security · contracts · concurrency ·<br/>data · delivery · untested (risk due: yes)"| FS
58
+
59
+ LANE{"Human lane decision<br/>resume test: resumable from<br/>request + git diff?"}
60
+ LANE -->|"light — human chooses"| LS
61
+ LANE -->|"full — default"| FS
62
+
63
+ subgraph LIGHT["Light lane — no durable artifacts"]
64
+ LS["aiw task start --lane=light --lane-reason<br/>rationale into session + ledger"] --> LE["aiw task evidence --validation<br/>compact checkpoint"]
65
+ LE --> LC["aiw workflow complete<br/>requires at least one entry"]
66
+ end
67
+
68
+ subgraph FULL["Full lane — staged SDD, gate + human approve per stage"]
69
+ FS["workflow start"] --> BR["brainstorming"] --> SP["specification"] --> TD["technical-design<br/>optional, skip needs reason"] --> PL["plan"] --> IM["implementation<br/>TDD"] --> VE["verification"] --> RV["code review"] --> TR["traceability"] --> FC["workflow complete"]
70
+ end
71
+
72
+ LS -.->|"stage or approve attempted"| B1["Blocked<br/>reason: lane_escalation_required"]
73
+ B1 --> ESC["aiw task escalate --reason<br/>one-way · evidence preserved"]
74
+ ESC --> FS
75
+
76
+ RV -.->|"adversarial-review skills installed"| ADV["judge A + judge B — blind, independent<br/>refuter: corroborated severe blocks<br/>refuted / inconclusive → advisory<br/>unevidenced severe → advisory"]
77
+
78
+ LC --> AUD["Audit trail<br/>approvals ledger · checkpoints · light evidence"]
79
+ FC --> AUD
80
+
81
+ subgraph CI["CI enforcement layer"]
82
+ E1["OPP-004: every block prints a reason code<br/>plus an exact next_command — continuations<br/>are executed by tests before they ship"]
83
+ E2["OPP-006 ratchet: every aiw command named<br/>by skills and READMEs executes in tests"]
84
+ E3["OPP-005 friction benchmark:<br/>in-band / out-of-band / dead-end<br/>budget: zero dead ends"]
85
+ end
86
+
87
+ B1 -.-> E1
88
+ E1 -.-> E2 -.-> E3
89
+ ```
90
+
91
+ Reading notes:
92
+
93
+ - `aiw risk assess` is **advisory**: it informs the lane decision and flags risk due; it never forces a lane or bypasses approval.
94
+ - The light lane is never agent-chosen: `resources/skills/ticket.md` routes it through the host's blocking question widget with the resume test stated verbatim.
95
+ - Every blocking exit on the diagram prints `[reason: <code>]` and either `Next: run \`aiw ...\`` or an explicit terminal statement — the full mapping lives in [Continuation inventory](continuation-inventory.md).
96
+ - A `high` risk assessment with an active workflow points at continuing the workflow (gate and approve); without one it names `aiw workflow start`.
97
+
48
98
  The SDD approval contract models the ordered stages brainstorming, specification, technical design, plan, implementation, verification, review, and traceability. Brainstorming and technical design are optional; skipping either requires a human-accepted reason. Every other stage must be approved. Automated quality-gate results are stored separately from human decisions and can only move an artifact to pending review, never directly to approved.
49
99
 
50
100
  Each approval is scoped to one workflow ID and bound to a SHA-256 fingerprint of the exact artifact reviewed, so a later feature cannot reuse earlier approvals. A content change or rejection makes that stage and its descendants stale. The CLI persists the versioned ledger in `.aiw/approvals.yml`, blocks stage commands when prerequisites are unresolved, and keeps passing quality gates distinct from human approval. The generated bootstrap and stage skills require the host's native blocking question widget for human decisions when available, with a waiting chat prompt as fallback. Approval records provide an audit trail but cannot prove the identity of a respondent or independently verify that the host widget was used.
package/docs/backlog.md CHANGED
@@ -109,6 +109,19 @@ Priority levels: **P0** blocks the product or makes it unsafe; **P1** is require
109
109
 
110
110
  - [x] **ENG-001** Update GitHub Actions to Node 24-native versions. _Acceptance:_ `actions/checkout` and `actions/setup-node` upgraded past v4 so release workflows no longer emit Node.js 20 deprecation warnings; CI and Release workflows pass on `ubuntu-latest` before and after the October 19, 2026 Ubuntu 26 runner migration.
111
111
 
112
+ ## Workflow proportionality (ODD/RDD analysis) — P1–P3
113
+
114
+ Derived from the Gentle-AI ODD/RDD analysis; scope and rejected alternatives recorded in [ADR-001](../.context/adrs/ADR-001-odd-rdd-proportionality-scope.md). Each item links to a self-contained opportunity document under `docs/opportunities/` and can be implemented independently.
115
+
116
+ - [x] **OPP-001** Add a light lane for small, understood work (proportional ceremony). _Acceptance:_ see [OPP-001](../opportunities/OPP-001-proportional-ceremony-light-lane.md); no durable spec/plan artifacts for light tasks, evidence and lane rationale still recorded, explicit escalation to the full lane on scope growth, full lane unchanged.
117
+ - [x] **OPP-002** Add `aiw risk assess` — read-only, content-based risk classification with an exact runnable next command. _Acceptance:_ see [OPP-002](../opportunities/OPP-002-risk-assess-command.md); deterministic and offline by default, no `.aiw/` mutation, escalation raises and never lowers tier, `next_command` executes as printed.
118
+ - [x] **OPP-003** Ship an optional `adversarial-review` package (blind judges + refuter with proof-carrying findings). _Acceptance:_ see [OPP-003](../opportunities/OPP-003-adversarial-review-package.md); severe findings without reproducible evidence degrade to advisory; refutation outcomes are corroborated/refuted/inconclusive with proof references; human acceptance still required.
119
+ - [ ] **OPP-004** Emit typed reason codes and exact continuation commands on every blocking exit. _Acceptance:_ see [OPP-004](../opportunities/OPP-004-exact-continuation-commands.md); every block carries a reason code and either a tested runnable `next_command` or an explicit terminal statement; continuation inventory stays complete in CI.
120
+ - [x] **OPP-005** Add a friction benchmark classifying blocks as in-band / out-of-band / dead-end over packaged-CLI journeys. _Acceptance:_ see [OPP-005](../opportunities/OPP-005-friction-benchmark.md); deterministic classification, continuations actually executed, CI fails on any dead end.
121
+ - [x] **OPP-006** Adopt the emission ≠ usability test policy: messages naming a command must have a test executing it. _Acceptance:_ see [OPP-006](../opportunities/OPP-006-emission-usability-test-policy.md); extraction helper covers emitted and generated command mentions; generated `ai-init` skills' commands are executed in e2e tests.
122
+ - [ ] **007-decision-findings-triage-scope-watch** Decision model — findings-evidence triage and lane scope-watch (advisory-only). _Acceptance:_ see [OPP-007](../opportunities/OPP-007-decision-findings-triage-scope-watch.md); unconfigured = byte-identical offline behavior; advisory flags never block, never lower tiers, never choose lanes.
123
+ - [ ] **008-embedding-covering-tests-judge-context** Embedding retrieval — covering-test detection and judge context scoping. _Acceptance:_ see [OPP-008](../opportunities/OPP-008-embedding-covering-tests-judge-context.md); retrieval only prevents false `untested` flags (absence fails safe toward high); scoped judge prompts labeled as context aids within token ceilings.
124
+
112
125
  ## Backlog rules
113
126
 
114
127
  - [x] **FND-009** Add self-validation workflow. _Acceptance:_ changes affecting AIW behavior execute against this repository and record generated-output evidence.
@@ -0,0 +1,21 @@
1
+ # Continuation Inventory
2
+
3
+ Every blocking `aiw` exit carries a stable reason code plus either an exact runnable continuation or an explicit terminal statement. A message may name a command only when running that command resolves the block it was printed for (OPP-004, [ADR-001](../.context/adrs/ADR-001-odd-rdd-proportionality-scope.md)).
4
+
5
+ - Codes are declared in `src/blocking-reasons.ts` (`BLOCKING_REASON_CODES`); adding a code requires a definition with a continuation producer and a test.
6
+ - With `--json`, blocking exits return `{ "reason_code": ..., "next_command" | "next_action" | "terminal": ... }`.
7
+ - The table below is generated from the registry; a test in `src/blocking-reasons.test.ts` fails CI when this page drifts from the code.
8
+
9
+ | Reason code | Description | Continuation |
10
+ | ------------------------------- | --------------------------------------------------------------------------------------------- | ---------------- |
11
+ | `gate_evidence_missing` | The quality gate is blocked because the required evidence artifact is missing. | explicit action |
12
+ | `gate_incomplete` | The quality gate found completeness issues in the stage artifact. | explicit action |
13
+ | `stage_prerequisite_pending` | A previous stage is awaiting human approval before this stage can start. | runnable command |
14
+ | `stage_prerequisite_unresolved` | Previous stages need human approval or an accepted optional skip before this stage can start. | runnable command |
15
+ | `stage_awaiting_approval` | This stage is awaiting human approval; approve or reject it before continuing. | runnable command |
16
+ | `stage_already_approved` | This stage is already approved; changed evidence requires rerunning its quality gate. | runnable command |
17
+ | `approval_evidence_missing` | Approval cannot be recorded because the stage evidence artifact is missing. | explicit action |
18
+ | `approval_stale` | The artifact changed after review; its approval is stale and the gate must run again. | runnable command |
19
+ | `approval_gate_missing` | No passing quality gate is recorded for this stage; gate it before approving. | runnable command |
20
+ | `lane_escalation_required` | Light-lane workflows do not author stage artifacts; escalate to the full lane to continue. | explicit action |
21
+ | `light_evidence_missing` | A light-lane workflow cannot complete without at least one recorded validation entry. | explicit action |
@@ -40,3 +40,7 @@ Observed behavior on real plans: an evidence-free plan scored `gate: ready` with
40
40
  ## Hard boundary (unchanged)
41
41
 
42
42
  Decision-model output is `inferred` evidence with confidence, never `confirmed`. It can move an artifact to _pending review_, never to _approved_, and cannot replace the fingerprinted human approval ledger. This matches the existing approval contract and keeps the audit trail human-owned.
43
+
44
+ ## Planned integration (OPP-007)
45
+
46
+ The next planned decision-model roles follow the same contracts: findings-evidence triage for severe review findings (does the cited proof support the claim — advisory flag, the refuter still decides) and a lane scope-watch that flags mismatch between a chosen light lane and deterministic risk signals. Both stay advisory-only with the deterministic floor unchanged; unconfigured behavior is byte-identical. See [OPP-007](opportunities/OPP-007-decision-findings-triage-scope-watch.md).
@@ -0,0 +1,62 @@
1
+ # OPP-001: Proportional ceremony — light lane for small, understood work
2
+
3
+ - Status: Implemented (2026-10-30, plan Wave 4)
4
+ - Priority: P2
5
+ - Added: 2026-10-30
6
+ - Decision: [ADR-001](../../.context/adrs/ADR-001-odd-rdd-proportionality-scope.md), Decision A
7
+
8
+ ## Problem
9
+
10
+ Every AI Workflow task, regardless of size or risk, traverses the same staged pipeline (brainstorm → specification → ADR → plan → implementation → verification → review → traceability), each stage producing durable artifacts under `.aiw/generated/`. For small, well-understood changes this generates artifacts nobody reads, and users bypass the workflow manually — which then produces no evidence at all. Gentle-AI retired SDD for exactly this reason; the market signal is that uniform ceremony causes abandonment in daily use.
11
+
12
+ ## Source concept (ODD)
13
+
14
+ ODD classifies task size with the **resume test**: work is _small_ when it is understood, risk is contained, and the work could be resumed from the original request plus `git diff`. Small work creates **no durable task artifacts**; substantial work creates exactly one feature document before the first source write. ODD also holds that findings never silently expand authorization, and scope growth is an explicit event.
15
+
16
+ References:
17
+
18
+ - Gentle-AI `docs/usage.md`, section "Organic Driven Development (ODD)" — protocol steps 4 (Classify) and 5 (Track before the first write)
19
+ - Gentle-AI `docs/intended-usage.md` — "Small, understood changes need no persistent task artifact"
20
+
21
+ ## Proposal
22
+
23
+ Add a **light lane** alongside the existing full SDD lane:
24
+
25
+ 1. New flag `aiw task start --lane=light` (full lane remains the default).
26
+ 2. Entry criteria, evaluated and recorded at start — all must hold:
27
+ - the change is understood well enough to resume from the request plus `git diff` (the ODD resume test, stated verbatim in the command output so the human makes the call);
28
+ - expected blast radius is contained (no new requirement IDs, no architecture decision needed);
29
+ - TDD applies or an exception is recorded with reason.
30
+ 3. Light-lane behavior:
31
+ - No durable spec/plan/report artifacts are scaffolded.
32
+ - Evidence is still required: task validation commands run and completion evidence is recorded in `.aiw/checkpoints/` (compact form, linked to the ticket if present).
33
+ - The lane decision and its entry criteria are persisted, so audits can see why the light lane was used.
34
+ 4. Escalation: if scope grows beyond the entry criteria (new requirement, ADR needed, risk found), the task must escalate to the full lane; the escalation is an explicit recorded event, never a silent expansion. Escalation preserves already-recorded evidence.
35
+
36
+ ## Affected areas
37
+
38
+ - `src/workflow.ts`, `src/types.ts` (lane state, escalation event)
39
+ - CLI: task/workflow commands (`src/cli.ts`)
40
+ - Stage skills and templates rendered through adapters (resources that describe the SDD sequence)
41
+ - `docs/README.md` (main README workflow section), `docs/architecture.md` (approval contract description)
42
+ - Checkpoint/evidence recording (`src/evidence.ts`, `src/human-approval.ts` interactions)
43
+
44
+ ## Acceptance criteria
45
+
46
+ - Given a task started with `--lane=light`, when no scope growth occurs, then completion requires no `specification.md`/`implementation-plan.md` artifacts and records validation evidence plus the lane rationale in checkpoints.
47
+ - Given a light-lane task, when scope growth is detected (or declared), then escalation to the full lane is required before further stage commands run, and prior evidence is preserved and linked.
48
+ - Given the full lane (default), when a task starts, then behavior is unchanged from today.
49
+ - Given a light-lane completion, when `.aiw/approvals.yml` is inspected, then the ledger shows the light-lane rationale, not a missing approval.
50
+ - Non-interactive runs without an explicit lane default to the full lane.
51
+
52
+ ## Non-goals
53
+
54
+ - No global "workflow off" switch (see ADR-001 Decision B).
55
+ - No change to full-lane stage order, gates, or approval semantics.
56
+ - No automatic lane detection that overrides the human's classification; automation may only suggest.
57
+
58
+ ## Open questions
59
+
60
+ - Should the light lane allow skipping the brainstorm/spec stages only, or also merge implementation + verification evidence into a single checkpoint artifact?
61
+ - Should escalation be self-declared by the agent, human-confirmed, or both (recommend both: agent proposes, human confirms)?
62
+ - Does the lane need representation in `traceability` output (e.g., a light-lane marker on the ticket)?