@multileaf/ai-workflow 0.11.0 → 0.13.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 +25 -0
- package/dist/blocking-reasons.d.ts +1 -1
- package/dist/blocking-reasons.js +30 -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 +13 -2
- package/dist/human-approval.js +43 -8
- package/dist/legacy-migration.d.ts +45 -0
- package/dist/legacy-migration.js +175 -0
- 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 +504 -59
- package/docs/backlog.md +13 -3
- package/docs/continuation-inventory.md +21 -16
- 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/OPP-016-behavioral-eval-scenarios.md +44 -0
- package/docs/opportunities/OPP-017-orchestration-depth.md +50 -0
- package/docs/opportunities/OPP-018-legacy-migration.md +59 -0
- package/docs/opportunities/README.md +32 -10
- package/docs/release-process.md +1 -1
- 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,14 +65,45 @@ 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 { applyMigration, detectLegacyState, planLegacyMigration, rollbackMigration, } from "./legacy-migration.js";
|
|
71
|
+
import { extractStandards, parseStandardsIndex } from "./standards.js";
|
|
72
|
+
import { digestPath, digestFreshness, extractDigest, formatDigestLine, serializeDigest, } from "./digest.js";
|
|
73
|
+
function installationRequired() {
|
|
74
|
+
return {
|
|
75
|
+
exitCode: 1,
|
|
76
|
+
error: formatBlockingOutput(describeBlockingReason("installation_required", {
|
|
77
|
+
detail: "Run `aiw install` first.",
|
|
78
|
+
})),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function stateMismatch(detail) {
|
|
82
|
+
return {
|
|
83
|
+
exitCode: 1,
|
|
84
|
+
error: formatBlockingOutput(describeBlockingReason("state_mismatch", { detail })),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
70
87
|
import { parseMigrationCheckpoint, planNeutralResourceMigration, readResourceBytes, serializeMigrationPreview, serializeMigrationCheckpoint, validateNeutralResourceDocument, writeResourceBytes, } from "./migration.js";
|
|
71
88
|
import { parsePrivateRegistries, parseTeamPreset, serializePrivateRegistries, serializeTeamPreset, fetchPrivateRegistryClient, } from "./team-configuration.js";
|
|
72
89
|
import { enforcePolicyGate, gitPolicyTracking } from "./policy-gate.js";
|
|
73
90
|
import { DEFAULT_TELEMETRY_CONFIG, loadTelemetryConfig, offerTelemetryConsent, parseTelemetryConfig, purgeTelemetryBuffer, readTelemetryBuffer, serializeTelemetryMapping, TELEMETRY_BUFFER_CAP, TELEMETRY_BUFFER_FILE, } from "./telemetry.js";
|
|
74
91
|
import { createPluginScaffold, validatePluginDirectory, writePluginScaffold, } from "./plugin-sdk.js";
|
|
75
|
-
import { executeOrchestrationPlan, parseOrchestrationPlan, serializeOrchestrationPreview, serializeOrchestrationResults, validateOrchestrationParallelism, } from "./orchestration.js";
|
|
92
|
+
import { createTaskWorktrees, executeOrchestrationPlan, listManagedWorktrees, removeWorktrees, parseOrchestrationPlan, serializeOrchestrationPreview, serializeOrchestrationResults, validateOrchestrationParallelism, } from "./orchestration.js";
|
|
93
|
+
function laneAdvisory(root, fs, lightChosen) {
|
|
94
|
+
const paths = collectWorkingTreeChanges(root);
|
|
95
|
+
if (!paths || paths.length === 0)
|
|
96
|
+
return undefined;
|
|
97
|
+
const assessment = assessRisk(paths.map((path) => ({ path })), { hasCoveringTest: hasCoveringTest(root, fs) });
|
|
98
|
+
if (lightChosen && assessment.tier === "high")
|
|
99
|
+
return `advisory: risk assess reports high for the current changes (${assessment.reasons
|
|
100
|
+
.slice(0, 2)
|
|
101
|
+
.map((reason) => reason.code)
|
|
102
|
+
.join(", ")}); consider the full lane, or escalate later with \`aiw task escalate --reason=...\`.`;
|
|
103
|
+
if (!lightChosen && assessment.tier === "passive")
|
|
104
|
+
return "advisory: current changes look passive; the light lane (--lane=light --lane-reason=...) may be viable for small work.";
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
76
107
|
function beginWorkflowSession(fs, aiw, options) {
|
|
77
108
|
const sessionPath = join(aiw, "workflow.yml");
|
|
78
109
|
const ledgerPath = join(aiw, "approvals.yml");
|
|
@@ -83,7 +114,7 @@ function beginWorkflowSession(fs, aiw, options) {
|
|
|
83
114
|
detail: `Workflow ${previous.workflowId} is still active. Complete it before starting another.`,
|
|
84
115
|
}));
|
|
85
116
|
if (!fs.exists(ledgerPath))
|
|
86
|
-
return
|
|
117
|
+
return stateMismatch("Completed workflow is missing its approval ledger.");
|
|
87
118
|
const archive = join(aiw, `checkpoints/workflow-${previous.workflowId}-approvals.yml`);
|
|
88
119
|
if (fs.exists(archive))
|
|
89
120
|
return { exitCode: 1, error: `Workflow approval archive already exists: ${archive}` };
|
|
@@ -93,7 +124,7 @@ function beginWorkflowSession(fs, aiw, options) {
|
|
|
93
124
|
else if (fs.exists(ledgerPath)) {
|
|
94
125
|
return {
|
|
95
126
|
exitCode: 1,
|
|
96
|
-
error: "Approval ledger exists without a workflow session; run doctor.",
|
|
127
|
+
error: stateMismatch("Approval ledger exists without a workflow session; run doctor.").error,
|
|
97
128
|
};
|
|
98
129
|
}
|
|
99
130
|
const workflowId = options.id ?? randomUUID();
|
|
@@ -120,6 +151,7 @@ function beginWorkflowSession(fs, aiw, options) {
|
|
|
120
151
|
...(options.lane === "light" && options.laneReason
|
|
121
152
|
? { lane: "light", laneReason: options.laneReason }
|
|
122
153
|
: {}),
|
|
154
|
+
...(options.process && options.process !== "feature" ? { process: options.process } : {}),
|
|
123
155
|
});
|
|
124
156
|
fs.write(ledgerPath, serializeApprovalLedger(ledger));
|
|
125
157
|
fs.write(sessionPath, serializeWorkflowSession(session));
|
|
@@ -132,7 +164,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
132
164
|
const command = args[0] ?? "help";
|
|
133
165
|
if (command === "task") {
|
|
134
166
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
135
|
-
return
|
|
167
|
+
return installationRequired();
|
|
136
168
|
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";
|
|
137
169
|
const action = args[1];
|
|
138
170
|
const sessionPath = join(aiw, "workflow.yml");
|
|
@@ -143,10 +175,24 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
143
175
|
"id",
|
|
144
176
|
"lane",
|
|
145
177
|
"lane-reason",
|
|
178
|
+
"process",
|
|
146
179
|
]);
|
|
147
180
|
if (!options?.ticket)
|
|
148
181
|
return { exitCode: 1, error: usage };
|
|
149
182
|
validateTaskTicket(options.ticket);
|
|
183
|
+
const processName = options.process;
|
|
184
|
+
if (processName !== undefined) {
|
|
185
|
+
if (!isWorkflowProcess(processName))
|
|
186
|
+
return {
|
|
187
|
+
exitCode: 1,
|
|
188
|
+
error: `Unknown process: ${processName}. Known processes: ${PROCESS_IDS.join(", ")}.`,
|
|
189
|
+
};
|
|
190
|
+
if (processName === "feature")
|
|
191
|
+
return {
|
|
192
|
+
exitCode: 1,
|
|
193
|
+
error: "The feature process is the default; omit --process for it.",
|
|
194
|
+
};
|
|
195
|
+
}
|
|
150
196
|
if (options.lane !== undefined && options.lane !== "light")
|
|
151
197
|
return { exitCode: 1, error: "Only --lane=light exists; omit it for the full lane." };
|
|
152
198
|
const laneReason = options["lane-reason"];
|
|
@@ -176,14 +222,17 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
176
222
|
ticket: options.ticket,
|
|
177
223
|
...(options.title ? { title: options.title } : {}),
|
|
178
224
|
...(lane ? { lane, laneReason: laneReason } : {}),
|
|
225
|
+
...(processName ? { process: processName } : {}),
|
|
179
226
|
});
|
|
180
227
|
if (result.exitCode !== 0)
|
|
181
228
|
return result;
|
|
229
|
+
const advisory = laneAdvisory(root, fs, lane === "light");
|
|
182
230
|
const title = options.title ? ` titled "${options.title}"` : "";
|
|
183
231
|
if (lane)
|
|
184
232
|
return {
|
|
185
233
|
exitCode: 0,
|
|
186
234
|
output: `Task started for ticket ${options.ticket}${title} (light lane): ${result.output}\n` +
|
|
235
|
+
(advisory ? `${advisory}\n` : "") +
|
|
187
236
|
"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
237
|
`Rationale recorded: ${laneReason}\n` +
|
|
189
238
|
'No durable spec/plan artifacts are required. Record validation with `aiw task evidence --validation="<command and result>"`, then complete with `aiw workflow complete`. If scope grows, escalate with `aiw task escalate --reason=...`.',
|
|
@@ -191,6 +240,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
191
240
|
return {
|
|
192
241
|
exitCode: 0,
|
|
193
242
|
output: `Task started for ticket ${options.ticket}${title}: ${result.output}\n` +
|
|
243
|
+
(advisory ? `${advisory}\n` : "") +
|
|
194
244
|
`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=...\`.`,
|
|
195
245
|
};
|
|
196
246
|
}
|
|
@@ -287,7 +337,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
287
337
|
...lane,
|
|
288
338
|
`Workflow: ${session.workflowId} (${session.status})`,
|
|
289
339
|
"Stages:",
|
|
290
|
-
...
|
|
340
|
+
...processStages(ledger).map((stage) => `- ${stage}: ${ledger.stages[stage]?.status ?? "not-started"}`),
|
|
291
341
|
];
|
|
292
342
|
if (session.status === "active" && effectiveLane(session) === "light") {
|
|
293
343
|
lines.push('Next: record validation with `aiw task evidence --validation="<command and result>"`, then `aiw workflow complete`. If scope grows: `aiw task escalate --reason=...`.');
|
|
@@ -302,20 +352,32 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
302
352
|
}
|
|
303
353
|
if (command === "workflow") {
|
|
304
354
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
305
|
-
return
|
|
355
|
+
return installationRequired();
|
|
306
356
|
const action = args[1];
|
|
307
357
|
const sessionPath = join(aiw, "workflow.yml");
|
|
308
358
|
if (action === "start") {
|
|
309
|
-
const options = parseNamedOptions(args.slice(2).filter((argument) => argument !== "--json"), ["id"]);
|
|
359
|
+
const options = parseNamedOptions(args.slice(2).filter((argument) => argument !== "--json"), ["id", "process"]);
|
|
310
360
|
if (!options)
|
|
311
|
-
return {
|
|
312
|
-
|
|
361
|
+
return {
|
|
362
|
+
exitCode: 1,
|
|
363
|
+
error: "Usage: aiw workflow start [--id=workflow-id] [--process=bugfix|idea-assessment]",
|
|
364
|
+
};
|
|
365
|
+
const processName = options.process;
|
|
366
|
+
if (processName !== undefined && !isWorkflowProcess(processName))
|
|
367
|
+
return {
|
|
368
|
+
exitCode: 1,
|
|
369
|
+
error: `Unknown process: ${processName}. Known processes: ${PROCESS_IDS.join(", ")}.`,
|
|
370
|
+
};
|
|
371
|
+
return beginWorkflowSession(fs, aiw, {
|
|
372
|
+
id: options.id,
|
|
373
|
+
...(processName ? { process: processName } : {}),
|
|
374
|
+
});
|
|
313
375
|
}
|
|
314
376
|
if (action === "status") {
|
|
315
377
|
if (!fs.exists(sessionPath))
|
|
316
378
|
return { exitCode: 0, output: "No SDD workflow is active." };
|
|
317
379
|
const { session, ledger } = readWorkflowState(aiw, fs, false);
|
|
318
|
-
const stages =
|
|
380
|
+
const stages = processStages(ledger).map((stage) => `- ${stage}: ${ledger.stages[stage]?.status ?? "not-started"}`);
|
|
319
381
|
return {
|
|
320
382
|
exitCode: 0,
|
|
321
383
|
output: `Workflow ${session.workflowId} (${session.status})\n${stages.join("\n")}\n${workflowNextAction(ledger)}`,
|
|
@@ -382,7 +444,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
382
444
|
output: `Workflow completed (light lane): ${session.workflowId}. Evidence: ${evidencePath}`,
|
|
383
445
|
};
|
|
384
446
|
}
|
|
385
|
-
const unresolved =
|
|
447
|
+
const unresolved = processStages(ledger).filter((stage) => ledger.stages[stage]?.status !== "approved" &&
|
|
386
448
|
ledger.stages[stage]?.status !== "skipped");
|
|
387
449
|
if (unresolved.length)
|
|
388
450
|
return {
|
|
@@ -457,7 +519,12 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
457
519
|
updatedAt: new Date().toISOString(),
|
|
458
520
|
});
|
|
459
521
|
fs.write(join(aiw, "approvals.yml"), serializeApprovalLedger(updated));
|
|
460
|
-
|
|
522
|
+
fs.mkdir(join(aiw, "digests"));
|
|
523
|
+
fs.write(digestPath(aiw, workflowStage), serializeDigest(extractDigest(workflowStage, fs.read(artifact), artifactHash)));
|
|
524
|
+
return {
|
|
525
|
+
exitCode: 0,
|
|
526
|
+
output: `Human approval recorded for ${workflowStage}. Digest available: aiw digest --stage=${workflowStage}`,
|
|
527
|
+
};
|
|
461
528
|
}
|
|
462
529
|
const options = parseNamedOptions(rest, ["reason"]);
|
|
463
530
|
if (!options?.reason)
|
|
@@ -471,7 +538,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
471
538
|
}
|
|
472
539
|
if (command === "ui") {
|
|
473
540
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
474
|
-
return
|
|
541
|
+
return installationRequired();
|
|
475
542
|
const parsed = parseNamedOptions(args.slice(1), ["port"]);
|
|
476
543
|
if (!parsed || (parsed.port !== undefined && !/^\d+$/.test(parsed.port)))
|
|
477
544
|
return { exitCode: 1, error: "Usage: aiw ui [--port=number]" };
|
|
@@ -485,20 +552,36 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
485
552
|
}
|
|
486
553
|
if (command === "orchestrate") {
|
|
487
554
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
488
|
-
return
|
|
555
|
+
return installationRequired();
|
|
489
556
|
const executeCount = args.filter((arg) => arg === "--execute").length;
|
|
490
557
|
if (executeCount > 1)
|
|
491
558
|
return {
|
|
492
559
|
exitCode: 1,
|
|
493
|
-
error: "Usage: aiw orchestrate --plan=path [--execute] [--max-parallel=number]",
|
|
560
|
+
error: "Usage: aiw orchestrate --plan=path [--execute] [--isolate] [--cleanup] [--max-parallel=number]",
|
|
494
561
|
};
|
|
495
562
|
const execute = executeCount === 1;
|
|
496
|
-
const
|
|
563
|
+
const isolate = args.includes("--isolate");
|
|
564
|
+
const cleanup = args.includes("--cleanup");
|
|
565
|
+
if (cleanup) {
|
|
566
|
+
const leaked = listManagedWorktrees(root, aiw);
|
|
567
|
+
if (leaked.length === 0)
|
|
568
|
+
return { exitCode: 0, output: "No managed worktrees to clean." };
|
|
569
|
+
if (!execute)
|
|
570
|
+
return {
|
|
571
|
+
exitCode: 0,
|
|
572
|
+
output: `Managed worktrees found:\n${leaked.map((path) => `- ${path}`).join("\n")}\nRe-run with --execute to remove them.`,
|
|
573
|
+
};
|
|
574
|
+
removeWorktrees(root, leaked, { force: true });
|
|
575
|
+
return { exitCode: 0, output: `Removed ${leaked.length} managed worktree(s).` };
|
|
576
|
+
}
|
|
577
|
+
const named = args
|
|
578
|
+
.slice(1)
|
|
579
|
+
.filter((arg) => arg !== "--execute" && arg !== "--isolate" && arg !== "--cleanup");
|
|
497
580
|
const parsed = parseNamedOptions(named, ["plan", "max-parallel"]);
|
|
498
581
|
if (!parsed?.plan)
|
|
499
582
|
return {
|
|
500
583
|
exitCode: 1,
|
|
501
|
-
error: "Usage: aiw orchestrate --plan=path [--execute] [--max-parallel=number]",
|
|
584
|
+
error: "Usage: aiw orchestrate --plan=path [--execute] [--isolate] [--cleanup] [--max-parallel=number]",
|
|
502
585
|
};
|
|
503
586
|
const planPath = safeProjectFile(root, parsed.plan, fs);
|
|
504
587
|
const plan = parseOrchestrationPlan(fs.read(planPath));
|
|
@@ -517,7 +600,19 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
517
600
|
error: `Orchestration checkpoint already exists; use a new plan id: ${checkpoint}`,
|
|
518
601
|
};
|
|
519
602
|
try {
|
|
520
|
-
const
|
|
603
|
+
const isolation = isolate
|
|
604
|
+
? {
|
|
605
|
+
options: { root, aiw, planId: plan.id },
|
|
606
|
+
worktrees: createTaskWorktrees(root, aiw, plan),
|
|
607
|
+
}
|
|
608
|
+
: undefined;
|
|
609
|
+
const results = await executeOrchestrationPlan(plan, services.orchestrator, maxParallel, isolation);
|
|
610
|
+
if (isolation) {
|
|
611
|
+
const passedPaths = results
|
|
612
|
+
.filter(({ status, worktree }) => status === "passed" && worktree)
|
|
613
|
+
.map(({ worktree }) => worktree);
|
|
614
|
+
removeWorktrees(root, passedPaths);
|
|
615
|
+
}
|
|
521
616
|
fs.write(checkpoint, serializeOrchestrationResults(plan, results));
|
|
522
617
|
const failed = results.filter(({ status }) => status !== "passed");
|
|
523
618
|
return failed.length
|
|
@@ -542,9 +637,12 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
542
637
|
if (fs.pathType?.(manifestPath) === "file") {
|
|
543
638
|
const manifest = fs.read(manifestPath);
|
|
544
639
|
const active = manifest.match(/^\s*active:\s*(.+)$/m)?.[1]?.trim() ?? "unknown";
|
|
640
|
+
const legacyHint = detectLegacyState(aiw, fs)
|
|
641
|
+
? "\nLegacy pre-ownership state detected. Run `aiw migrate --dry-run` to review, then `aiw migrate` to upgrade to the current state layout."
|
|
642
|
+
: "";
|
|
545
643
|
return {
|
|
546
644
|
exitCode: 0,
|
|
547
|
-
output: `AI Workflow is already installed for target: ${active}. Existing state preserved
|
|
645
|
+
output: `AI Workflow is already installed for target: ${active}. Existing state preserved.${legacyHint}`,
|
|
548
646
|
};
|
|
549
647
|
}
|
|
550
648
|
const aiInitPath = join(root, renderAiInitPath(target));
|
|
@@ -640,7 +738,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
640
738
|
}
|
|
641
739
|
if (command === "telemetry") {
|
|
642
740
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
643
|
-
return
|
|
741
|
+
return installationRequired();
|
|
644
742
|
const usage = "Usage: aiw telemetry <status|show|enable|disable> [--verbose] [--commands=include|exclude] [--outcomes=include|exclude]";
|
|
645
743
|
const action = args[1];
|
|
646
744
|
if (!action)
|
|
@@ -753,7 +851,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
753
851
|
}
|
|
754
852
|
if (command === "context-bench") {
|
|
755
853
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
756
|
-
return
|
|
854
|
+
return installationRequired();
|
|
757
855
|
const saveCount = args.filter((arg) => arg === "--save").length;
|
|
758
856
|
if (saveCount > 1)
|
|
759
857
|
return { exitCode: 1, error: "Usage: aiw context-bench [--baseline=path] [--save]" };
|
|
@@ -798,7 +896,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
798
896
|
}
|
|
799
897
|
if (command === "parity") {
|
|
800
898
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
801
|
-
return
|
|
899
|
+
return installationRequired();
|
|
802
900
|
const parsed = parseNamedOptions(args.slice(1), ["tasks"]);
|
|
803
901
|
if (!parsed)
|
|
804
902
|
return { exitCode: 1, error: "Usage: aiw parity [--tasks=directory]" };
|
|
@@ -852,7 +950,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
852
950
|
}
|
|
853
951
|
if (command === "scan") {
|
|
854
952
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
855
|
-
return
|
|
953
|
+
return installationRequired();
|
|
856
954
|
const profile = await (services.profiler?.profile(root) ?? profileProject(root));
|
|
857
955
|
let interpreter = services.interpreter;
|
|
858
956
|
let interpreterModel;
|
|
@@ -1132,7 +1230,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
1132
1230
|
return { exitCode: 1, error: `Unsupported target: ${target}` };
|
|
1133
1231
|
const manifestPath = join(aiw, "manifest.yml");
|
|
1134
1232
|
if (!fs.exists(manifestPath))
|
|
1135
|
-
return
|
|
1233
|
+
return installationRequired();
|
|
1136
1234
|
const dryRun = args.includes("--dry-run");
|
|
1137
1235
|
const targetPath = join(root, renderAiInitPath(target));
|
|
1138
1236
|
const neutralAiInit = join(aiw, "resources/skills/ai-init.md");
|
|
@@ -1459,7 +1557,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
1459
1557
|
}
|
|
1460
1558
|
if (command === "confirm") {
|
|
1461
1559
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
1462
|
-
return
|
|
1560
|
+
return installationRequired();
|
|
1463
1561
|
const overrides = [];
|
|
1464
1562
|
const accept = args.indexOf("--accept");
|
|
1465
1563
|
if (accept >= 0 && args[accept + 1])
|
|
@@ -1483,7 +1581,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
1483
1581
|
}
|
|
1484
1582
|
if (command === "recommend") {
|
|
1485
1583
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
1486
|
-
return
|
|
1584
|
+
return installationRequired();
|
|
1487
1585
|
const profile = await (services.profiler?.profile(root) ?? profileProject(root));
|
|
1488
1586
|
const recommendations = services.recommender?.recommend(profile) ?? recommendCapabilities(profile);
|
|
1489
1587
|
let ordered = recommendations;
|
|
@@ -1551,7 +1649,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
1551
1649
|
}
|
|
1552
1650
|
if (command === "skills") {
|
|
1553
1651
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
1554
|
-
return
|
|
1652
|
+
return installationRequired();
|
|
1555
1653
|
const action = args[1];
|
|
1556
1654
|
const executor = services.externalSkills ?? nodeCommandExecutor;
|
|
1557
1655
|
const approvedPermissions = parseApprovedPermissions(args);
|
|
@@ -1615,7 +1713,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
1615
1713
|
}
|
|
1616
1714
|
if (command === "generate" || command === "sync") {
|
|
1617
1715
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
1618
|
-
return
|
|
1716
|
+
return installationRequired();
|
|
1619
1717
|
const profilePath = join(aiw, "profile.yml");
|
|
1620
1718
|
const persistedProfile = fs.exists(profilePath) ? fs.read(profilePath) : "";
|
|
1621
1719
|
const profile = /^status:\s*scanned$/m.test(persistedProfile)
|
|
@@ -1724,8 +1822,16 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
1724
1822
|
if (command === "doctor") {
|
|
1725
1823
|
const required = ["manifest.yml", "profile.yml", "lock.yml", "ownership.yml"];
|
|
1726
1824
|
const missing = required.filter((file) => !fs.exists(join(aiw, file)));
|
|
1727
|
-
if (missing.length)
|
|
1825
|
+
if (missing.length) {
|
|
1826
|
+
if (detectLegacyState(aiw, fs))
|
|
1827
|
+
return {
|
|
1828
|
+
exitCode: 1,
|
|
1829
|
+
error: formatBlockingOutput(describeBlockingReason("migration_required", {
|
|
1830
|
+
detail: "Legacy pre-ownership state detected; review the migration plan first.",
|
|
1831
|
+
})) + "\nNext: run `aiw migrate --dry-run`, then `aiw migrate`.",
|
|
1832
|
+
};
|
|
1728
1833
|
return { exitCode: 1, error: `Missing AI Workflow files: ${missing.join(", ")}` };
|
|
1834
|
+
}
|
|
1729
1835
|
try {
|
|
1730
1836
|
parseManifest(fs.read(join(aiw, "manifest.yml")));
|
|
1731
1837
|
parseOwnership(fs.read(join(aiw, "ownership.yml")));
|
|
@@ -1844,7 +1950,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
1844
1950
|
}
|
|
1845
1951
|
if (command === "brainstorm") {
|
|
1846
1952
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
1847
|
-
return
|
|
1953
|
+
return installationRequired();
|
|
1848
1954
|
assertStageReady(aiw, fs, "brainstorming");
|
|
1849
1955
|
const title = args.find((arg) => arg.startsWith("--title="))?.slice(8) || "Untitled Brainstorm";
|
|
1850
1956
|
const path = join(aiw, "generated/specs/brainstorm.md");
|
|
@@ -1853,7 +1959,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
1853
1959
|
}
|
|
1854
1960
|
if (command === "spec") {
|
|
1855
1961
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
1856
|
-
return
|
|
1962
|
+
return installationRequired();
|
|
1857
1963
|
assertStageReady(aiw, fs, "specification");
|
|
1858
1964
|
const title = args.find((arg) => arg.startsWith("--title="))?.slice(8) || "Untitled Specification";
|
|
1859
1965
|
const ticket = args.find((arg) => arg.startsWith("--ticket="))?.slice(9);
|
|
@@ -1865,7 +1971,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
1865
1971
|
}
|
|
1866
1972
|
if (command === "adr") {
|
|
1867
1973
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
1868
|
-
return
|
|
1974
|
+
return installationRequired();
|
|
1869
1975
|
assertStageReady(aiw, fs, "technical-design");
|
|
1870
1976
|
const id = args.find((arg) => arg.startsWith("--id="))?.slice(5) || "001";
|
|
1871
1977
|
const title = args.find((arg) => arg.startsWith("--title="))?.slice(8) || "Untitled Decision";
|
|
@@ -1875,26 +1981,27 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
1875
1981
|
}
|
|
1876
1982
|
if (command === "plan") {
|
|
1877
1983
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
1878
|
-
return
|
|
1984
|
+
return installationRequired();
|
|
1879
1985
|
assertStageReady(aiw, fs, "plan");
|
|
1880
1986
|
const title = args.find((arg) => arg.startsWith("--title="))?.slice(8) || "Untitled Implementation Plan";
|
|
1881
1987
|
const path = join(aiw, "generated/plans/implementation-plan.md");
|
|
1882
|
-
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`);
|
|
1988
|
+
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`);
|
|
1883
1989
|
return { exitCode: 0, output: `Implementation plan created: ${path}` };
|
|
1884
1990
|
}
|
|
1885
1991
|
if (command === "verify") {
|
|
1886
1992
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
1887
|
-
return
|
|
1993
|
+
return installationRequired();
|
|
1888
1994
|
assertStageReady(aiw, fs, "verification");
|
|
1889
1995
|
const specPath = join(aiw, "generated/specs/specification.md");
|
|
1890
1996
|
const planPath = join(aiw, "generated/plans/implementation-plan.md");
|
|
1891
1997
|
const issues = [];
|
|
1998
|
+
let regressionNotice;
|
|
1892
1999
|
let specification = "";
|
|
1893
2000
|
if (!fs.exists(specPath))
|
|
1894
2001
|
issues.push("Specification artifact is missing.");
|
|
1895
2002
|
else {
|
|
1896
2003
|
specification = fs.read(specPath);
|
|
1897
|
-
if (!specification.includes("
|
|
2004
|
+
if (!specification.toLowerCase().includes("acceptance criteria"))
|
|
1898
2005
|
issues.push("Requirements have no acceptance criteria.");
|
|
1899
2006
|
}
|
|
1900
2007
|
if (!fs.exists(planPath))
|
|
@@ -1907,17 +2014,24 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
1907
2014
|
const uncovered = requirements.filter((requirement) => !plan.includes(requirement));
|
|
1908
2015
|
if (uncovered.length > 0)
|
|
1909
2016
|
issues.push(`Untested requirements: ${uncovered.join(", ")}.`);
|
|
2017
|
+
const validationCount = (plan.match(/Validation:/g) ?? []).length;
|
|
2018
|
+
const regressionCount = (plan.match(/Regression:/g) ?? []).length;
|
|
2019
|
+
regressionNotice =
|
|
2020
|
+
validationCount > regressionCount
|
|
2021
|
+
? `${validationCount - regressionCount} of ${validationCount} plan tasks declare no regression coverage for touched existing behavior.`
|
|
2022
|
+
: undefined;
|
|
1910
2023
|
}
|
|
1911
2024
|
return issues.length
|
|
1912
2025
|
? { exitCode: 1, error: `Verification failed: ${issues.join(" ")}` }
|
|
1913
2026
|
: {
|
|
1914
2027
|
exitCode: 0,
|
|
1915
|
-
output: "Verification passed: requirements have acceptance criteria and validation coverage."
|
|
2028
|
+
output: "Verification passed: requirements have acceptance criteria and validation coverage." +
|
|
2029
|
+
(regressionNotice ? ` Advisory: ${regressionNotice}` : ""),
|
|
1916
2030
|
};
|
|
1917
2031
|
}
|
|
1918
2032
|
if (command === "trace") {
|
|
1919
2033
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
1920
|
-
return
|
|
2034
|
+
return installationRequired();
|
|
1921
2035
|
assertStageReady(aiw, fs, "traceability");
|
|
1922
2036
|
const specPath = join(aiw, "generated/specs/specification.md");
|
|
1923
2037
|
const planPath = join(aiw, "generated/plans/implementation-plan.md");
|
|
@@ -1946,6 +2060,208 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
1946
2060
|
fs.write(path, serialized);
|
|
1947
2061
|
return { exitCode: 0, output: `Traceability graph created: ${path}` };
|
|
1948
2062
|
}
|
|
2063
|
+
if (command === "standards") {
|
|
2064
|
+
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
2065
|
+
return installationRequired();
|
|
2066
|
+
const action = args[1] ?? "list";
|
|
2067
|
+
const index = fs.exists(join(aiw, "standards/index.yml"))
|
|
2068
|
+
? parseStandardsIndex(fs.read(join(aiw, "standards/index.yml")))
|
|
2069
|
+
: { schema: 1, standards: {} };
|
|
2070
|
+
if (action === "extract") {
|
|
2071
|
+
fs.mkdir(join(aiw, "standards"));
|
|
2072
|
+
const documents = extractStandards(root, fs);
|
|
2073
|
+
for (const document of documents) {
|
|
2074
|
+
const body = [
|
|
2075
|
+
`# Standard: ${document.topic}`,
|
|
2076
|
+
"",
|
|
2077
|
+
`Status: ${document.status}`,
|
|
2078
|
+
"",
|
|
2079
|
+
...document.statements.flatMap((statement) => [
|
|
2080
|
+
statement.convention,
|
|
2081
|
+
...(statement.occurrences > 0
|
|
2082
|
+
? statement.evidence.map((evidence) => `- Evidence: ${evidence.path}:${evidence.line}`)
|
|
2083
|
+
: ["- Evidence: none (deterministic scan found no instances)"]),
|
|
2084
|
+
]),
|
|
2085
|
+
"",
|
|
2086
|
+
].join("\n");
|
|
2087
|
+
fs.write(join(aiw, `standards/${document.topic}.md`), body);
|
|
2088
|
+
if (index.standards[document.topic] === undefined)
|
|
2089
|
+
index.standards[document.topic] = {
|
|
2090
|
+
status: "inferred",
|
|
2091
|
+
updatedAt: new Date().toISOString(),
|
|
2092
|
+
};
|
|
2093
|
+
}
|
|
2094
|
+
const serialized = [
|
|
2095
|
+
"schema: 1",
|
|
2096
|
+
"standards:",
|
|
2097
|
+
...Object.entries(index.standards).map(([topic, entry]) => ` ${topic}:\n status: ${entry.status}\n updatedAt: ${entry.updatedAt}`),
|
|
2098
|
+
"",
|
|
2099
|
+
].join("\n");
|
|
2100
|
+
fs.write(join(aiw, "standards/index.yml"), serialized);
|
|
2101
|
+
const names = documents.map((document) => document.topic).join(", ");
|
|
2102
|
+
return {
|
|
2103
|
+
exitCode: 0,
|
|
2104
|
+
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.`,
|
|
2105
|
+
};
|
|
2106
|
+
}
|
|
2107
|
+
if (action === "list") {
|
|
2108
|
+
const entries = Object.entries(index.standards);
|
|
2109
|
+
if (entries.length === 0)
|
|
2110
|
+
return {
|
|
2111
|
+
exitCode: 0,
|
|
2112
|
+
output: "No standards recorded. Run `aiw standards extract` first.",
|
|
2113
|
+
};
|
|
2114
|
+
return {
|
|
2115
|
+
exitCode: 0,
|
|
2116
|
+
output: [
|
|
2117
|
+
"Standards:",
|
|
2118
|
+
...entries.map(([topic, entry]) => `- ${topic}: ${entry.status}`),
|
|
2119
|
+
"Confirm or reject with `aiw standards confirm|reject <topic>`.",
|
|
2120
|
+
].join("\n"),
|
|
2121
|
+
};
|
|
2122
|
+
}
|
|
2123
|
+
if (action === "confirm" || action === "reject") {
|
|
2124
|
+
const topic = args[2];
|
|
2125
|
+
if (!topic)
|
|
2126
|
+
return { exitCode: 1, error: `Usage: aiw standards ${action} <topic>` };
|
|
2127
|
+
const path = join(aiw, `standards/${topic}.md`);
|
|
2128
|
+
if (!fs.exists(path))
|
|
2129
|
+
return {
|
|
2130
|
+
exitCode: 1,
|
|
2131
|
+
error: `No standard document for ${topic}; run \`aiw standards extract\` first.`,
|
|
2132
|
+
};
|
|
2133
|
+
const status = action === "confirm" ? "confirmed" : "rejected";
|
|
2134
|
+
index.standards[topic] = { status, updatedAt: new Date().toISOString() };
|
|
2135
|
+
const existing = fs.read(path).replace(/^Status: .*$/m, `Status: ${status}`);
|
|
2136
|
+
fs.write(path, existing);
|
|
2137
|
+
const serialized = [
|
|
2138
|
+
"schema: 1",
|
|
2139
|
+
"standards:",
|
|
2140
|
+
...Object.entries(index.standards).map(([topicEntry, entry]) => ` ${topicEntry}:\n status: ${entry.status}\n updatedAt: ${entry.updatedAt}`),
|
|
2141
|
+
"",
|
|
2142
|
+
].join("\n");
|
|
2143
|
+
fs.write(join(aiw, "standards/index.yml"), serialized);
|
|
2144
|
+
return {
|
|
2145
|
+
exitCode: 0,
|
|
2146
|
+
output: `Standard ${topic} ${status}. ${status === "confirmed"
|
|
2147
|
+
? "Confirmed standards are referenced by generated rules."
|
|
2148
|
+
: "Rejected standards are never rendered."}`,
|
|
2149
|
+
};
|
|
2150
|
+
}
|
|
2151
|
+
return {
|
|
2152
|
+
exitCode: 1,
|
|
2153
|
+
error: "Usage: aiw standards <extract|list|confirm|reject> [topic] — standards are inferred until a human confirms them.",
|
|
2154
|
+
};
|
|
2155
|
+
}
|
|
2156
|
+
if (command === "migrate") {
|
|
2157
|
+
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
2158
|
+
return installationRequired();
|
|
2159
|
+
const dry = args.includes("--dry-run");
|
|
2160
|
+
const doRollback = args.includes("--rollback");
|
|
2161
|
+
if (doRollback) {
|
|
2162
|
+
const restored = rollbackMigration(root, aiw, fs);
|
|
2163
|
+
return { exitCode: 0, output: `Legacy state restored from snapshot: ${restored}` };
|
|
2164
|
+
}
|
|
2165
|
+
if (!detectLegacyState(aiw, fs))
|
|
2166
|
+
return {
|
|
2167
|
+
exitCode: 0,
|
|
2168
|
+
output: "Nothing to migrate: state already includes the ownership inventory.",
|
|
2169
|
+
};
|
|
2170
|
+
const manifestPath = join(aiw, "manifest.yml");
|
|
2171
|
+
const manifestTarget = /^\s*active:\s*(.+)$/m.exec(fs.read(manifestPath))?.[1]?.trim() ||
|
|
2172
|
+
detectTarget(root, fs) ||
|
|
2173
|
+
"codex";
|
|
2174
|
+
if (!isTarget(manifestTarget))
|
|
2175
|
+
throw new Error(`Unsupported target: ${manifestTarget}`);
|
|
2176
|
+
const managedCandidates = [];
|
|
2177
|
+
for (const resource of loadBundledResources(manifestTarget)) {
|
|
2178
|
+
managedCandidates.push({
|
|
2179
|
+
path: join(root, renderResourcePath(manifestTarget, resource.type, resource.id)),
|
|
2180
|
+
bundled: resource.content,
|
|
2181
|
+
metadata: {
|
|
2182
|
+
target: manifestTarget,
|
|
2183
|
+
resourceType: resource.type,
|
|
2184
|
+
resourceId: resource.id,
|
|
2185
|
+
kind: "target-resource",
|
|
2186
|
+
},
|
|
2187
|
+
});
|
|
2188
|
+
}
|
|
2189
|
+
managedCandidates.push({
|
|
2190
|
+
path: join(root, describeResource(manifestTarget, "context", "project").path),
|
|
2191
|
+
metadata: {
|
|
2192
|
+
target: manifestTarget,
|
|
2193
|
+
resourceType: "context",
|
|
2194
|
+
resourceId: "project",
|
|
2195
|
+
kind: "target-resource",
|
|
2196
|
+
},
|
|
2197
|
+
});
|
|
2198
|
+
const plan = planLegacyMigration(root, aiw, fs, managedCandidates);
|
|
2199
|
+
const report = [
|
|
2200
|
+
`Target: ${manifestTarget}`,
|
|
2201
|
+
plan.archived.length
|
|
2202
|
+
? `Archived (superseded machine state): ${plan.archived.join(", ")}`
|
|
2203
|
+
: "",
|
|
2204
|
+
plan.preserved.length ? `Preserved byte-identical: ${plan.preserved.join(", ")}` : "",
|
|
2205
|
+
`Rebuilt: ownership inventory (${plan.rebuilt.length} managed paths)`,
|
|
2206
|
+
...plan.userModified.map((entry) => `Legacy-user-modified (sync preserves for review): ${entry}`),
|
|
2207
|
+
...plan.manualActions.map((entry) => `MANUAL ACTION REQUIRED: ${entry}`),
|
|
2208
|
+
dry ? "Mode: dry-run — nothing written." : `Rollback snapshot: ${plan.rollbackSnapshot}`,
|
|
2209
|
+
].filter((line) => line.length > 0);
|
|
2210
|
+
if (dry)
|
|
2211
|
+
return { exitCode: 0, output: ["Migration plan (dry-run):", ...report].join("\n") };
|
|
2212
|
+
try {
|
|
2213
|
+
applyMigration(root, aiw, fs, managedCandidates, plan);
|
|
2214
|
+
}
|
|
2215
|
+
catch (error) {
|
|
2216
|
+
return { exitCode: 1, error: error instanceof Error ? error.message : String(error) };
|
|
2217
|
+
}
|
|
2218
|
+
return { exitCode: 0, output: ["Migration complete.", ...report].join("\n") };
|
|
2219
|
+
}
|
|
2220
|
+
if (command === "digest") {
|
|
2221
|
+
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
2222
|
+
return installationRequired();
|
|
2223
|
+
const stageArg = args.find((argument) => argument.startsWith("--stage="))?.slice(8);
|
|
2224
|
+
const stages = stageArg
|
|
2225
|
+
? [stageArg]
|
|
2226
|
+
: fs.exists(join(aiw, "workflow.yml"))
|
|
2227
|
+
? processStages(readWorkflowState(aiw, fs, false).ledger)
|
|
2228
|
+
: [...APPROVAL_STAGES];
|
|
2229
|
+
const lines = [];
|
|
2230
|
+
let served = 0;
|
|
2231
|
+
for (const stage of stages) {
|
|
2232
|
+
const artifactPath = approvalArtifactPath(aiw, stage);
|
|
2233
|
+
const freshness = digestFreshness(aiw, fs, stage, artifactPath ?? undefined);
|
|
2234
|
+
if (freshness.state === "missing")
|
|
2235
|
+
continue;
|
|
2236
|
+
lines.push(formatDigestLine(freshness));
|
|
2237
|
+
served += 1;
|
|
2238
|
+
}
|
|
2239
|
+
if (lines.length === 0)
|
|
2240
|
+
return {
|
|
2241
|
+
exitCode: 0,
|
|
2242
|
+
output: "No digests yet: digests appear when stages pass their gate and receive human approval.",
|
|
2243
|
+
};
|
|
2244
|
+
lines.push(`served: ${served} digest${served === 1 ? "" : "s"} (full reads on demand)`);
|
|
2245
|
+
return { exitCode: 0, output: lines.join("\n") };
|
|
2246
|
+
}
|
|
2247
|
+
if (command === "diagnosis") {
|
|
2248
|
+
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
2249
|
+
return installationRequired();
|
|
2250
|
+
assertStageReady(aiw, fs, "diagnosis");
|
|
2251
|
+
const title = args.find((argument) => argument.startsWith("--title="))?.slice(8) || "Untitled Diagnosis";
|
|
2252
|
+
const path = join(aiw, "generated/reports/diagnosis.md");
|
|
2253
|
+
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`);
|
|
2254
|
+
return { exitCode: 0, output: `Diagnosis artifact created: ${path}` };
|
|
2255
|
+
}
|
|
2256
|
+
if (command === "assess") {
|
|
2257
|
+
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
2258
|
+
return installationRequired();
|
|
2259
|
+
assertStageReady(aiw, fs, "assessment");
|
|
2260
|
+
const title = args.find((argument) => argument.startsWith("--title="))?.slice(8) || "Untitled Assessment";
|
|
2261
|
+
const path = join(aiw, "generated/reports/assessment.md");
|
|
2262
|
+
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`);
|
|
2263
|
+
return { exitCode: 0, output: `Assessment artifact created: ${path}` };
|
|
2264
|
+
}
|
|
1949
2265
|
if (command === "risk") {
|
|
1950
2266
|
if (args[1] !== "assess")
|
|
1951
2267
|
return { exitCode: 1, error: "Usage: aiw risk assess [--base-ref=ref] [--json]" };
|
|
@@ -1953,7 +2269,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
1953
2269
|
}
|
|
1954
2270
|
if (command === "gate") {
|
|
1955
2271
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
1956
|
-
return
|
|
2272
|
+
return installationRequired();
|
|
1957
2273
|
const stage = args[1];
|
|
1958
2274
|
if (!stage || !APPROVAL_STAGES.includes(stage))
|
|
1959
2275
|
return {
|
|
@@ -1980,6 +2296,17 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
1980
2296
|
detail: `Quality gate blocked: ${issues.join(" ")}`,
|
|
1981
2297
|
}));
|
|
1982
2298
|
}
|
|
2299
|
+
if (hasQualityGate &&
|
|
2300
|
+
(workflowStage === "implementation" || workflowStage === "verification")) {
|
|
2301
|
+
const pairIssues = compareVerificationPairs(aiw, fs, workflowStage);
|
|
2302
|
+
if (pairIssues.length)
|
|
2303
|
+
throw new BlockingError(describeBlockingReason("gate_incomplete", {
|
|
2304
|
+
stage,
|
|
2305
|
+
artifactPath,
|
|
2306
|
+
gateIssues: pairIssues,
|
|
2307
|
+
detail: `Quality gate blocked: ${pairIssues.join(" ")}`,
|
|
2308
|
+
}));
|
|
2309
|
+
}
|
|
1983
2310
|
const { ledger } = readActiveWorkflow(aiw, fs);
|
|
1984
2311
|
const updated = beginStageApproval(ledger, workflowStage, {
|
|
1985
2312
|
artifactHash: stageFingerprint(fs.read(artifactPath)),
|
|
@@ -2008,7 +2335,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
2008
2335
|
}
|
|
2009
2336
|
if (command === "resolve") {
|
|
2010
2337
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
2011
|
-
return
|
|
2338
|
+
return installationRequired();
|
|
2012
2339
|
const packageArg = args.find((arg) => arg.startsWith("--package="));
|
|
2013
2340
|
const sourceArg = args.find((arg) => arg.startsWith("--source="));
|
|
2014
2341
|
if (!packageArg && !sourceArg)
|
|
@@ -2070,7 +2397,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
2070
2397
|
}
|
|
2071
2398
|
if (command === "organization-policy") {
|
|
2072
2399
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
2073
|
-
return
|
|
2400
|
+
return installationRequired();
|
|
2074
2401
|
const fileArg = args.find((arg) => arg.startsWith("--file="));
|
|
2075
2402
|
if (!fileArg)
|
|
2076
2403
|
return { exitCode: 1, error: "Usage: aiw organization-policy --file=path [--replace]" };
|
|
@@ -2086,7 +2413,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
2086
2413
|
}
|
|
2087
2414
|
if (command === "policy-check") {
|
|
2088
2415
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
2089
|
-
return
|
|
2416
|
+
return installationRequired();
|
|
2090
2417
|
const policyPath = join(aiw, "organization.yml");
|
|
2091
2418
|
if (!fs.exists(policyPath))
|
|
2092
2419
|
return { exitCode: 1, error: "Organization policy is required for the CI policy gate." };
|
|
@@ -2124,7 +2451,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
2124
2451
|
}
|
|
2125
2452
|
if (command === "preset") {
|
|
2126
2453
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
2127
|
-
return
|
|
2454
|
+
return installationRequired();
|
|
2128
2455
|
const fileArg = args.find((arg) => arg.startsWith("--file="));
|
|
2129
2456
|
if (!fileArg)
|
|
2130
2457
|
return { exitCode: 1, error: "Usage: aiw preset --file=path [--replace]" };
|
|
@@ -2140,7 +2467,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
2140
2467
|
}
|
|
2141
2468
|
if (command === "update") {
|
|
2142
2469
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
2143
|
-
return
|
|
2470
|
+
return installationRequired();
|
|
2144
2471
|
const packageArg = args.find((arg) => arg.startsWith("--package="));
|
|
2145
2472
|
if (!packageArg)
|
|
2146
2473
|
return { exitCode: 1, error: "Usage: aiw update --package=path --target=target" };
|
|
@@ -2171,7 +2498,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
2171
2498
|
if (command === "registry") {
|
|
2172
2499
|
if (args[1] === "configure") {
|
|
2173
2500
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
2174
|
-
return
|
|
2501
|
+
return installationRequired();
|
|
2175
2502
|
const fileArg = args.find((arg) => arg.startsWith("--file="));
|
|
2176
2503
|
if (!fileArg)
|
|
2177
2504
|
return {
|
|
@@ -2257,9 +2584,39 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
2257
2584
|
};
|
|
2258
2585
|
}
|
|
2259
2586
|
const output = args.includes("--task") ? retrieveTaskContext(store) : composeContext(store);
|
|
2587
|
+
const stageArg = args.find((argument) => argument.startsWith("--stage="))?.slice(8);
|
|
2588
|
+
const digestLines = [];
|
|
2589
|
+
if (fs.exists(join(aiw, "workflow.yml"))) {
|
|
2590
|
+
for (const stage of processStages(readWorkflowState(aiw, fs, false).ledger)) {
|
|
2591
|
+
const artifactPath = approvalArtifactPath(aiw, stage);
|
|
2592
|
+
const freshness = digestFreshness(aiw, fs, stage, artifactPath ?? undefined);
|
|
2593
|
+
if (freshness.state !== "missing")
|
|
2594
|
+
digestLines.push(formatDigestLine(freshness));
|
|
2595
|
+
}
|
|
2596
|
+
}
|
|
2597
|
+
const budgetArg = args.find((argument) => argument.startsWith("--budget="))?.slice(9);
|
|
2598
|
+
if (stageArg) {
|
|
2599
|
+
const budget = Number(budgetArg ?? parseContextBudgets(aiw, fs)[stageArg]);
|
|
2600
|
+
if (Number.isFinite(budget) && budget > 0) {
|
|
2601
|
+
const estimated = Math.ceil(output.length / 4);
|
|
2602
|
+
if (estimated > budget)
|
|
2603
|
+
throw new BlockingError(describeBlockingReason("context_budget_exceeded", {
|
|
2604
|
+
stage: stageArg,
|
|
2605
|
+
detail: `Stage ${stageArg} context is ~${estimated} tokens, over the ${budget}-token ceiling.`,
|
|
2606
|
+
}));
|
|
2607
|
+
}
|
|
2608
|
+
const digestBlock = digestLines.length > 0 ? `\n${digestLines.join("\n")}` : "";
|
|
2609
|
+
return {
|
|
2610
|
+
exitCode: 0,
|
|
2611
|
+
output: `${output}${digestBlock}\n\nstage: ${stageArg} · ~${Math.ceil(output.length / 4)} tokens${Number.isFinite(budget) && budget > 0 ? ` · ceiling ${budget}` : " · no ceiling declared"}`,
|
|
2612
|
+
};
|
|
2613
|
+
}
|
|
2260
2614
|
return { exitCode: 0, output };
|
|
2261
2615
|
}
|
|
2262
|
-
return {
|
|
2616
|
+
return {
|
|
2617
|
+
exitCode: 1,
|
|
2618
|
+
error: "Usage: aiw context [--layer=project --content=text] | aiw context --stage=<name> [--budget=tokens]",
|
|
2619
|
+
};
|
|
2263
2620
|
}
|
|
2264
2621
|
if (command === "context-summary") {
|
|
2265
2622
|
const summaryArg = args.find((arg) => arg.startsWith("--summary="))?.slice(10);
|
|
@@ -2277,7 +2634,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
2277
2634
|
}
|
|
2278
2635
|
if (command === "routing") {
|
|
2279
2636
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
2280
|
-
return
|
|
2637
|
+
return installationRequired();
|
|
2281
2638
|
const usage = "Usage: aiw routing init | show | resolve --category=name";
|
|
2282
2639
|
const action = args[1];
|
|
2283
2640
|
const routingPath = join(aiw, ROUTING_TABLE_PATH);
|
|
@@ -2333,7 +2690,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
2333
2690
|
}
|
|
2334
2691
|
if (command === "task-usage") {
|
|
2335
2692
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
2336
|
-
return
|
|
2693
|
+
return installationRequired();
|
|
2337
2694
|
const parsed = parseNamedOptions(args.slice(1), ["workflow"]);
|
|
2338
2695
|
if (!parsed)
|
|
2339
2696
|
return { exitCode: 1, error: "Usage: aiw task-usage [--workflow=id-or-ticket]" };
|
|
@@ -2362,7 +2719,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
2362
2719
|
}
|
|
2363
2720
|
if (command === "cost-report") {
|
|
2364
2721
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
2365
|
-
return
|
|
2722
|
+
return installationRequired();
|
|
2366
2723
|
const sidecarPath = join(aiw, USAGE_SIDECAR_PATH);
|
|
2367
2724
|
if (!fs.exists(sidecarPath))
|
|
2368
2725
|
return {
|
|
@@ -2384,7 +2741,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
2384
2741
|
}
|
|
2385
2742
|
if (command === "heartbeat") {
|
|
2386
2743
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
2387
|
-
return
|
|
2744
|
+
return installationRequired();
|
|
2388
2745
|
const parsed = parseNamedOptions(args.slice(1), ["session", "warn", "ttl"]);
|
|
2389
2746
|
if (!parsed)
|
|
2390
2747
|
return {
|
|
@@ -2434,7 +2791,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
2434
2791
|
}
|
|
2435
2792
|
if (command === "hooks") {
|
|
2436
2793
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
2437
|
-
return
|
|
2794
|
+
return installationRequired();
|
|
2438
2795
|
const action = args[1];
|
|
2439
2796
|
if (action !== "install")
|
|
2440
2797
|
return {
|
|
@@ -2478,7 +2835,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
2478
2835
|
}
|
|
2479
2836
|
if (command === "usage-sidecar") {
|
|
2480
2837
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
2481
|
-
return
|
|
2838
|
+
return installationRequired();
|
|
2482
2839
|
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";
|
|
2483
2840
|
const action = args[1];
|
|
2484
2841
|
const sidecarPath = join(aiw, USAGE_SIDECAR_PATH);
|
|
@@ -2687,7 +3044,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
2687
3044
|
}
|
|
2688
3045
|
if (command === "self-validate") {
|
|
2689
3046
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
2690
|
-
return
|
|
3047
|
+
return installationRequired();
|
|
2691
3048
|
const ticket = args.find((arg) => arg.startsWith("--ticket="))?.slice(9);
|
|
2692
3049
|
if (!ticket)
|
|
2693
3050
|
return { exitCode: 1, error: "Usage: aiw self-validate --ticket=TYPE-000" };
|
|
@@ -2723,7 +3080,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
2723
3080
|
}
|
|
2724
3081
|
return {
|
|
2725
3082
|
exitCode: 0,
|
|
2726
|
-
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]",
|
|
3083
|
+
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]",
|
|
2727
3084
|
};
|
|
2728
3085
|
}
|
|
2729
3086
|
catch (error) {
|
|
@@ -2741,12 +3098,14 @@ function readWorkflowState(aiw, fs, requireActive = true) {
|
|
|
2741
3098
|
const session = parseWorkflowSession(fs.read(sessionPath));
|
|
2742
3099
|
let ledger = parseApprovalLedger(fs.read(ledgerPath));
|
|
2743
3100
|
if (session.workflowId !== ledger.workflowId)
|
|
2744
|
-
throw new
|
|
3101
|
+
throw new BlockingError(describeBlockingReason("state_mismatch", {
|
|
3102
|
+
detail: "Workflow session and approval ledger IDs do not match; run doctor.",
|
|
3103
|
+
}));
|
|
2745
3104
|
if (requireActive && session.status !== "active")
|
|
2746
3105
|
throw new BlockingError(describeBlockingReason("workflow_complete", {
|
|
2747
3106
|
detail: `Workflow ${session.workflowId} is complete; start a new workflow first.`,
|
|
2748
3107
|
}));
|
|
2749
|
-
const changed =
|
|
3108
|
+
const changed = processStages(ledger).find((stage) => {
|
|
2750
3109
|
const record = ledger.stages[stage];
|
|
2751
3110
|
if (!record?.artifactHash || !["pending-human", "approved"].includes(record.status))
|
|
2752
3111
|
return false;
|
|
@@ -2774,6 +3133,85 @@ function workflowNextAction(ledger) {
|
|
|
2774
3133
|
return `Next action: revise ${next}, rerun its gate, and request human approval.`;
|
|
2775
3134
|
return `Next action: prepare ${next} evidence.`;
|
|
2776
3135
|
}
|
|
3136
|
+
function parseContextBudgets(aiw, fs) {
|
|
3137
|
+
const path = join(aiw, "budgets.yml");
|
|
3138
|
+
if (!fs.exists(path))
|
|
3139
|
+
return {};
|
|
3140
|
+
const value = parseYaml(fs.read(path));
|
|
3141
|
+
if (!value || typeof value !== "object")
|
|
3142
|
+
return {};
|
|
3143
|
+
const stages = value.stages;
|
|
3144
|
+
if (!stages || typeof stages !== "object")
|
|
3145
|
+
return {};
|
|
3146
|
+
const budgets = {};
|
|
3147
|
+
for (const [stage, ceiling] of Object.entries(stages)) {
|
|
3148
|
+
const n = Number(ceiling);
|
|
3149
|
+
if (Number.isFinite(n) && n > 0)
|
|
3150
|
+
budgets[stage] = n;
|
|
3151
|
+
}
|
|
3152
|
+
return budgets;
|
|
3153
|
+
}
|
|
3154
|
+
const PLAN_PAIR_PATTERN = /Validation:\s*([^→\n]+)→\s*([^\n]+)/g;
|
|
3155
|
+
const EXIT_PATTERN = /exit\s+(\d+)/i;
|
|
3156
|
+
function normalizeCommand(command) {
|
|
3157
|
+
return command.trim().replace(/\s+/g, " ").toLowerCase();
|
|
3158
|
+
}
|
|
3159
|
+
/**
|
|
3160
|
+
* OPP-014 paired verification: plan tasks declare `Validation: <command> →
|
|
3161
|
+
* <expected>`; reports record `Validation: <command> → <observed>`. The gate
|
|
3162
|
+
* compares the pairs — command match, observed presence, exit-code agreement.
|
|
3163
|
+
* Legacy plans without pair arrows are accepted unchanged (ratchet).
|
|
3164
|
+
*/
|
|
3165
|
+
export function compareVerificationPairs(aiw, fs, stage) {
|
|
3166
|
+
const planPath = join(aiw, "generated/plans/implementation-plan.md");
|
|
3167
|
+
if (!fs.exists(planPath))
|
|
3168
|
+
return [];
|
|
3169
|
+
const plan = fs.read(planPath);
|
|
3170
|
+
if (!plan.includes("→"))
|
|
3171
|
+
return [];
|
|
3172
|
+
const declared = [];
|
|
3173
|
+
for (const blockMatch of plan.matchAll(/TASK-\d+[\s\S]*?(?=TASK-\d+|$)/g)) {
|
|
3174
|
+
const block = blockMatch[0];
|
|
3175
|
+
const taskId = /^\s*(TASK-\d+)/.exec(block)?.[1] ?? "TASK-?";
|
|
3176
|
+
for (const pair of block.matchAll(PLAN_PAIR_PATTERN)) {
|
|
3177
|
+
const command = pair[1].trim();
|
|
3178
|
+
if (!command)
|
|
3179
|
+
continue;
|
|
3180
|
+
declared.push({
|
|
3181
|
+
task: taskId,
|
|
3182
|
+
command,
|
|
3183
|
+
expectedExitText: EXIT_PATTERN.exec(pair[2])?.[1],
|
|
3184
|
+
raw: pair[2].trim(),
|
|
3185
|
+
});
|
|
3186
|
+
}
|
|
3187
|
+
}
|
|
3188
|
+
if (declared.length === 0)
|
|
3189
|
+
return [];
|
|
3190
|
+
const reportPath = stage === "implementation"
|
|
3191
|
+
? join(aiw, "generated/reports/implementation-report.md")
|
|
3192
|
+
: join(aiw, "generated/reports/verification-report.md");
|
|
3193
|
+
const issues = [];
|
|
3194
|
+
const observed = new Map();
|
|
3195
|
+
if (fs.exists(reportPath)) {
|
|
3196
|
+
for (const line of fs.read(reportPath).split("\n")) {
|
|
3197
|
+
const match = /Validation:\s*([^→\n]+)→\s*([^\n]+)/.exec(line);
|
|
3198
|
+
if (match)
|
|
3199
|
+
observed.set(normalizeCommand(match[1]), match[2].trim());
|
|
3200
|
+
}
|
|
3201
|
+
}
|
|
3202
|
+
for (const pair of declared) {
|
|
3203
|
+
const key = normalizeCommand(pair.command);
|
|
3204
|
+
if (!observed.has(key)) {
|
|
3205
|
+
issues.push(`[verification_pair_missing] ${pair.task}: \`${pair.command}\``);
|
|
3206
|
+
continue;
|
|
3207
|
+
}
|
|
3208
|
+
const observedExit = EXIT_PATTERN.exec(observed.get(key) ?? "")?.[1];
|
|
3209
|
+
if (pair.expectedExitText && observedExit && pair.expectedExitText !== observedExit) {
|
|
3210
|
+
issues.push(`[verification_pair_mismatch] ${pair.task}: expected exit ${pair.expectedExitText}, observed exit ${observedExit}`);
|
|
3211
|
+
}
|
|
3212
|
+
}
|
|
3213
|
+
return issues;
|
|
3214
|
+
}
|
|
2777
3215
|
function parseLightEvidence(content) {
|
|
2778
3216
|
const value = parseYaml(content);
|
|
2779
3217
|
if (!value || typeof value !== "object" || value.schema !== 1)
|
|
@@ -2794,6 +3232,11 @@ function parseLightEvidence(content) {
|
|
|
2794
3232
|
}
|
|
2795
3233
|
function assertStageReady(aiw, fs, stage) {
|
|
2796
3234
|
const { session, ledger } = readActiveWorkflow(aiw, fs);
|
|
3235
|
+
if (!processStages(ledger).includes(stage))
|
|
3236
|
+
throw new BlockingError(describeBlockingReason("stage_not_in_process", {
|
|
3237
|
+
stage,
|
|
3238
|
+
detail: `Stage ${stage} is not part of the ${ledger.process ?? "feature"} process for workflow ${session.workflowId}.`,
|
|
3239
|
+
}));
|
|
2797
3240
|
if (effectiveLane(session) === "light")
|
|
2798
3241
|
throw new BlockingError(describeBlockingReason("lane_escalation_required", {
|
|
2799
3242
|
stage,
|
|
@@ -2837,6 +3280,8 @@ function approvalArtifactPath(aiw, stage) {
|
|
|
2837
3280
|
verification: "generated/reports/verification-report.md",
|
|
2838
3281
|
review: "generated/reports/code-review.md",
|
|
2839
3282
|
traceability: "generated/artifacts/traceability.yml",
|
|
3283
|
+
diagnosis: "generated/reports/diagnosis.md",
|
|
3284
|
+
assessment: "generated/reports/assessment.md",
|
|
2840
3285
|
};
|
|
2841
3286
|
return resolve(aiw, paths[stage]);
|
|
2842
3287
|
}
|