@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.
- package/CHANGELOG.md +34 -0
- package/README.md +12 -1
- package/dist/blocking-reasons.d.ts +67 -0
- package/dist/blocking-reasons.js +199 -0
- package/dist/command-evidence.d.ts +32 -0
- package/dist/command-evidence.js +61 -0
- package/dist/human-approval.d.ts +19 -2
- package/dist/human-approval.js +82 -9
- package/dist/recommendations.js +16 -1
- package/dist/risk-assess.d.ts +50 -0
- package/dist/risk-assess.js +273 -0
- package/dist/workflow.js +280 -30
- package/docs/README.md +2 -0
- package/docs/architecture.md +50 -0
- package/docs/backlog.md +13 -0
- package/docs/continuation-inventory.md +24 -0
- package/docs/decision-model-evaluation.md +4 -0
- package/docs/opportunities/OPP-001-proportional-ceremony-light-lane.md +62 -0
- package/docs/opportunities/OPP-002-risk-assess-command.md +70 -0
- package/docs/opportunities/OPP-003-adversarial-review-package.md +63 -0
- package/docs/opportunities/OPP-004-exact-continuation-commands.md +59 -0
- package/docs/opportunities/OPP-005-friction-benchmark.md +62 -0
- package/docs/opportunities/OPP-006-emission-usability-test-policy.md +57 -0
- package/docs/opportunities/OPP-007-decision-findings-triage-scope-watch.md +54 -0
- package/docs/opportunities/OPP-008-embedding-covering-tests-judge-context.md +52 -0
- package/docs/opportunities/README.md +33 -0
- package/docs/product-requirements.md +3 -0
- package/docs/product-vision.md +1 -0
- package/docs/quality.md +10 -0
- package/docs/release-process.md +9 -1
- package/docs/semantic-mapping.md +4 -0
- package/package.json +1 -1
- package/resources/package.yaml +3 -0
- package/resources/skills/code-review.md +2 -0
- package/resources/skills/review-judge-a.md +24 -0
- package/resources/skills/review-judge-b.md +24 -0
- package/resources/skills/review-refuter.md +28 -0
- 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";
|
|
@@ -76,10 +79,9 @@ function beginWorkflowSession(fs, aiw, options) {
|
|
|
76
79
|
if (fs.exists(sessionPath)) {
|
|
77
80
|
const previous = parseWorkflowSession(fs.read(sessionPath));
|
|
78
81
|
if (previous.status === "active")
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
};
|
|
82
|
+
throw new BlockingError(describeBlockingReason("workflow_still_active", {
|
|
83
|
+
detail: `Workflow ${previous.workflowId} is still active. Complete it before starting another.`,
|
|
84
|
+
}));
|
|
83
85
|
if (!fs.exists(ledgerPath))
|
|
84
86
|
return { exitCode: 1, error: "Completed workflow is missing its approval ledger." };
|
|
85
87
|
const archive = join(aiw, `checkpoints/workflow-${previous.workflowId}-approvals.yml`);
|
|
@@ -110,8 +112,15 @@ function beginWorkflowSession(fs, aiw, options) {
|
|
|
110
112
|
updatedAt: now,
|
|
111
113
|
...(options.ticket ? { ticket: options.ticket } : {}),
|
|
112
114
|
...(options.title ? { title: options.title } : {}),
|
|
115
|
+
...(options.lane === "light" && options.laneReason
|
|
116
|
+
? { lane: "light", laneReason: options.laneReason }
|
|
117
|
+
: {}),
|
|
113
118
|
};
|
|
114
|
-
const ledger = createApprovalLedger(workflowId
|
|
119
|
+
const ledger = createApprovalLedger(workflowId, {
|
|
120
|
+
...(options.lane === "light" && options.laneReason
|
|
121
|
+
? { lane: "light", laneReason: options.laneReason }
|
|
122
|
+
: {}),
|
|
123
|
+
});
|
|
115
124
|
fs.write(ledgerPath, serializeApprovalLedger(ledger));
|
|
116
125
|
fs.write(sessionPath, serializeWorkflowSession(session));
|
|
117
126
|
return { exitCode: 0, output: `Workflow started: ${workflowId}.` };
|
|
@@ -124,14 +133,35 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
124
133
|
if (command === "task") {
|
|
125
134
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
126
135
|
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";
|
|
136
|
+
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
137
|
const action = args[1];
|
|
129
138
|
const sessionPath = join(aiw, "workflow.yml");
|
|
130
139
|
if (action === "start") {
|
|
131
|
-
const options = parseNamedOptions(args.slice(2), [
|
|
140
|
+
const options = parseNamedOptions(args.slice(2), [
|
|
141
|
+
"ticket",
|
|
142
|
+
"title",
|
|
143
|
+
"id",
|
|
144
|
+
"lane",
|
|
145
|
+
"lane-reason",
|
|
146
|
+
]);
|
|
132
147
|
if (!options?.ticket)
|
|
133
148
|
return { exitCode: 1, error: usage };
|
|
134
149
|
validateTaskTicket(options.ticket);
|
|
150
|
+
if (options.lane !== undefined && options.lane !== "light")
|
|
151
|
+
return { exitCode: 1, error: "Only --lane=light exists; omit it for the full lane." };
|
|
152
|
+
const laneReason = options["lane-reason"];
|
|
153
|
+
if (options.lane === "light") {
|
|
154
|
+
if (!laneReason)
|
|
155
|
+
return {
|
|
156
|
+
exitCode: 1,
|
|
157
|
+
error: "--lane=light requires --lane-reason: the resume-test call, recorded for audit.",
|
|
158
|
+
};
|
|
159
|
+
if (laneReason.length > 500 || /[\r\n]/.test(laneReason))
|
|
160
|
+
return {
|
|
161
|
+
exitCode: 1,
|
|
162
|
+
error: "--lane-reason must be a single line of at most 500 bytes.",
|
|
163
|
+
};
|
|
164
|
+
}
|
|
135
165
|
if (options.id !== undefined) {
|
|
136
166
|
try {
|
|
137
167
|
assertWorkflowId(options.id);
|
|
@@ -140,20 +170,104 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
140
170
|
return { exitCode: 1, error: "Workflow ID must be a UUID." };
|
|
141
171
|
}
|
|
142
172
|
}
|
|
173
|
+
const lane = options.lane === "light" ? "light" : undefined;
|
|
143
174
|
const result = beginWorkflowSession(fs, aiw, {
|
|
144
175
|
...(options.id ? { id: options.id } : {}),
|
|
145
176
|
ticket: options.ticket,
|
|
146
177
|
...(options.title ? { title: options.title } : {}),
|
|
178
|
+
...(lane ? { lane, laneReason: laneReason } : {}),
|
|
147
179
|
});
|
|
148
180
|
if (result.exitCode !== 0)
|
|
149
181
|
return result;
|
|
150
182
|
const title = options.title ? ` titled "${options.title}"` : "";
|
|
183
|
+
if (lane)
|
|
184
|
+
return {
|
|
185
|
+
exitCode: 0,
|
|
186
|
+
output: `Task started for ticket ${options.ticket}${title} (light lane): ${result.output}\n` +
|
|
187
|
+
"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" +
|
|
188
|
+
`Rationale recorded: ${laneReason}\n` +
|
|
189
|
+
'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=...`.',
|
|
190
|
+
};
|
|
151
191
|
return {
|
|
152
192
|
exitCode: 0,
|
|
153
193
|
output: `Task started for ticket ${options.ticket}${title}: ${result.output}\n` +
|
|
154
194
|
`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
195
|
};
|
|
156
196
|
}
|
|
197
|
+
if (action === "escalate") {
|
|
198
|
+
const options = parseNamedOptions(args.slice(2), ["reason"]);
|
|
199
|
+
if (!options?.reason)
|
|
200
|
+
return { exitCode: 1, error: "Usage: aiw task escalate --reason=why-scope-grew" };
|
|
201
|
+
if (!fs.exists(sessionPath))
|
|
202
|
+
return { exitCode: 1, error: "No SDD workflow is active. Start one first." };
|
|
203
|
+
const { session } = readWorkflowState(aiw, fs);
|
|
204
|
+
if (session.lane !== "light")
|
|
205
|
+
return {
|
|
206
|
+
exitCode: 1,
|
|
207
|
+
error: "Only light-lane workflows escalate; this one already runs the full lane.",
|
|
208
|
+
};
|
|
209
|
+
if (session.laneEscalatedAt)
|
|
210
|
+
return {
|
|
211
|
+
exitCode: 1,
|
|
212
|
+
error: `Workflow ${session.workflowId} already escalated to the full lane.`,
|
|
213
|
+
};
|
|
214
|
+
const now = new Date().toISOString();
|
|
215
|
+
fs.write(sessionPath, serializeWorkflowSession({
|
|
216
|
+
...session,
|
|
217
|
+
laneEscalatedAt: now,
|
|
218
|
+
laneEscalationReason: options.reason,
|
|
219
|
+
updatedAt: now,
|
|
220
|
+
}));
|
|
221
|
+
const evidencePath = join(aiw, `checkpoints/task-light-${session.workflowId}.yml`);
|
|
222
|
+
return {
|
|
223
|
+
exitCode: 0,
|
|
224
|
+
output: `Workflow ${session.workflowId} escalated to the full lane.\n` +
|
|
225
|
+
(fs.exists(evidencePath)
|
|
226
|
+
? `Light-lane evidence preserved and linked: ${evidencePath}\n`
|
|
227
|
+
: "No light-lane evidence had been recorded yet.\n") +
|
|
228
|
+
"Stages now run the full approval sequence from brainstorming.",
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
if (action === "evidence") {
|
|
232
|
+
const options = parseNamedOptions(args.slice(2), ["validation"]);
|
|
233
|
+
if (!options?.validation)
|
|
234
|
+
return {
|
|
235
|
+
exitCode: 1,
|
|
236
|
+
error: 'Usage: aiw task evidence --validation="<command and its observed result>"',
|
|
237
|
+
};
|
|
238
|
+
if (!fs.exists(sessionPath))
|
|
239
|
+
return { exitCode: 1, error: "No SDD workflow is active. Start one first." };
|
|
240
|
+
const { session } = readWorkflowState(aiw, fs);
|
|
241
|
+
if (effectiveLane(session) !== "light")
|
|
242
|
+
return {
|
|
243
|
+
exitCode: 1,
|
|
244
|
+
error: "Evidence entries are a light-lane command; full-lane workflows record stage artifacts and gates.",
|
|
245
|
+
};
|
|
246
|
+
const evidencePath = join(aiw, `checkpoints/task-light-${session.workflowId}.yml`);
|
|
247
|
+
const existing = fs.exists(evidencePath)
|
|
248
|
+
? parseLightEvidence(fs.read(evidencePath))
|
|
249
|
+
: {
|
|
250
|
+
schema: 1,
|
|
251
|
+
workflowId: session.workflowId,
|
|
252
|
+
...(session.ticket ? { ticket: session.ticket } : {}),
|
|
253
|
+
lane: "light",
|
|
254
|
+
...(session.laneReason ? { laneReason: session.laneReason } : {}),
|
|
255
|
+
entries: [],
|
|
256
|
+
};
|
|
257
|
+
const updated = {
|
|
258
|
+
...existing,
|
|
259
|
+
entries: [
|
|
260
|
+
...existing.entries,
|
|
261
|
+
{ note: options.validation, recordedAt: new Date().toISOString() },
|
|
262
|
+
],
|
|
263
|
+
};
|
|
264
|
+
fs.mkdir(join(aiw, "checkpoints"));
|
|
265
|
+
fs.write(evidencePath, stringify({ ...updated }, { lineWidth: 0 }));
|
|
266
|
+
return {
|
|
267
|
+
exitCode: 0,
|
|
268
|
+
output: `Validation evidence recorded (${updated.entries.length} entr${updated.entries.length === 1 ? "y" : "ies"}): ${options.validation}`,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
157
271
|
if (action === "status") {
|
|
158
272
|
if (!fs.exists(sessionPath))
|
|
159
273
|
return {
|
|
@@ -161,14 +275,24 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
161
275
|
output: "No SDD workflow is active. Start one with `aiw task start --ticket=TEAM-123`.",
|
|
162
276
|
};
|
|
163
277
|
const { session, ledger } = readWorkflowState(aiw, fs, false);
|
|
278
|
+
const lane = session.lane === "light"
|
|
279
|
+
? [
|
|
280
|
+
`Lane: light — ${session.laneEscalatedAt ? `escalated to the full lane (${session.laneEscalationReason})` : session.laneReason}`,
|
|
281
|
+
]
|
|
282
|
+
: [];
|
|
164
283
|
const lines = [
|
|
165
284
|
...(session.ticket
|
|
166
285
|
? [`Ticket: ${session.ticket}${session.title ? ` — ${session.title}` : ""}`]
|
|
167
286
|
: []),
|
|
287
|
+
...lane,
|
|
168
288
|
`Workflow: ${session.workflowId} (${session.status})`,
|
|
169
289
|
"Stages:",
|
|
170
290
|
...APPROVAL_STAGES.map((stage) => `- ${stage}: ${ledger.stages[stage]?.status ?? "not-started"}`),
|
|
171
291
|
];
|
|
292
|
+
if (session.status === "active" && effectiveLane(session) === "light") {
|
|
293
|
+
lines.push('Next: record validation with `aiw task evidence --validation="<command and result>"`, then `aiw workflow complete`. If scope grows: `aiw task escalate --reason=...`.');
|
|
294
|
+
return { exitCode: 0, output: lines.join("\n") };
|
|
295
|
+
}
|
|
172
296
|
const next = APPROVAL_STAGES.find((stage) => (ledger.stages[stage]?.status ?? "not-started") === "not-started");
|
|
173
297
|
if (session.status === "active" && next)
|
|
174
298
|
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)`}.`);
|
|
@@ -182,7 +306,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
182
306
|
const action = args[1];
|
|
183
307
|
const sessionPath = join(aiw, "workflow.yml");
|
|
184
308
|
if (action === "start") {
|
|
185
|
-
const options = parseNamedOptions(args.slice(2), ["id"]);
|
|
309
|
+
const options = parseNamedOptions(args.slice(2).filter((argument) => argument !== "--json"), ["id"]);
|
|
186
310
|
if (!options)
|
|
187
311
|
return { exitCode: 1, error: "Usage: aiw workflow start [--id=workflow-id]" };
|
|
188
312
|
return beginWorkflowSession(fs, aiw, { id: options.id });
|
|
@@ -197,8 +321,67 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
197
321
|
output: `Workflow ${session.workflowId} (${session.status})\n${stages.join("\n")}\n${workflowNextAction(ledger)}`,
|
|
198
322
|
};
|
|
199
323
|
}
|
|
324
|
+
if (action === "abandon") {
|
|
325
|
+
const options = parseNamedOptions(args.slice(2), ["reason"]);
|
|
326
|
+
if (!options?.reason)
|
|
327
|
+
return { exitCode: 1, error: "Usage: aiw workflow abandon --reason=why-obsolete" };
|
|
328
|
+
if (options.reason.length > 500 || /[\r\n]/.test(options.reason))
|
|
329
|
+
return {
|
|
330
|
+
exitCode: 1,
|
|
331
|
+
error: "--reason must be a single line of at most 500 bytes.",
|
|
332
|
+
};
|
|
333
|
+
if (!fs.exists(sessionPath))
|
|
334
|
+
return {
|
|
335
|
+
exitCode: 1,
|
|
336
|
+
error: "No SDD workflow is active; start one with `aiw workflow start` or `aiw task start --ticket=TEAM-123`. [reason: workflow_not_active] Terminal: no automatic continuation beyond starting a workflow.",
|
|
337
|
+
};
|
|
338
|
+
const { session } = readActiveWorkflow(aiw, fs);
|
|
339
|
+
const ledgerPath = join(aiw, "approvals.yml");
|
|
340
|
+
const sessionArchive = join(aiw, "checkpoints", `workflow-${session.workflowId}-session.yml`);
|
|
341
|
+
const ledgerArchive = join(aiw, "checkpoints", `workflow-${session.workflowId}-approvals.yml`);
|
|
342
|
+
fs.mkdir(join(aiw, "checkpoints"));
|
|
343
|
+
const now = new Date().toISOString();
|
|
344
|
+
const archivedSession = {
|
|
345
|
+
...session,
|
|
346
|
+
status: "abandoned",
|
|
347
|
+
abandonedReason: options.reason,
|
|
348
|
+
updatedAt: now,
|
|
349
|
+
};
|
|
350
|
+
fs.write(sessionArchive, serializeWorkflowSession(archivedSession));
|
|
351
|
+
if (fs.exists(ledgerPath))
|
|
352
|
+
fs.write(ledgerArchive, fs.read(ledgerPath));
|
|
353
|
+
fs.remove?.(sessionPath);
|
|
354
|
+
if (fs.exists(ledgerPath))
|
|
355
|
+
fs.remove?.(ledgerPath);
|
|
356
|
+
return {
|
|
357
|
+
exitCode: 0,
|
|
358
|
+
output: `Workflow ${session.workflowId} abandoned (${options.reason}).\n` +
|
|
359
|
+
`Session archived: ${sessionArchive}\n` +
|
|
360
|
+
(fs.exists(ledgerArchive) ? `Approval ledger archived: ${ledgerArchive}\n` : "") +
|
|
361
|
+
"Start a new one when ready.",
|
|
362
|
+
};
|
|
363
|
+
}
|
|
200
364
|
if (action === "complete") {
|
|
201
365
|
const { session, ledger } = readActiveWorkflow(aiw, fs);
|
|
366
|
+
if (effectiveLane(session) === "light") {
|
|
367
|
+
const evidencePath = join(aiw, `checkpoints/task-light-${session.workflowId}.yml`);
|
|
368
|
+
const hasEntries = fs.exists(evidencePath)
|
|
369
|
+
? parseLightEvidence(fs.read(evidencePath)).entries.length > 0
|
|
370
|
+
: false;
|
|
371
|
+
if (!hasEntries)
|
|
372
|
+
throw new BlockingError(describeBlockingReason("light_evidence_missing", {
|
|
373
|
+
detail: `Light-lane workflow ${session.workflowId} cannot complete without recorded validation evidence.`,
|
|
374
|
+
}));
|
|
375
|
+
fs.write(sessionPath, serializeWorkflowSession({
|
|
376
|
+
...session,
|
|
377
|
+
status: "complete",
|
|
378
|
+
updatedAt: new Date().toISOString(),
|
|
379
|
+
}));
|
|
380
|
+
return {
|
|
381
|
+
exitCode: 0,
|
|
382
|
+
output: `Workflow completed (light lane): ${session.workflowId}. Evidence: ${evidencePath}`,
|
|
383
|
+
};
|
|
384
|
+
}
|
|
202
385
|
const unresolved = APPROVAL_STAGES.filter((stage) => ledger.stages[stage]?.status !== "approved" &&
|
|
203
386
|
ledger.stages[stage]?.status !== "skipped");
|
|
204
387
|
if (unresolved.length)
|
|
@@ -222,10 +405,15 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
222
405
|
const stage = args[1];
|
|
223
406
|
if (!stage || !APPROVAL_STAGES.includes(stage))
|
|
224
407
|
return { exitCode: 1, error: `Usage: aiw ${command} <workflow-stage> [options]` };
|
|
225
|
-
const
|
|
408
|
+
const rest = args.slice(2).filter((argument) => argument !== "--json");
|
|
409
|
+
const { session, ledger } = readActiveWorkflow(aiw, fs);
|
|
410
|
+
if (effectiveLane(session) === "light")
|
|
411
|
+
throw new BlockingError(describeBlockingReason("lane_escalation_required", {
|
|
412
|
+
detail: `Light-lane workflow ${session.workflowId} does not approve stages; escalate to the full lane to continue.`,
|
|
413
|
+
}));
|
|
226
414
|
const workflowStage = stage;
|
|
227
415
|
if (command === "skip") {
|
|
228
|
-
const options = parseNamedOptions(
|
|
416
|
+
const options = parseNamedOptions(rest, ["reason"]);
|
|
229
417
|
if (!options?.reason)
|
|
230
418
|
return {
|
|
231
419
|
exitCode: 1,
|
|
@@ -241,19 +429,28 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
241
429
|
}
|
|
242
430
|
const artifact = approvalArtifactPath(aiw, workflowStage);
|
|
243
431
|
if (!artifact || !fs.exists(artifact))
|
|
244
|
-
|
|
432
|
+
throw new BlockingError(describeBlockingReason("approval_evidence_missing", {
|
|
433
|
+
stage: workflowStage,
|
|
434
|
+
artifactPath: artifact ?? undefined,
|
|
435
|
+
detail: `Approval evidence is missing for ${workflowStage}.`,
|
|
436
|
+
}));
|
|
245
437
|
const artifactHash = stageFingerprint(fs.read(artifact));
|
|
246
438
|
if (command === "approve") {
|
|
247
439
|
if (ledger.stages[workflowStage]?.artifactHash !== artifactHash) {
|
|
248
440
|
const invalidated = invalidateStageAndDescendants(ledger, workflowStage, new Date().toISOString());
|
|
249
441
|
fs.write(join(aiw, "approvals.yml"), serializeApprovalLedger(invalidated));
|
|
250
442
|
const existing = ledger.stages[workflowStage];
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
: `
|
|
256
|
-
|
|
443
|
+
if (existing)
|
|
444
|
+
throw new BlockingError(describeBlockingReason("approval_stale", {
|
|
445
|
+
stage: workflowStage,
|
|
446
|
+
artifactPath: artifact,
|
|
447
|
+
detail: `The ${workflowStage} artifact changed after review; its approval is stale. Rerun the quality gate and request approval again.`,
|
|
448
|
+
}));
|
|
449
|
+
throw new BlockingError(describeBlockingReason("approval_gate_missing", {
|
|
450
|
+
stage: workflowStage,
|
|
451
|
+
artifactPath: artifact,
|
|
452
|
+
detail: `No passing quality gate recorded for ${workflowStage}. Run aiw gate ${workflowStage} before approving.`,
|
|
453
|
+
}));
|
|
257
454
|
}
|
|
258
455
|
const updated = approveStage(ledger, workflowStage, {
|
|
259
456
|
artifactHash,
|
|
@@ -262,7 +459,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
262
459
|
fs.write(join(aiw, "approvals.yml"), serializeApprovalLedger(updated));
|
|
263
460
|
return { exitCode: 0, output: `Human approval recorded for ${workflowStage}.` };
|
|
264
461
|
}
|
|
265
|
-
const options = parseNamedOptions(
|
|
462
|
+
const options = parseNamedOptions(rest, ["reason"]);
|
|
266
463
|
if (!options?.reason)
|
|
267
464
|
return { exitCode: 1, error: "Usage: aiw reject <stage> --reason=human-feedback" };
|
|
268
465
|
const updated = rejectStage(ledger, workflowStage, options.reason, new Date().toISOString());
|
|
@@ -1749,6 +1946,11 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
1749
1946
|
fs.write(path, serialized);
|
|
1750
1947
|
return { exitCode: 0, output: `Traceability graph created: ${path}` };
|
|
1751
1948
|
}
|
|
1949
|
+
if (command === "risk") {
|
|
1950
|
+
if (args[1] !== "assess")
|
|
1951
|
+
return { exitCode: 1, error: "Usage: aiw risk assess [--base-ref=ref] [--json]" };
|
|
1952
|
+
return runRiskAssess(args.slice(2), root, fs);
|
|
1953
|
+
}
|
|
1752
1954
|
if (command === "gate") {
|
|
1753
1955
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
1754
1956
|
return { exitCode: 1, error: "Run `aiw install` first." };
|
|
@@ -1762,15 +1964,21 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
1762
1964
|
assertStageReady(aiw, fs, workflowStage);
|
|
1763
1965
|
const artifactPath = approvalArtifactPath(aiw, workflowStage);
|
|
1764
1966
|
if (!artifactPath || !fs.exists(artifactPath))
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1967
|
+
throw new BlockingError(describeBlockingReason("gate_evidence_missing", {
|
|
1968
|
+
stage,
|
|
1969
|
+
artifactPath: artifactPath ?? undefined,
|
|
1970
|
+
detail: `Quality gate blocked: required evidence is missing for ${stage}.`,
|
|
1971
|
+
}));
|
|
1769
1972
|
const hasQualityGate = stageDefinition(workflowStage).requiresQualityGate;
|
|
1770
1973
|
if (hasQualityGate) {
|
|
1771
1974
|
const issues = evaluateQualityGate(stage, fs.read(artifactPath));
|
|
1772
1975
|
if (issues.length)
|
|
1773
|
-
|
|
1976
|
+
throw new BlockingError(describeBlockingReason("gate_incomplete", {
|
|
1977
|
+
stage,
|
|
1978
|
+
artifactPath,
|
|
1979
|
+
gateIssues: issues,
|
|
1980
|
+
detail: `Quality gate blocked: ${issues.join(" ")}`,
|
|
1981
|
+
}));
|
|
1774
1982
|
}
|
|
1775
1983
|
const { ledger } = readActiveWorkflow(aiw, fs);
|
|
1776
1984
|
const updated = beginStageApproval(ledger, workflowStage, {
|
|
@@ -2519,6 +2727,9 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
2519
2727
|
};
|
|
2520
2728
|
}
|
|
2521
2729
|
catch (error) {
|
|
2730
|
+
if (error instanceof BlockingError && args.includes("--json")) {
|
|
2731
|
+
return { exitCode: 1, output: JSON.stringify(blockingResultToJson(error.result)) };
|
|
2732
|
+
}
|
|
2522
2733
|
return { exitCode: 1, error: error instanceof Error ? error.message : String(error) };
|
|
2523
2734
|
}
|
|
2524
2735
|
}
|
|
@@ -2532,7 +2743,9 @@ function readWorkflowState(aiw, fs, requireActive = true) {
|
|
|
2532
2743
|
if (session.workflowId !== ledger.workflowId)
|
|
2533
2744
|
throw new Error("Workflow session and approval ledger IDs do not match; run doctor.");
|
|
2534
2745
|
if (requireActive && session.status !== "active")
|
|
2535
|
-
throw new
|
|
2746
|
+
throw new BlockingError(describeBlockingReason("workflow_complete", {
|
|
2747
|
+
detail: `Workflow ${session.workflowId} is complete; start a new workflow first.`,
|
|
2748
|
+
}));
|
|
2536
2749
|
const changed = APPROVAL_STAGES.find((stage) => {
|
|
2537
2750
|
const record = ledger.stages[stage];
|
|
2538
2751
|
if (!record?.artifactHash || !["pending-human", "approved"].includes(record.status))
|
|
@@ -2561,20 +2774,57 @@ function workflowNextAction(ledger) {
|
|
|
2561
2774
|
return `Next action: revise ${next}, rerun its gate, and request human approval.`;
|
|
2562
2775
|
return `Next action: prepare ${next} evidence.`;
|
|
2563
2776
|
}
|
|
2777
|
+
function parseLightEvidence(content) {
|
|
2778
|
+
const value = parseYaml(content);
|
|
2779
|
+
if (!value || typeof value !== "object" || value.schema !== 1)
|
|
2780
|
+
throw new Error("Light-lane evidence checkpoint must be schema version 1.");
|
|
2781
|
+
const record = value;
|
|
2782
|
+
const entries = Array.isArray(record.entries) ? record.entries : [];
|
|
2783
|
+
return {
|
|
2784
|
+
schema: 1,
|
|
2785
|
+
workflowId: String(record.workflowId ?? ""),
|
|
2786
|
+
...(record.ticket !== undefined ? { ticket: String(record.ticket) } : {}),
|
|
2787
|
+
lane: "light",
|
|
2788
|
+
...(record.laneReason !== undefined ? { laneReason: String(record.laneReason) } : {}),
|
|
2789
|
+
entries: entries.map((entry) => ({
|
|
2790
|
+
note: String(entry.note ?? ""),
|
|
2791
|
+
recordedAt: String(entry.recordedAt ?? ""),
|
|
2792
|
+
})),
|
|
2793
|
+
};
|
|
2794
|
+
}
|
|
2564
2795
|
function assertStageReady(aiw, fs, stage) {
|
|
2565
|
-
const { ledger } = readActiveWorkflow(aiw, fs);
|
|
2796
|
+
const { session, ledger } = readActiveWorkflow(aiw, fs);
|
|
2797
|
+
if (effectiveLane(session) === "light")
|
|
2798
|
+
throw new BlockingError(describeBlockingReason("lane_escalation_required", {
|
|
2799
|
+
stage,
|
|
2800
|
+
detail: `Light-lane workflow ${session.workflowId} does not author stage artifacts; escalate to the full lane to continue.`,
|
|
2801
|
+
}));
|
|
2566
2802
|
const unresolved = getUnresolvedPrerequisites(ledger, stage);
|
|
2567
2803
|
if (unresolved.length) {
|
|
2568
2804
|
const pending = unresolved.filter((prerequisite) => ledger.stages[prerequisite]?.status === "pending-human");
|
|
2569
2805
|
if (pending.length)
|
|
2570
|
-
throw new
|
|
2571
|
-
|
|
2806
|
+
throw new BlockingError(describeBlockingReason("stage_prerequisite_pending", {
|
|
2807
|
+
stage,
|
|
2808
|
+
pendingStages: pending,
|
|
2809
|
+
detail: `Cannot enter ${stage}; previous stage(s) are awaiting human approval: ${pending.join(", ")}.`,
|
|
2810
|
+
}));
|
|
2811
|
+
throw new BlockingError(describeBlockingReason("stage_prerequisite_unresolved", {
|
|
2812
|
+
stage,
|
|
2813
|
+
unresolvedStages: unresolved,
|
|
2814
|
+
detail: `Cannot enter ${stage}; previous stages require human approval or an accepted optional skip: ${unresolved.join(", ")}.`,
|
|
2815
|
+
}));
|
|
2572
2816
|
}
|
|
2573
2817
|
const current = ledger.stages[stage];
|
|
2574
2818
|
if (current?.status === "pending-human")
|
|
2575
|
-
throw new
|
|
2819
|
+
throw new BlockingError(describeBlockingReason("stage_awaiting_approval", {
|
|
2820
|
+
stage,
|
|
2821
|
+
detail: `${stage} is awaiting human approval. Approve or reject it before continuing.`,
|
|
2822
|
+
}));
|
|
2576
2823
|
if (current?.status === "approved")
|
|
2577
|
-
throw new
|
|
2824
|
+
throw new BlockingError(describeBlockingReason("stage_already_approved", {
|
|
2825
|
+
stage,
|
|
2826
|
+
detail: `${stage} is already approved. Evidence changes require rerunning its gate.`,
|
|
2827
|
+
}));
|
|
2578
2828
|
}
|
|
2579
2829
|
function approvalArtifactPath(aiw, stage) {
|
|
2580
2830
|
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.
|
package/docs/architecture.md
CHANGED
|
@@ -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. An obsolete active workflow is archived with `aiw workflow abandon --reason=...`: session and ledger move under `.aiw/checkpoints/` with `status: abandoned`, freeing the active slot.
|
|
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,24 @@
|
|
|
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 |
|
|
22
|
+
| `workflow_still_active` | A workflow is already active; finish or archive it before starting another. | explicit action |
|
|
23
|
+
| `workflow_not_active` | No SDD workflow is active for this command to operate on. | runnable command |
|
|
24
|
+
| `workflow_complete` | This workflow is already complete; start a new workflow to continue working. | runnable command |
|
|
@@ -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).
|