@multileaf/ai-workflow 0.10.0 → 0.12.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 +29 -0
- package/dist/blocking-reasons.d.ts +1 -1
- package/dist/blocking-reasons.js +42 -0
- package/dist/digest.d.ts +48 -0
- package/dist/digest.js +106 -0
- package/dist/eval/artifact-evals.d.ts +42 -0
- package/dist/eval/artifact-evals.js +141 -0
- package/dist/generator.js +1 -1
- package/dist/human-approval.d.ts +16 -3
- package/dist/human-approval.js +56 -12
- package/dist/orchestration.d.ts +38 -1
- package/dist/orchestration.js +82 -3
- package/dist/quality-gates.d.ts +1 -1
- package/dist/quality-gates.js +23 -0
- package/dist/risk-assess.d.ts +4 -0
- package/dist/risk-assess.js +5 -1
- package/dist/standards.d.ts +34 -0
- package/dist/standards.js +159 -0
- package/dist/types.d.ts +15 -1
- package/dist/types.js +24 -0
- package/dist/workflow.d.ts +7 -0
- package/dist/workflow.js +472 -62
- package/docs/architecture.md +1 -1
- package/docs/backlog.md +10 -3
- package/docs/continuation-inventory.md +20 -13
- package/docs/opportunities/OPP-009-process-entry-points.md +56 -0
- package/docs/opportunities/OPP-010-orchestration-worktree-isolation.md +52 -0
- package/docs/opportunities/OPP-011-evals-redteam-generated-artifacts.md +56 -0
- package/docs/opportunities/OPP-012-standards-discovery.md +51 -0
- package/docs/opportunities/OPP-013-read-optimized-artifacts-digest-layer.md +52 -0
- package/docs/opportunities/OPP-014-paired-verification-contract.md +54 -0
- package/docs/opportunities/OPP-015-token-recovery-governance.md +52 -0
- package/docs/opportunities/README.md +25 -10
- package/package.json +1 -1
- package/resources/skills/implementation-planning.md +1 -1
- package/resources/templates/brainstorm.md +0 -1
- package/resources/templates/decision-record.md +0 -1
- package/resources/templates/implementation-evidence.md +1 -1
- package/resources/templates/implementation-plan.md +2 -1
- package/resources/templates/migration-report.md +0 -1
- package/resources/templates/security-review.md +0 -1
- package/resources/templates/task.md +0 -1
- package/resources/templates/technical-design.md +0 -1
- package/resources/templates/test-plan.md +0 -1
- package/resources/templates/verification-report.md +2 -1
package/dist/workflow.js
CHANGED
|
@@ -50,7 +50,7 @@ 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, effectiveLane, } from "./human-approval.js";
|
|
53
|
+
import { APPROVAL_STAGES, PROCESS_IDS, processStages, isWorkflowProcess, assertWorkflowId, validateTaskTicket, approveStage, beginStageApproval, createApprovalLedger, getUnresolvedPrerequisites, invalidateStageAndDescendants, parseApprovalLedger, parseWorkflowSession, rejectStage, serializeApprovalLedger, serializeWorkflowSession, skipOptionalStage, stageDefinition, stageFingerprint, effectiveLane, } from "./human-approval.js";
|
|
54
54
|
import { parse as parseYaml, stringify } from "yaml";
|
|
55
55
|
import { measureContextQuality, serializeContextQuality } from "./context-quality.js";
|
|
56
56
|
import { collectContextBench, compareContextBench, parseContextBench, serializeContextBench, } from "./context-benchmark.js";
|
|
@@ -65,26 +65,55 @@ import { parseTraceabilityGraph } from "./traceability.js";
|
|
|
65
65
|
import { serializeAdapterCapabilities } from "./adapter-contract.js";
|
|
66
66
|
import { buildTraceabilityGraph, serializeTraceabilityGraph } from "./traceability.js";
|
|
67
67
|
import { evaluateQualityGate } from "./quality-gates.js";
|
|
68
|
-
import { BlockingError, blockingResultToJson, describeBlockingReason } from "./blocking-reasons.js";
|
|
69
|
-
import { runRiskAssess } from "./risk-assess.js";
|
|
68
|
+
import { BlockingError, blockingResultToJson, describeBlockingReason, formatBlockingOutput, } from "./blocking-reasons.js";
|
|
69
|
+
import { assessRisk, collectWorkingTreeChanges, hasCoveringTest, runRiskAssess, } from "./risk-assess.js";
|
|
70
|
+
import { extractStandards, parseStandardsIndex } from "./standards.js";
|
|
71
|
+
import { digestPath, digestFreshness, extractDigest, formatDigestLine, serializeDigest, } from "./digest.js";
|
|
72
|
+
function installationRequired() {
|
|
73
|
+
return {
|
|
74
|
+
exitCode: 1,
|
|
75
|
+
error: formatBlockingOutput(describeBlockingReason("installation_required", {
|
|
76
|
+
detail: "Run `aiw install` first.",
|
|
77
|
+
})),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function stateMismatch(detail) {
|
|
81
|
+
return {
|
|
82
|
+
exitCode: 1,
|
|
83
|
+
error: formatBlockingOutput(describeBlockingReason("state_mismatch", { detail })),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
70
86
|
import { parseMigrationCheckpoint, planNeutralResourceMigration, readResourceBytes, serializeMigrationPreview, serializeMigrationCheckpoint, validateNeutralResourceDocument, writeResourceBytes, } from "./migration.js";
|
|
71
87
|
import { parsePrivateRegistries, parseTeamPreset, serializePrivateRegistries, serializeTeamPreset, fetchPrivateRegistryClient, } from "./team-configuration.js";
|
|
72
88
|
import { enforcePolicyGate, gitPolicyTracking } from "./policy-gate.js";
|
|
73
89
|
import { DEFAULT_TELEMETRY_CONFIG, loadTelemetryConfig, offerTelemetryConsent, parseTelemetryConfig, purgeTelemetryBuffer, readTelemetryBuffer, serializeTelemetryMapping, TELEMETRY_BUFFER_CAP, TELEMETRY_BUFFER_FILE, } from "./telemetry.js";
|
|
74
90
|
import { createPluginScaffold, validatePluginDirectory, writePluginScaffold, } from "./plugin-sdk.js";
|
|
75
|
-
import { executeOrchestrationPlan, parseOrchestrationPlan, serializeOrchestrationPreview, serializeOrchestrationResults, validateOrchestrationParallelism, } from "./orchestration.js";
|
|
91
|
+
import { createTaskWorktrees, executeOrchestrationPlan, listManagedWorktrees, removeWorktrees, parseOrchestrationPlan, serializeOrchestrationPreview, serializeOrchestrationResults, validateOrchestrationParallelism, } from "./orchestration.js";
|
|
92
|
+
function laneAdvisory(root, fs, lightChosen) {
|
|
93
|
+
const paths = collectWorkingTreeChanges(root);
|
|
94
|
+
if (!paths || paths.length === 0)
|
|
95
|
+
return undefined;
|
|
96
|
+
const assessment = assessRisk(paths.map((path) => ({ path })), { hasCoveringTest: hasCoveringTest(root, fs) });
|
|
97
|
+
if (lightChosen && assessment.tier === "high")
|
|
98
|
+
return `advisory: risk assess reports high for the current changes (${assessment.reasons
|
|
99
|
+
.slice(0, 2)
|
|
100
|
+
.map((reason) => reason.code)
|
|
101
|
+
.join(", ")}); consider the full lane, or escalate later with \`aiw task escalate --reason=...\`.`;
|
|
102
|
+
if (!lightChosen && assessment.tier === "passive")
|
|
103
|
+
return "advisory: current changes look passive; the light lane (--lane=light --lane-reason=...) may be viable for small work.";
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
76
106
|
function beginWorkflowSession(fs, aiw, options) {
|
|
77
107
|
const sessionPath = join(aiw, "workflow.yml");
|
|
78
108
|
const ledgerPath = join(aiw, "approvals.yml");
|
|
79
109
|
if (fs.exists(sessionPath)) {
|
|
80
110
|
const previous = parseWorkflowSession(fs.read(sessionPath));
|
|
81
111
|
if (previous.status === "active")
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
};
|
|
112
|
+
throw new BlockingError(describeBlockingReason("workflow_still_active", {
|
|
113
|
+
detail: `Workflow ${previous.workflowId} is still active. Complete it before starting another.`,
|
|
114
|
+
}));
|
|
86
115
|
if (!fs.exists(ledgerPath))
|
|
87
|
-
return
|
|
116
|
+
return stateMismatch("Completed workflow is missing its approval ledger.");
|
|
88
117
|
const archive = join(aiw, `checkpoints/workflow-${previous.workflowId}-approvals.yml`);
|
|
89
118
|
if (fs.exists(archive))
|
|
90
119
|
return { exitCode: 1, error: `Workflow approval archive already exists: ${archive}` };
|
|
@@ -94,7 +123,7 @@ function beginWorkflowSession(fs, aiw, options) {
|
|
|
94
123
|
else if (fs.exists(ledgerPath)) {
|
|
95
124
|
return {
|
|
96
125
|
exitCode: 1,
|
|
97
|
-
error: "Approval ledger exists without a workflow session; run doctor.",
|
|
126
|
+
error: stateMismatch("Approval ledger exists without a workflow session; run doctor.").error,
|
|
98
127
|
};
|
|
99
128
|
}
|
|
100
129
|
const workflowId = options.id ?? randomUUID();
|
|
@@ -121,6 +150,7 @@ function beginWorkflowSession(fs, aiw, options) {
|
|
|
121
150
|
...(options.lane === "light" && options.laneReason
|
|
122
151
|
? { lane: "light", laneReason: options.laneReason }
|
|
123
152
|
: {}),
|
|
153
|
+
...(options.process && options.process !== "feature" ? { process: options.process } : {}),
|
|
124
154
|
});
|
|
125
155
|
fs.write(ledgerPath, serializeApprovalLedger(ledger));
|
|
126
156
|
fs.write(sessionPath, serializeWorkflowSession(session));
|
|
@@ -133,7 +163,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
133
163
|
const command = args[0] ?? "help";
|
|
134
164
|
if (command === "task") {
|
|
135
165
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
136
|
-
return
|
|
166
|
+
return installationRequired();
|
|
137
167
|
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";
|
|
138
168
|
const action = args[1];
|
|
139
169
|
const sessionPath = join(aiw, "workflow.yml");
|
|
@@ -144,10 +174,24 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
144
174
|
"id",
|
|
145
175
|
"lane",
|
|
146
176
|
"lane-reason",
|
|
177
|
+
"process",
|
|
147
178
|
]);
|
|
148
179
|
if (!options?.ticket)
|
|
149
180
|
return { exitCode: 1, error: usage };
|
|
150
181
|
validateTaskTicket(options.ticket);
|
|
182
|
+
const processName = options.process;
|
|
183
|
+
if (processName !== undefined) {
|
|
184
|
+
if (!isWorkflowProcess(processName))
|
|
185
|
+
return {
|
|
186
|
+
exitCode: 1,
|
|
187
|
+
error: `Unknown process: ${processName}. Known processes: ${PROCESS_IDS.join(", ")}.`,
|
|
188
|
+
};
|
|
189
|
+
if (processName === "feature")
|
|
190
|
+
return {
|
|
191
|
+
exitCode: 1,
|
|
192
|
+
error: "The feature process is the default; omit --process for it.",
|
|
193
|
+
};
|
|
194
|
+
}
|
|
151
195
|
if (options.lane !== undefined && options.lane !== "light")
|
|
152
196
|
return { exitCode: 1, error: "Only --lane=light exists; omit it for the full lane." };
|
|
153
197
|
const laneReason = options["lane-reason"];
|
|
@@ -177,14 +221,17 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
177
221
|
ticket: options.ticket,
|
|
178
222
|
...(options.title ? { title: options.title } : {}),
|
|
179
223
|
...(lane ? { lane, laneReason: laneReason } : {}),
|
|
224
|
+
...(processName ? { process: processName } : {}),
|
|
180
225
|
});
|
|
181
226
|
if (result.exitCode !== 0)
|
|
182
227
|
return result;
|
|
228
|
+
const advisory = laneAdvisory(root, fs, lane === "light");
|
|
183
229
|
const title = options.title ? ` titled "${options.title}"` : "";
|
|
184
230
|
if (lane)
|
|
185
231
|
return {
|
|
186
232
|
exitCode: 0,
|
|
187
233
|
output: `Task started for ticket ${options.ticket}${title} (light lane): ${result.output}\n` +
|
|
234
|
+
(advisory ? `${advisory}\n` : "") +
|
|
188
235
|
"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
236
|
`Rationale recorded: ${laneReason}\n` +
|
|
190
237
|
'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=...`.',
|
|
@@ -192,6 +239,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
192
239
|
return {
|
|
193
240
|
exitCode: 0,
|
|
194
241
|
output: `Task started for ticket ${options.ticket}${title}: ${result.output}\n` +
|
|
242
|
+
(advisory ? `${advisory}\n` : "") +
|
|
195
243
|
`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=...\`.`,
|
|
196
244
|
};
|
|
197
245
|
}
|
|
@@ -288,7 +336,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
288
336
|
...lane,
|
|
289
337
|
`Workflow: ${session.workflowId} (${session.status})`,
|
|
290
338
|
"Stages:",
|
|
291
|
-
...
|
|
339
|
+
...processStages(ledger).map((stage) => `- ${stage}: ${ledger.stages[stage]?.status ?? "not-started"}`),
|
|
292
340
|
];
|
|
293
341
|
if (session.status === "active" && effectiveLane(session) === "light") {
|
|
294
342
|
lines.push('Next: record validation with `aiw task evidence --validation="<command and result>"`, then `aiw workflow complete`. If scope grows: `aiw task escalate --reason=...`.');
|
|
@@ -303,25 +351,77 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
303
351
|
}
|
|
304
352
|
if (command === "workflow") {
|
|
305
353
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
306
|
-
return
|
|
354
|
+
return installationRequired();
|
|
307
355
|
const action = args[1];
|
|
308
356
|
const sessionPath = join(aiw, "workflow.yml");
|
|
309
357
|
if (action === "start") {
|
|
310
|
-
const options = parseNamedOptions(args.slice(2), ["id"]);
|
|
358
|
+
const options = parseNamedOptions(args.slice(2).filter((argument) => argument !== "--json"), ["id", "process"]);
|
|
311
359
|
if (!options)
|
|
312
|
-
return {
|
|
313
|
-
|
|
360
|
+
return {
|
|
361
|
+
exitCode: 1,
|
|
362
|
+
error: "Usage: aiw workflow start [--id=workflow-id] [--process=bugfix|idea-assessment]",
|
|
363
|
+
};
|
|
364
|
+
const processName = options.process;
|
|
365
|
+
if (processName !== undefined && !isWorkflowProcess(processName))
|
|
366
|
+
return {
|
|
367
|
+
exitCode: 1,
|
|
368
|
+
error: `Unknown process: ${processName}. Known processes: ${PROCESS_IDS.join(", ")}.`,
|
|
369
|
+
};
|
|
370
|
+
return beginWorkflowSession(fs, aiw, {
|
|
371
|
+
id: options.id,
|
|
372
|
+
...(processName ? { process: processName } : {}),
|
|
373
|
+
});
|
|
314
374
|
}
|
|
315
375
|
if (action === "status") {
|
|
316
376
|
if (!fs.exists(sessionPath))
|
|
317
377
|
return { exitCode: 0, output: "No SDD workflow is active." };
|
|
318
378
|
const { session, ledger } = readWorkflowState(aiw, fs, false);
|
|
319
|
-
const stages =
|
|
379
|
+
const stages = processStages(ledger).map((stage) => `- ${stage}: ${ledger.stages[stage]?.status ?? "not-started"}`);
|
|
320
380
|
return {
|
|
321
381
|
exitCode: 0,
|
|
322
382
|
output: `Workflow ${session.workflowId} (${session.status})\n${stages.join("\n")}\n${workflowNextAction(ledger)}`,
|
|
323
383
|
};
|
|
324
384
|
}
|
|
385
|
+
if (action === "abandon") {
|
|
386
|
+
const options = parseNamedOptions(args.slice(2), ["reason"]);
|
|
387
|
+
if (!options?.reason)
|
|
388
|
+
return { exitCode: 1, error: "Usage: aiw workflow abandon --reason=why-obsolete" };
|
|
389
|
+
if (options.reason.length > 500 || /[\r\n]/.test(options.reason))
|
|
390
|
+
return {
|
|
391
|
+
exitCode: 1,
|
|
392
|
+
error: "--reason must be a single line of at most 500 bytes.",
|
|
393
|
+
};
|
|
394
|
+
if (!fs.exists(sessionPath))
|
|
395
|
+
return {
|
|
396
|
+
exitCode: 1,
|
|
397
|
+
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.",
|
|
398
|
+
};
|
|
399
|
+
const { session } = readActiveWorkflow(aiw, fs);
|
|
400
|
+
const ledgerPath = join(aiw, "approvals.yml");
|
|
401
|
+
const sessionArchive = join(aiw, "checkpoints", `workflow-${session.workflowId}-session.yml`);
|
|
402
|
+
const ledgerArchive = join(aiw, "checkpoints", `workflow-${session.workflowId}-approvals.yml`);
|
|
403
|
+
fs.mkdir(join(aiw, "checkpoints"));
|
|
404
|
+
const now = new Date().toISOString();
|
|
405
|
+
const archivedSession = {
|
|
406
|
+
...session,
|
|
407
|
+
status: "abandoned",
|
|
408
|
+
abandonedReason: options.reason,
|
|
409
|
+
updatedAt: now,
|
|
410
|
+
};
|
|
411
|
+
fs.write(sessionArchive, serializeWorkflowSession(archivedSession));
|
|
412
|
+
if (fs.exists(ledgerPath))
|
|
413
|
+
fs.write(ledgerArchive, fs.read(ledgerPath));
|
|
414
|
+
fs.remove?.(sessionPath);
|
|
415
|
+
if (fs.exists(ledgerPath))
|
|
416
|
+
fs.remove?.(ledgerPath);
|
|
417
|
+
return {
|
|
418
|
+
exitCode: 0,
|
|
419
|
+
output: `Workflow ${session.workflowId} abandoned (${options.reason}).\n` +
|
|
420
|
+
`Session archived: ${sessionArchive}\n` +
|
|
421
|
+
(fs.exists(ledgerArchive) ? `Approval ledger archived: ${ledgerArchive}\n` : "") +
|
|
422
|
+
"Start a new one when ready.",
|
|
423
|
+
};
|
|
424
|
+
}
|
|
325
425
|
if (action === "complete") {
|
|
326
426
|
const { session, ledger } = readActiveWorkflow(aiw, fs);
|
|
327
427
|
if (effectiveLane(session) === "light") {
|
|
@@ -343,7 +443,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
343
443
|
output: `Workflow completed (light lane): ${session.workflowId}. Evidence: ${evidencePath}`,
|
|
344
444
|
};
|
|
345
445
|
}
|
|
346
|
-
const unresolved =
|
|
446
|
+
const unresolved = processStages(ledger).filter((stage) => ledger.stages[stage]?.status !== "approved" &&
|
|
347
447
|
ledger.stages[stage]?.status !== "skipped");
|
|
348
448
|
if (unresolved.length)
|
|
349
449
|
return {
|
|
@@ -418,7 +518,12 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
418
518
|
updatedAt: new Date().toISOString(),
|
|
419
519
|
});
|
|
420
520
|
fs.write(join(aiw, "approvals.yml"), serializeApprovalLedger(updated));
|
|
421
|
-
|
|
521
|
+
fs.mkdir(join(aiw, "digests"));
|
|
522
|
+
fs.write(digestPath(aiw, workflowStage), serializeDigest(extractDigest(workflowStage, fs.read(artifact), artifactHash)));
|
|
523
|
+
return {
|
|
524
|
+
exitCode: 0,
|
|
525
|
+
output: `Human approval recorded for ${workflowStage}. Digest available: aiw digest --stage=${workflowStage}`,
|
|
526
|
+
};
|
|
422
527
|
}
|
|
423
528
|
const options = parseNamedOptions(rest, ["reason"]);
|
|
424
529
|
if (!options?.reason)
|
|
@@ -432,7 +537,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
432
537
|
}
|
|
433
538
|
if (command === "ui") {
|
|
434
539
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
435
|
-
return
|
|
540
|
+
return installationRequired();
|
|
436
541
|
const parsed = parseNamedOptions(args.slice(1), ["port"]);
|
|
437
542
|
if (!parsed || (parsed.port !== undefined && !/^\d+$/.test(parsed.port)))
|
|
438
543
|
return { exitCode: 1, error: "Usage: aiw ui [--port=number]" };
|
|
@@ -446,20 +551,36 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
446
551
|
}
|
|
447
552
|
if (command === "orchestrate") {
|
|
448
553
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
449
|
-
return
|
|
554
|
+
return installationRequired();
|
|
450
555
|
const executeCount = args.filter((arg) => arg === "--execute").length;
|
|
451
556
|
if (executeCount > 1)
|
|
452
557
|
return {
|
|
453
558
|
exitCode: 1,
|
|
454
|
-
error: "Usage: aiw orchestrate --plan=path [--execute] [--max-parallel=number]",
|
|
559
|
+
error: "Usage: aiw orchestrate --plan=path [--execute] [--isolate] [--cleanup] [--max-parallel=number]",
|
|
455
560
|
};
|
|
456
561
|
const execute = executeCount === 1;
|
|
457
|
-
const
|
|
562
|
+
const isolate = args.includes("--isolate");
|
|
563
|
+
const cleanup = args.includes("--cleanup");
|
|
564
|
+
if (cleanup) {
|
|
565
|
+
const leaked = listManagedWorktrees(root, aiw);
|
|
566
|
+
if (leaked.length === 0)
|
|
567
|
+
return { exitCode: 0, output: "No managed worktrees to clean." };
|
|
568
|
+
if (!execute)
|
|
569
|
+
return {
|
|
570
|
+
exitCode: 0,
|
|
571
|
+
output: `Managed worktrees found:\n${leaked.map((path) => `- ${path}`).join("\n")}\nRe-run with --execute to remove them.`,
|
|
572
|
+
};
|
|
573
|
+
removeWorktrees(root, leaked, { force: true });
|
|
574
|
+
return { exitCode: 0, output: `Removed ${leaked.length} managed worktree(s).` };
|
|
575
|
+
}
|
|
576
|
+
const named = args
|
|
577
|
+
.slice(1)
|
|
578
|
+
.filter((arg) => arg !== "--execute" && arg !== "--isolate" && arg !== "--cleanup");
|
|
458
579
|
const parsed = parseNamedOptions(named, ["plan", "max-parallel"]);
|
|
459
580
|
if (!parsed?.plan)
|
|
460
581
|
return {
|
|
461
582
|
exitCode: 1,
|
|
462
|
-
error: "Usage: aiw orchestrate --plan=path [--execute] [--max-parallel=number]",
|
|
583
|
+
error: "Usage: aiw orchestrate --plan=path [--execute] [--isolate] [--cleanup] [--max-parallel=number]",
|
|
463
584
|
};
|
|
464
585
|
const planPath = safeProjectFile(root, parsed.plan, fs);
|
|
465
586
|
const plan = parseOrchestrationPlan(fs.read(planPath));
|
|
@@ -478,7 +599,19 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
478
599
|
error: `Orchestration checkpoint already exists; use a new plan id: ${checkpoint}`,
|
|
479
600
|
};
|
|
480
601
|
try {
|
|
481
|
-
const
|
|
602
|
+
const isolation = isolate
|
|
603
|
+
? {
|
|
604
|
+
options: { root, aiw, planId: plan.id },
|
|
605
|
+
worktrees: createTaskWorktrees(root, aiw, plan),
|
|
606
|
+
}
|
|
607
|
+
: undefined;
|
|
608
|
+
const results = await executeOrchestrationPlan(plan, services.orchestrator, maxParallel, isolation);
|
|
609
|
+
if (isolation) {
|
|
610
|
+
const passedPaths = results
|
|
611
|
+
.filter(({ status, worktree }) => status === "passed" && worktree)
|
|
612
|
+
.map(({ worktree }) => worktree);
|
|
613
|
+
removeWorktrees(root, passedPaths);
|
|
614
|
+
}
|
|
482
615
|
fs.write(checkpoint, serializeOrchestrationResults(plan, results));
|
|
483
616
|
const failed = results.filter(({ status }) => status !== "passed");
|
|
484
617
|
return failed.length
|
|
@@ -601,7 +734,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
601
734
|
}
|
|
602
735
|
if (command === "telemetry") {
|
|
603
736
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
604
|
-
return
|
|
737
|
+
return installationRequired();
|
|
605
738
|
const usage = "Usage: aiw telemetry <status|show|enable|disable> [--verbose] [--commands=include|exclude] [--outcomes=include|exclude]";
|
|
606
739
|
const action = args[1];
|
|
607
740
|
if (!action)
|
|
@@ -714,7 +847,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
714
847
|
}
|
|
715
848
|
if (command === "context-bench") {
|
|
716
849
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
717
|
-
return
|
|
850
|
+
return installationRequired();
|
|
718
851
|
const saveCount = args.filter((arg) => arg === "--save").length;
|
|
719
852
|
if (saveCount > 1)
|
|
720
853
|
return { exitCode: 1, error: "Usage: aiw context-bench [--baseline=path] [--save]" };
|
|
@@ -759,7 +892,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
759
892
|
}
|
|
760
893
|
if (command === "parity") {
|
|
761
894
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
762
|
-
return
|
|
895
|
+
return installationRequired();
|
|
763
896
|
const parsed = parseNamedOptions(args.slice(1), ["tasks"]);
|
|
764
897
|
if (!parsed)
|
|
765
898
|
return { exitCode: 1, error: "Usage: aiw parity [--tasks=directory]" };
|
|
@@ -813,7 +946,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
813
946
|
}
|
|
814
947
|
if (command === "scan") {
|
|
815
948
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
816
|
-
return
|
|
949
|
+
return installationRequired();
|
|
817
950
|
const profile = await (services.profiler?.profile(root) ?? profileProject(root));
|
|
818
951
|
let interpreter = services.interpreter;
|
|
819
952
|
let interpreterModel;
|
|
@@ -1093,7 +1226,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
1093
1226
|
return { exitCode: 1, error: `Unsupported target: ${target}` };
|
|
1094
1227
|
const manifestPath = join(aiw, "manifest.yml");
|
|
1095
1228
|
if (!fs.exists(manifestPath))
|
|
1096
|
-
return
|
|
1229
|
+
return installationRequired();
|
|
1097
1230
|
const dryRun = args.includes("--dry-run");
|
|
1098
1231
|
const targetPath = join(root, renderAiInitPath(target));
|
|
1099
1232
|
const neutralAiInit = join(aiw, "resources/skills/ai-init.md");
|
|
@@ -1420,7 +1553,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
1420
1553
|
}
|
|
1421
1554
|
if (command === "confirm") {
|
|
1422
1555
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
1423
|
-
return
|
|
1556
|
+
return installationRequired();
|
|
1424
1557
|
const overrides = [];
|
|
1425
1558
|
const accept = args.indexOf("--accept");
|
|
1426
1559
|
if (accept >= 0 && args[accept + 1])
|
|
@@ -1444,7 +1577,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
1444
1577
|
}
|
|
1445
1578
|
if (command === "recommend") {
|
|
1446
1579
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
1447
|
-
return
|
|
1580
|
+
return installationRequired();
|
|
1448
1581
|
const profile = await (services.profiler?.profile(root) ?? profileProject(root));
|
|
1449
1582
|
const recommendations = services.recommender?.recommend(profile) ?? recommendCapabilities(profile);
|
|
1450
1583
|
let ordered = recommendations;
|
|
@@ -1512,7 +1645,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
1512
1645
|
}
|
|
1513
1646
|
if (command === "skills") {
|
|
1514
1647
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
1515
|
-
return
|
|
1648
|
+
return installationRequired();
|
|
1516
1649
|
const action = args[1];
|
|
1517
1650
|
const executor = services.externalSkills ?? nodeCommandExecutor;
|
|
1518
1651
|
const approvedPermissions = parseApprovedPermissions(args);
|
|
@@ -1576,7 +1709,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
1576
1709
|
}
|
|
1577
1710
|
if (command === "generate" || command === "sync") {
|
|
1578
1711
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
1579
|
-
return
|
|
1712
|
+
return installationRequired();
|
|
1580
1713
|
const profilePath = join(aiw, "profile.yml");
|
|
1581
1714
|
const persistedProfile = fs.exists(profilePath) ? fs.read(profilePath) : "";
|
|
1582
1715
|
const profile = /^status:\s*scanned$/m.test(persistedProfile)
|
|
@@ -1805,7 +1938,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
1805
1938
|
}
|
|
1806
1939
|
if (command === "brainstorm") {
|
|
1807
1940
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
1808
|
-
return
|
|
1941
|
+
return installationRequired();
|
|
1809
1942
|
assertStageReady(aiw, fs, "brainstorming");
|
|
1810
1943
|
const title = args.find((arg) => arg.startsWith("--title="))?.slice(8) || "Untitled Brainstorm";
|
|
1811
1944
|
const path = join(aiw, "generated/specs/brainstorm.md");
|
|
@@ -1814,7 +1947,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
1814
1947
|
}
|
|
1815
1948
|
if (command === "spec") {
|
|
1816
1949
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
1817
|
-
return
|
|
1950
|
+
return installationRequired();
|
|
1818
1951
|
assertStageReady(aiw, fs, "specification");
|
|
1819
1952
|
const title = args.find((arg) => arg.startsWith("--title="))?.slice(8) || "Untitled Specification";
|
|
1820
1953
|
const ticket = args.find((arg) => arg.startsWith("--ticket="))?.slice(9);
|
|
@@ -1826,7 +1959,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
1826
1959
|
}
|
|
1827
1960
|
if (command === "adr") {
|
|
1828
1961
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
1829
|
-
return
|
|
1962
|
+
return installationRequired();
|
|
1830
1963
|
assertStageReady(aiw, fs, "technical-design");
|
|
1831
1964
|
const id = args.find((arg) => arg.startsWith("--id="))?.slice(5) || "001";
|
|
1832
1965
|
const title = args.find((arg) => arg.startsWith("--title="))?.slice(8) || "Untitled Decision";
|
|
@@ -1836,26 +1969,27 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
1836
1969
|
}
|
|
1837
1970
|
if (command === "plan") {
|
|
1838
1971
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
1839
|
-
return
|
|
1972
|
+
return installationRequired();
|
|
1840
1973
|
assertStageReady(aiw, fs, "plan");
|
|
1841
1974
|
const title = args.find((arg) => arg.startsWith("--title="))?.slice(8) || "Untitled Implementation Plan";
|
|
1842
1975
|
const path = join(aiw, "generated/plans/implementation-plan.md");
|
|
1843
|
-
fs.write(path, `# ${title}\n\n## Linked Requirements\n\n- REQ-001: Link to the specification requirement.\n\n## Tasks\n\n- [ ] TASK-001: Implement the first increment.\n - Requirement: REQ-001\n - Code: src/\n - Tests: src/**/*.test.ts\n - Validation: npm test\n - Evidence: Record the passing command output and changed files.\n\n## Validation Commands\n\n- npm run check\n\n## Risks and Dependencies\n\n- Dependencies: List prerequisite tasks, packages, or decisions.\n- Risks: Record delivery, compatibility, and validation risks with mitigations.\n\n## Completion Evidence\n\n- TASK-001: Link code changes, test results, and validation output.\n\n`);
|
|
1976
|
+
fs.write(path, `# ${title}\n\n## Linked Requirements\n\n- REQ-001: Link to the specification requirement.\n\n## Tasks\n\n- [ ] TASK-001: Implement the first increment.\n - Requirement: REQ-001\n - Code: src/\n - Tests: src/**/*.test.ts\n - Validation: npm test → exit 0\n - Regression: <optional — touched existing behavior → the test proving it holds>\n - Evidence: Record the passing command output and changed files.\n\n## Validation Commands\n\n- npm run check\n\n## Risks and Dependencies\n\n- Dependencies: List prerequisite tasks, packages, or decisions.\n- Risks: Record delivery, compatibility, and validation risks with mitigations.\n\n## Completion Evidence\n\n- TASK-001: Link code changes, test results, and validation output.\n\n`);
|
|
1844
1977
|
return { exitCode: 0, output: `Implementation plan created: ${path}` };
|
|
1845
1978
|
}
|
|
1846
1979
|
if (command === "verify") {
|
|
1847
1980
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
1848
|
-
return
|
|
1981
|
+
return installationRequired();
|
|
1849
1982
|
assertStageReady(aiw, fs, "verification");
|
|
1850
1983
|
const specPath = join(aiw, "generated/specs/specification.md");
|
|
1851
1984
|
const planPath = join(aiw, "generated/plans/implementation-plan.md");
|
|
1852
1985
|
const issues = [];
|
|
1986
|
+
let regressionNotice;
|
|
1853
1987
|
let specification = "";
|
|
1854
1988
|
if (!fs.exists(specPath))
|
|
1855
1989
|
issues.push("Specification artifact is missing.");
|
|
1856
1990
|
else {
|
|
1857
1991
|
specification = fs.read(specPath);
|
|
1858
|
-
if (!specification.includes("
|
|
1992
|
+
if (!specification.toLowerCase().includes("acceptance criteria"))
|
|
1859
1993
|
issues.push("Requirements have no acceptance criteria.");
|
|
1860
1994
|
}
|
|
1861
1995
|
if (!fs.exists(planPath))
|
|
@@ -1868,17 +2002,24 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
1868
2002
|
const uncovered = requirements.filter((requirement) => !plan.includes(requirement));
|
|
1869
2003
|
if (uncovered.length > 0)
|
|
1870
2004
|
issues.push(`Untested requirements: ${uncovered.join(", ")}.`);
|
|
2005
|
+
const validationCount = (plan.match(/Validation:/g) ?? []).length;
|
|
2006
|
+
const regressionCount = (plan.match(/Regression:/g) ?? []).length;
|
|
2007
|
+
regressionNotice =
|
|
2008
|
+
validationCount > regressionCount
|
|
2009
|
+
? `${validationCount - regressionCount} of ${validationCount} plan tasks declare no regression coverage for touched existing behavior.`
|
|
2010
|
+
: undefined;
|
|
1871
2011
|
}
|
|
1872
2012
|
return issues.length
|
|
1873
2013
|
? { exitCode: 1, error: `Verification failed: ${issues.join(" ")}` }
|
|
1874
2014
|
: {
|
|
1875
2015
|
exitCode: 0,
|
|
1876
|
-
output: "Verification passed: requirements have acceptance criteria and validation coverage."
|
|
2016
|
+
output: "Verification passed: requirements have acceptance criteria and validation coverage." +
|
|
2017
|
+
(regressionNotice ? ` Advisory: ${regressionNotice}` : ""),
|
|
1877
2018
|
};
|
|
1878
2019
|
}
|
|
1879
2020
|
if (command === "trace") {
|
|
1880
2021
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
1881
|
-
return
|
|
2022
|
+
return installationRequired();
|
|
1882
2023
|
assertStageReady(aiw, fs, "traceability");
|
|
1883
2024
|
const specPath = join(aiw, "generated/specs/specification.md");
|
|
1884
2025
|
const planPath = join(aiw, "generated/plans/implementation-plan.md");
|
|
@@ -1907,6 +2048,144 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
1907
2048
|
fs.write(path, serialized);
|
|
1908
2049
|
return { exitCode: 0, output: `Traceability graph created: ${path}` };
|
|
1909
2050
|
}
|
|
2051
|
+
if (command === "standards") {
|
|
2052
|
+
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
2053
|
+
return installationRequired();
|
|
2054
|
+
const action = args[1] ?? "list";
|
|
2055
|
+
const index = fs.exists(join(aiw, "standards/index.yml"))
|
|
2056
|
+
? parseStandardsIndex(fs.read(join(aiw, "standards/index.yml")))
|
|
2057
|
+
: { schema: 1, standards: {} };
|
|
2058
|
+
if (action === "extract") {
|
|
2059
|
+
fs.mkdir(join(aiw, "standards"));
|
|
2060
|
+
const documents = extractStandards(root, fs);
|
|
2061
|
+
for (const document of documents) {
|
|
2062
|
+
const body = [
|
|
2063
|
+
`# Standard: ${document.topic}`,
|
|
2064
|
+
"",
|
|
2065
|
+
`Status: ${document.status}`,
|
|
2066
|
+
"",
|
|
2067
|
+
...document.statements.flatMap((statement) => [
|
|
2068
|
+
statement.convention,
|
|
2069
|
+
...(statement.occurrences > 0
|
|
2070
|
+
? statement.evidence.map((evidence) => `- Evidence: ${evidence.path}:${evidence.line}`)
|
|
2071
|
+
: ["- Evidence: none (deterministic scan found no instances)"]),
|
|
2072
|
+
]),
|
|
2073
|
+
"",
|
|
2074
|
+
].join("\n");
|
|
2075
|
+
fs.write(join(aiw, `standards/${document.topic}.md`), body);
|
|
2076
|
+
if (index.standards[document.topic] === undefined)
|
|
2077
|
+
index.standards[document.topic] = {
|
|
2078
|
+
status: "inferred",
|
|
2079
|
+
updatedAt: new Date().toISOString(),
|
|
2080
|
+
};
|
|
2081
|
+
}
|
|
2082
|
+
const serialized = [
|
|
2083
|
+
"schema: 1",
|
|
2084
|
+
"standards:",
|
|
2085
|
+
...Object.entries(index.standards).map(([topic, entry]) => ` ${topic}:\n status: ${entry.status}\n updatedAt: ${entry.updatedAt}`),
|
|
2086
|
+
"",
|
|
2087
|
+
].join("\n");
|
|
2088
|
+
fs.write(join(aiw, "standards/index.yml"), serialized);
|
|
2089
|
+
const names = documents.map((document) => document.topic).join(", ");
|
|
2090
|
+
return {
|
|
2091
|
+
exitCode: 0,
|
|
2092
|
+
output: `Standards extracted (${names}). All are inferred — review them, then confirm with \`aiw standards confirm <topic>\`. Rejected or confirmed standards never change until you say so.`,
|
|
2093
|
+
};
|
|
2094
|
+
}
|
|
2095
|
+
if (action === "list") {
|
|
2096
|
+
const entries = Object.entries(index.standards);
|
|
2097
|
+
if (entries.length === 0)
|
|
2098
|
+
return {
|
|
2099
|
+
exitCode: 0,
|
|
2100
|
+
output: "No standards recorded. Run `aiw standards extract` first.",
|
|
2101
|
+
};
|
|
2102
|
+
return {
|
|
2103
|
+
exitCode: 0,
|
|
2104
|
+
output: [
|
|
2105
|
+
"Standards:",
|
|
2106
|
+
...entries.map(([topic, entry]) => `- ${topic}: ${entry.status}`),
|
|
2107
|
+
"Confirm or reject with `aiw standards confirm|reject <topic>`.",
|
|
2108
|
+
].join("\n"),
|
|
2109
|
+
};
|
|
2110
|
+
}
|
|
2111
|
+
if (action === "confirm" || action === "reject") {
|
|
2112
|
+
const topic = args[2];
|
|
2113
|
+
if (!topic)
|
|
2114
|
+
return { exitCode: 1, error: `Usage: aiw standards ${action} <topic>` };
|
|
2115
|
+
const path = join(aiw, `standards/${topic}.md`);
|
|
2116
|
+
if (!fs.exists(path))
|
|
2117
|
+
return {
|
|
2118
|
+
exitCode: 1,
|
|
2119
|
+
error: `No standard document for ${topic}; run \`aiw standards extract\` first.`,
|
|
2120
|
+
};
|
|
2121
|
+
const status = action === "confirm" ? "confirmed" : "rejected";
|
|
2122
|
+
index.standards[topic] = { status, updatedAt: new Date().toISOString() };
|
|
2123
|
+
const existing = fs.read(path).replace(/^Status: .*$/m, `Status: ${status}`);
|
|
2124
|
+
fs.write(path, existing);
|
|
2125
|
+
const serialized = [
|
|
2126
|
+
"schema: 1",
|
|
2127
|
+
"standards:",
|
|
2128
|
+
...Object.entries(index.standards).map(([topicEntry, entry]) => ` ${topicEntry}:\n status: ${entry.status}\n updatedAt: ${entry.updatedAt}`),
|
|
2129
|
+
"",
|
|
2130
|
+
].join("\n");
|
|
2131
|
+
fs.write(join(aiw, "standards/index.yml"), serialized);
|
|
2132
|
+
return {
|
|
2133
|
+
exitCode: 0,
|
|
2134
|
+
output: `Standard ${topic} ${status}. ${status === "confirmed"
|
|
2135
|
+
? "Confirmed standards are referenced by generated rules."
|
|
2136
|
+
: "Rejected standards are never rendered."}`,
|
|
2137
|
+
};
|
|
2138
|
+
}
|
|
2139
|
+
return {
|
|
2140
|
+
exitCode: 1,
|
|
2141
|
+
error: "Usage: aiw standards <extract|list|confirm|reject> [topic] — standards are inferred until a human confirms them.",
|
|
2142
|
+
};
|
|
2143
|
+
}
|
|
2144
|
+
if (command === "digest") {
|
|
2145
|
+
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
2146
|
+
return installationRequired();
|
|
2147
|
+
const stageArg = args.find((argument) => argument.startsWith("--stage="))?.slice(8);
|
|
2148
|
+
const stages = stageArg
|
|
2149
|
+
? [stageArg]
|
|
2150
|
+
: fs.exists(join(aiw, "workflow.yml"))
|
|
2151
|
+
? processStages(readWorkflowState(aiw, fs, false).ledger)
|
|
2152
|
+
: [...APPROVAL_STAGES];
|
|
2153
|
+
const lines = [];
|
|
2154
|
+
let served = 0;
|
|
2155
|
+
for (const stage of stages) {
|
|
2156
|
+
const artifactPath = approvalArtifactPath(aiw, stage);
|
|
2157
|
+
const freshness = digestFreshness(aiw, fs, stage, artifactPath ?? undefined);
|
|
2158
|
+
if (freshness.state === "missing")
|
|
2159
|
+
continue;
|
|
2160
|
+
lines.push(formatDigestLine(freshness));
|
|
2161
|
+
served += 1;
|
|
2162
|
+
}
|
|
2163
|
+
if (lines.length === 0)
|
|
2164
|
+
return {
|
|
2165
|
+
exitCode: 0,
|
|
2166
|
+
output: "No digests yet: digests appear when stages pass their gate and receive human approval.",
|
|
2167
|
+
};
|
|
2168
|
+
lines.push(`served: ${served} digest${served === 1 ? "" : "s"} (full reads on demand)`);
|
|
2169
|
+
return { exitCode: 0, output: lines.join("\n") };
|
|
2170
|
+
}
|
|
2171
|
+
if (command === "diagnosis") {
|
|
2172
|
+
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
2173
|
+
return installationRequired();
|
|
2174
|
+
assertStageReady(aiw, fs, "diagnosis");
|
|
2175
|
+
const title = args.find((argument) => argument.startsWith("--title="))?.slice(8) || "Untitled Diagnosis";
|
|
2176
|
+
const path = join(aiw, "generated/reports/diagnosis.md");
|
|
2177
|
+
fs.write(path, `# ${title}\n\n## Reproduction\n\n- Command: \`<command that reproduces the bug>\`\n- Observed: <what actually happens>\n\n## Expected\n\n- <the correct behavior>\n\n## Root Cause\n\n- <evidence-backed cause, file:line when known>\n\n## Scope\n\n- In scope: <files and behavior this fix touches>\n- Out of scope: <explicitly excluded>\n`);
|
|
2178
|
+
return { exitCode: 0, output: `Diagnosis artifact created: ${path}` };
|
|
2179
|
+
}
|
|
2180
|
+
if (command === "assess") {
|
|
2181
|
+
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
2182
|
+
return installationRequired();
|
|
2183
|
+
assertStageReady(aiw, fs, "assessment");
|
|
2184
|
+
const title = args.find((argument) => argument.startsWith("--title="))?.slice(8) || "Untitled Assessment";
|
|
2185
|
+
const path = join(aiw, "generated/reports/assessment.md");
|
|
2186
|
+
fs.write(path, `# ${title}\n\n## Evidence\n\n- <attributed evidence: source, finding>\n\n## Outcome\n\ngo | clarify | stop\n\n## Rationale\n\n- <why this outcome follows from the evidence>\n\n## Next Step\n\n- <go: recommended feature scope · clarify: the open questions · stop: conditions that would reopen>\n`);
|
|
2187
|
+
return { exitCode: 0, output: `Assessment artifact created: ${path}` };
|
|
2188
|
+
}
|
|
1910
2189
|
if (command === "risk") {
|
|
1911
2190
|
if (args[1] !== "assess")
|
|
1912
2191
|
return { exitCode: 1, error: "Usage: aiw risk assess [--base-ref=ref] [--json]" };
|
|
@@ -1914,7 +2193,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
1914
2193
|
}
|
|
1915
2194
|
if (command === "gate") {
|
|
1916
2195
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
1917
|
-
return
|
|
2196
|
+
return installationRequired();
|
|
1918
2197
|
const stage = args[1];
|
|
1919
2198
|
if (!stage || !APPROVAL_STAGES.includes(stage))
|
|
1920
2199
|
return {
|
|
@@ -1941,6 +2220,17 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
1941
2220
|
detail: `Quality gate blocked: ${issues.join(" ")}`,
|
|
1942
2221
|
}));
|
|
1943
2222
|
}
|
|
2223
|
+
if (hasQualityGate &&
|
|
2224
|
+
(workflowStage === "implementation" || workflowStage === "verification")) {
|
|
2225
|
+
const pairIssues = compareVerificationPairs(aiw, fs, workflowStage);
|
|
2226
|
+
if (pairIssues.length)
|
|
2227
|
+
throw new BlockingError(describeBlockingReason("gate_incomplete", {
|
|
2228
|
+
stage,
|
|
2229
|
+
artifactPath,
|
|
2230
|
+
gateIssues: pairIssues,
|
|
2231
|
+
detail: `Quality gate blocked: ${pairIssues.join(" ")}`,
|
|
2232
|
+
}));
|
|
2233
|
+
}
|
|
1944
2234
|
const { ledger } = readActiveWorkflow(aiw, fs);
|
|
1945
2235
|
const updated = beginStageApproval(ledger, workflowStage, {
|
|
1946
2236
|
artifactHash: stageFingerprint(fs.read(artifactPath)),
|
|
@@ -1969,7 +2259,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
1969
2259
|
}
|
|
1970
2260
|
if (command === "resolve") {
|
|
1971
2261
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
1972
|
-
return
|
|
2262
|
+
return installationRequired();
|
|
1973
2263
|
const packageArg = args.find((arg) => arg.startsWith("--package="));
|
|
1974
2264
|
const sourceArg = args.find((arg) => arg.startsWith("--source="));
|
|
1975
2265
|
if (!packageArg && !sourceArg)
|
|
@@ -2031,7 +2321,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
2031
2321
|
}
|
|
2032
2322
|
if (command === "organization-policy") {
|
|
2033
2323
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
2034
|
-
return
|
|
2324
|
+
return installationRequired();
|
|
2035
2325
|
const fileArg = args.find((arg) => arg.startsWith("--file="));
|
|
2036
2326
|
if (!fileArg)
|
|
2037
2327
|
return { exitCode: 1, error: "Usage: aiw organization-policy --file=path [--replace]" };
|
|
@@ -2047,7 +2337,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
2047
2337
|
}
|
|
2048
2338
|
if (command === "policy-check") {
|
|
2049
2339
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
2050
|
-
return
|
|
2340
|
+
return installationRequired();
|
|
2051
2341
|
const policyPath = join(aiw, "organization.yml");
|
|
2052
2342
|
if (!fs.exists(policyPath))
|
|
2053
2343
|
return { exitCode: 1, error: "Organization policy is required for the CI policy gate." };
|
|
@@ -2085,7 +2375,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
2085
2375
|
}
|
|
2086
2376
|
if (command === "preset") {
|
|
2087
2377
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
2088
|
-
return
|
|
2378
|
+
return installationRequired();
|
|
2089
2379
|
const fileArg = args.find((arg) => arg.startsWith("--file="));
|
|
2090
2380
|
if (!fileArg)
|
|
2091
2381
|
return { exitCode: 1, error: "Usage: aiw preset --file=path [--replace]" };
|
|
@@ -2101,7 +2391,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
2101
2391
|
}
|
|
2102
2392
|
if (command === "update") {
|
|
2103
2393
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
2104
|
-
return
|
|
2394
|
+
return installationRequired();
|
|
2105
2395
|
const packageArg = args.find((arg) => arg.startsWith("--package="));
|
|
2106
2396
|
if (!packageArg)
|
|
2107
2397
|
return { exitCode: 1, error: "Usage: aiw update --package=path --target=target" };
|
|
@@ -2132,7 +2422,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
2132
2422
|
if (command === "registry") {
|
|
2133
2423
|
if (args[1] === "configure") {
|
|
2134
2424
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
2135
|
-
return
|
|
2425
|
+
return installationRequired();
|
|
2136
2426
|
const fileArg = args.find((arg) => arg.startsWith("--file="));
|
|
2137
2427
|
if (!fileArg)
|
|
2138
2428
|
return {
|
|
@@ -2218,9 +2508,39 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
2218
2508
|
};
|
|
2219
2509
|
}
|
|
2220
2510
|
const output = args.includes("--task") ? retrieveTaskContext(store) : composeContext(store);
|
|
2511
|
+
const stageArg = args.find((argument) => argument.startsWith("--stage="))?.slice(8);
|
|
2512
|
+
const digestLines = [];
|
|
2513
|
+
if (fs.exists(join(aiw, "workflow.yml"))) {
|
|
2514
|
+
for (const stage of processStages(readWorkflowState(aiw, fs, false).ledger)) {
|
|
2515
|
+
const artifactPath = approvalArtifactPath(aiw, stage);
|
|
2516
|
+
const freshness = digestFreshness(aiw, fs, stage, artifactPath ?? undefined);
|
|
2517
|
+
if (freshness.state !== "missing")
|
|
2518
|
+
digestLines.push(formatDigestLine(freshness));
|
|
2519
|
+
}
|
|
2520
|
+
}
|
|
2521
|
+
const budgetArg = args.find((argument) => argument.startsWith("--budget="))?.slice(9);
|
|
2522
|
+
if (stageArg) {
|
|
2523
|
+
const budget = Number(budgetArg ?? parseContextBudgets(aiw, fs)[stageArg]);
|
|
2524
|
+
if (Number.isFinite(budget) && budget > 0) {
|
|
2525
|
+
const estimated = Math.ceil(output.length / 4);
|
|
2526
|
+
if (estimated > budget)
|
|
2527
|
+
throw new BlockingError(describeBlockingReason("context_budget_exceeded", {
|
|
2528
|
+
stage: stageArg,
|
|
2529
|
+
detail: `Stage ${stageArg} context is ~${estimated} tokens, over the ${budget}-token ceiling.`,
|
|
2530
|
+
}));
|
|
2531
|
+
}
|
|
2532
|
+
const digestBlock = digestLines.length > 0 ? `\n${digestLines.join("\n")}` : "";
|
|
2533
|
+
return {
|
|
2534
|
+
exitCode: 0,
|
|
2535
|
+
output: `${output}${digestBlock}\n\nstage: ${stageArg} · ~${Math.ceil(output.length / 4)} tokens${Number.isFinite(budget) && budget > 0 ? ` · ceiling ${budget}` : " · no ceiling declared"}`,
|
|
2536
|
+
};
|
|
2537
|
+
}
|
|
2221
2538
|
return { exitCode: 0, output };
|
|
2222
2539
|
}
|
|
2223
|
-
return {
|
|
2540
|
+
return {
|
|
2541
|
+
exitCode: 1,
|
|
2542
|
+
error: "Usage: aiw context [--layer=project --content=text] | aiw context --stage=<name> [--budget=tokens]",
|
|
2543
|
+
};
|
|
2224
2544
|
}
|
|
2225
2545
|
if (command === "context-summary") {
|
|
2226
2546
|
const summaryArg = args.find((arg) => arg.startsWith("--summary="))?.slice(10);
|
|
@@ -2238,7 +2558,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
2238
2558
|
}
|
|
2239
2559
|
if (command === "routing") {
|
|
2240
2560
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
2241
|
-
return
|
|
2561
|
+
return installationRequired();
|
|
2242
2562
|
const usage = "Usage: aiw routing init | show | resolve --category=name";
|
|
2243
2563
|
const action = args[1];
|
|
2244
2564
|
const routingPath = join(aiw, ROUTING_TABLE_PATH);
|
|
@@ -2294,7 +2614,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
2294
2614
|
}
|
|
2295
2615
|
if (command === "task-usage") {
|
|
2296
2616
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
2297
|
-
return
|
|
2617
|
+
return installationRequired();
|
|
2298
2618
|
const parsed = parseNamedOptions(args.slice(1), ["workflow"]);
|
|
2299
2619
|
if (!parsed)
|
|
2300
2620
|
return { exitCode: 1, error: "Usage: aiw task-usage [--workflow=id-or-ticket]" };
|
|
@@ -2323,7 +2643,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
2323
2643
|
}
|
|
2324
2644
|
if (command === "cost-report") {
|
|
2325
2645
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
2326
|
-
return
|
|
2646
|
+
return installationRequired();
|
|
2327
2647
|
const sidecarPath = join(aiw, USAGE_SIDECAR_PATH);
|
|
2328
2648
|
if (!fs.exists(sidecarPath))
|
|
2329
2649
|
return {
|
|
@@ -2345,7 +2665,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
2345
2665
|
}
|
|
2346
2666
|
if (command === "heartbeat") {
|
|
2347
2667
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
2348
|
-
return
|
|
2668
|
+
return installationRequired();
|
|
2349
2669
|
const parsed = parseNamedOptions(args.slice(1), ["session", "warn", "ttl"]);
|
|
2350
2670
|
if (!parsed)
|
|
2351
2671
|
return {
|
|
@@ -2395,7 +2715,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
2395
2715
|
}
|
|
2396
2716
|
if (command === "hooks") {
|
|
2397
2717
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
2398
|
-
return
|
|
2718
|
+
return installationRequired();
|
|
2399
2719
|
const action = args[1];
|
|
2400
2720
|
if (action !== "install")
|
|
2401
2721
|
return {
|
|
@@ -2439,7 +2759,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
2439
2759
|
}
|
|
2440
2760
|
if (command === "usage-sidecar") {
|
|
2441
2761
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
2442
|
-
return
|
|
2762
|
+
return installationRequired();
|
|
2443
2763
|
const usage = "Usage: aiw usage-sidecar record --command=name (--cache-read=N | --fresh-input=N | --cache-write=N | --output=N) [--model=name] [--harness=name] [--harness-version=text] [--estimated] | aiw usage-sidecar report";
|
|
2444
2764
|
const action = args[1];
|
|
2445
2765
|
const sidecarPath = join(aiw, USAGE_SIDECAR_PATH);
|
|
@@ -2648,7 +2968,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
2648
2968
|
}
|
|
2649
2969
|
if (command === "self-validate") {
|
|
2650
2970
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
2651
|
-
return
|
|
2971
|
+
return installationRequired();
|
|
2652
2972
|
const ticket = args.find((arg) => arg.startsWith("--ticket="))?.slice(9);
|
|
2653
2973
|
if (!ticket)
|
|
2654
2974
|
return { exitCode: 1, error: "Usage: aiw self-validate --ticket=TYPE-000" };
|
|
@@ -2684,7 +3004,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
2684
3004
|
}
|
|
2685
3005
|
return {
|
|
2686
3006
|
exitCode: 0,
|
|
2687
|
-
output: "aiw install [--target target] | workflow <start|status|complete> [--id=id] | task <start|status> --ticket=TEAM-123 [--title=text] | approve <stage> | reject <stage> --reason=text | skip <optional-stage> --reason=text | ui [--port=number] | orchestrate --plan=path [--execute] [--max-parallel=number] | telemetry <status|show|enable|disable> [privacy options] | plugin <create|validate> [options] | scan | semantic <status|query> | decision <status|triage> | self-validate --ticket=TYPE-000 | organization-policy --file=path [--replace] | policy-check --package=path [--package=path] | preset --file=path [--replace] | context | context-summary | context-quality | capabilities [--target=target] | token-usage [--stage=name --budget=number --used=number] | registry [--search=query] [--private=name] | registry configure --file=path [--replace] | skills <search|inspect|install|check|update> [options] | audit-package --package=path | verify-package --package=path --checksum=sha256 | brainstorm [--title=title] | spec [--title=title] | adr [--id=id --title=title] | plan [--title=title] | verify | trace | gate <stage> | recommend [--select=id,id] | status | doctor | repair | uninstall [--dry-run] | target <target> | rollback | confirm | resolve --package=path | update --package=path --target=target | validate [--package=path]",
|
|
3007
|
+
output: "aiw install [--target target] | workflow <start|status|complete> [--id=id] | task <start|status> --ticket=TEAM-123 [--title=text] | approve <stage> | reject <stage> --reason=text | skip <optional-stage> --reason=text | ui [--port=number] | orchestrate --plan=path [--execute] [--max-parallel=number] | telemetry <status|show|enable|disable> [privacy options] | plugin <create|validate> [options] | scan | semantic <status|query> | decision <status|triage> | self-validate --ticket=TYPE-000 | organization-policy --file=path [--replace] | policy-check --package=path [--package=path] | preset --file=path [--replace] | context | context-summary | context-quality | capabilities [--target=target] | token-usage [--stage=name --budget=number --used=number] | registry [--search=query] [--private=name] | registry configure --file=path [--replace] | skills <search|inspect|install|check|update> [options] | audit-package --package=path | verify-package --package=path --checksum=sha256 | brainstorm [--title=title] | spec [--title=title] | adr [--id=id --title=title] | plan [--title=title] | verify | trace | gate <stage> | standards <extract|list|confirm|reject> | recommend [--select=id,id] | status | doctor | repair | uninstall [--dry-run] | target <target> | rollback | confirm | resolve --package=path | update --package=path --target=target | validate [--package=path]",
|
|
2688
3008
|
};
|
|
2689
3009
|
}
|
|
2690
3010
|
catch (error) {
|
|
@@ -2702,10 +3022,14 @@ function readWorkflowState(aiw, fs, requireActive = true) {
|
|
|
2702
3022
|
const session = parseWorkflowSession(fs.read(sessionPath));
|
|
2703
3023
|
let ledger = parseApprovalLedger(fs.read(ledgerPath));
|
|
2704
3024
|
if (session.workflowId !== ledger.workflowId)
|
|
2705
|
-
throw new
|
|
3025
|
+
throw new BlockingError(describeBlockingReason("state_mismatch", {
|
|
3026
|
+
detail: "Workflow session and approval ledger IDs do not match; run doctor.",
|
|
3027
|
+
}));
|
|
2706
3028
|
if (requireActive && session.status !== "active")
|
|
2707
|
-
throw new
|
|
2708
|
-
|
|
3029
|
+
throw new BlockingError(describeBlockingReason("workflow_complete", {
|
|
3030
|
+
detail: `Workflow ${session.workflowId} is complete; start a new workflow first.`,
|
|
3031
|
+
}));
|
|
3032
|
+
const changed = processStages(ledger).find((stage) => {
|
|
2709
3033
|
const record = ledger.stages[stage];
|
|
2710
3034
|
if (!record?.artifactHash || !["pending-human", "approved"].includes(record.status))
|
|
2711
3035
|
return false;
|
|
@@ -2733,6 +3057,85 @@ function workflowNextAction(ledger) {
|
|
|
2733
3057
|
return `Next action: revise ${next}, rerun its gate, and request human approval.`;
|
|
2734
3058
|
return `Next action: prepare ${next} evidence.`;
|
|
2735
3059
|
}
|
|
3060
|
+
function parseContextBudgets(aiw, fs) {
|
|
3061
|
+
const path = join(aiw, "budgets.yml");
|
|
3062
|
+
if (!fs.exists(path))
|
|
3063
|
+
return {};
|
|
3064
|
+
const value = parseYaml(fs.read(path));
|
|
3065
|
+
if (!value || typeof value !== "object")
|
|
3066
|
+
return {};
|
|
3067
|
+
const stages = value.stages;
|
|
3068
|
+
if (!stages || typeof stages !== "object")
|
|
3069
|
+
return {};
|
|
3070
|
+
const budgets = {};
|
|
3071
|
+
for (const [stage, ceiling] of Object.entries(stages)) {
|
|
3072
|
+
const n = Number(ceiling);
|
|
3073
|
+
if (Number.isFinite(n) && n > 0)
|
|
3074
|
+
budgets[stage] = n;
|
|
3075
|
+
}
|
|
3076
|
+
return budgets;
|
|
3077
|
+
}
|
|
3078
|
+
const PLAN_PAIR_PATTERN = /Validation:\s*([^→\n]+)→\s*([^\n]+)/g;
|
|
3079
|
+
const EXIT_PATTERN = /exit\s+(\d+)/i;
|
|
3080
|
+
function normalizeCommand(command) {
|
|
3081
|
+
return command.trim().replace(/\s+/g, " ").toLowerCase();
|
|
3082
|
+
}
|
|
3083
|
+
/**
|
|
3084
|
+
* OPP-014 paired verification: plan tasks declare `Validation: <command> →
|
|
3085
|
+
* <expected>`; reports record `Validation: <command> → <observed>`. The gate
|
|
3086
|
+
* compares the pairs — command match, observed presence, exit-code agreement.
|
|
3087
|
+
* Legacy plans without pair arrows are accepted unchanged (ratchet).
|
|
3088
|
+
*/
|
|
3089
|
+
export function compareVerificationPairs(aiw, fs, stage) {
|
|
3090
|
+
const planPath = join(aiw, "generated/plans/implementation-plan.md");
|
|
3091
|
+
if (!fs.exists(planPath))
|
|
3092
|
+
return [];
|
|
3093
|
+
const plan = fs.read(planPath);
|
|
3094
|
+
if (!plan.includes("→"))
|
|
3095
|
+
return [];
|
|
3096
|
+
const declared = [];
|
|
3097
|
+
for (const blockMatch of plan.matchAll(/TASK-\d+[\s\S]*?(?=TASK-\d+|$)/g)) {
|
|
3098
|
+
const block = blockMatch[0];
|
|
3099
|
+
const taskId = /^\s*(TASK-\d+)/.exec(block)?.[1] ?? "TASK-?";
|
|
3100
|
+
for (const pair of block.matchAll(PLAN_PAIR_PATTERN)) {
|
|
3101
|
+
const command = pair[1].trim();
|
|
3102
|
+
if (!command)
|
|
3103
|
+
continue;
|
|
3104
|
+
declared.push({
|
|
3105
|
+
task: taskId,
|
|
3106
|
+
command,
|
|
3107
|
+
expectedExitText: EXIT_PATTERN.exec(pair[2])?.[1],
|
|
3108
|
+
raw: pair[2].trim(),
|
|
3109
|
+
});
|
|
3110
|
+
}
|
|
3111
|
+
}
|
|
3112
|
+
if (declared.length === 0)
|
|
3113
|
+
return [];
|
|
3114
|
+
const reportPath = stage === "implementation"
|
|
3115
|
+
? join(aiw, "generated/reports/implementation-report.md")
|
|
3116
|
+
: join(aiw, "generated/reports/verification-report.md");
|
|
3117
|
+
const issues = [];
|
|
3118
|
+
const observed = new Map();
|
|
3119
|
+
if (fs.exists(reportPath)) {
|
|
3120
|
+
for (const line of fs.read(reportPath).split("\n")) {
|
|
3121
|
+
const match = /Validation:\s*([^→\n]+)→\s*([^\n]+)/.exec(line);
|
|
3122
|
+
if (match)
|
|
3123
|
+
observed.set(normalizeCommand(match[1]), match[2].trim());
|
|
3124
|
+
}
|
|
3125
|
+
}
|
|
3126
|
+
for (const pair of declared) {
|
|
3127
|
+
const key = normalizeCommand(pair.command);
|
|
3128
|
+
if (!observed.has(key)) {
|
|
3129
|
+
issues.push(`[verification_pair_missing] ${pair.task}: \`${pair.command}\``);
|
|
3130
|
+
continue;
|
|
3131
|
+
}
|
|
3132
|
+
const observedExit = EXIT_PATTERN.exec(observed.get(key) ?? "")?.[1];
|
|
3133
|
+
if (pair.expectedExitText && observedExit && pair.expectedExitText !== observedExit) {
|
|
3134
|
+
issues.push(`[verification_pair_mismatch] ${pair.task}: expected exit ${pair.expectedExitText}, observed exit ${observedExit}`);
|
|
3135
|
+
}
|
|
3136
|
+
}
|
|
3137
|
+
return issues;
|
|
3138
|
+
}
|
|
2736
3139
|
function parseLightEvidence(content) {
|
|
2737
3140
|
const value = parseYaml(content);
|
|
2738
3141
|
if (!value || typeof value !== "object" || value.schema !== 1)
|
|
@@ -2753,6 +3156,11 @@ function parseLightEvidence(content) {
|
|
|
2753
3156
|
}
|
|
2754
3157
|
function assertStageReady(aiw, fs, stage) {
|
|
2755
3158
|
const { session, ledger } = readActiveWorkflow(aiw, fs);
|
|
3159
|
+
if (!processStages(ledger).includes(stage))
|
|
3160
|
+
throw new BlockingError(describeBlockingReason("stage_not_in_process", {
|
|
3161
|
+
stage,
|
|
3162
|
+
detail: `Stage ${stage} is not part of the ${ledger.process ?? "feature"} process for workflow ${session.workflowId}.`,
|
|
3163
|
+
}));
|
|
2756
3164
|
if (effectiveLane(session) === "light")
|
|
2757
3165
|
throw new BlockingError(describeBlockingReason("lane_escalation_required", {
|
|
2758
3166
|
stage,
|
|
@@ -2796,6 +3204,8 @@ function approvalArtifactPath(aiw, stage) {
|
|
|
2796
3204
|
verification: "generated/reports/verification-report.md",
|
|
2797
3205
|
review: "generated/reports/code-review.md",
|
|
2798
3206
|
traceability: "generated/artifacts/traceability.yml",
|
|
3207
|
+
diagnosis: "generated/reports/diagnosis.md",
|
|
3208
|
+
assessment: "generated/reports/assessment.md",
|
|
2799
3209
|
};
|
|
2800
3210
|
return resolve(aiw, paths[stage]);
|
|
2801
3211
|
}
|