@multileaf/ai-workflow 0.11.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 +18 -0
- package/dist/blocking-reasons.d.ts +1 -1
- package/dist/blocking-reasons.js +24 -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/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 +426 -57
- package/docs/backlog.md +10 -3
- package/docs/continuation-inventory.md +20 -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/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,14 +65,44 @@ 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");
|
|
@@ -83,7 +113,7 @@ function beginWorkflowSession(fs, aiw, options) {
|
|
|
83
113
|
detail: `Workflow ${previous.workflowId} is still active. Complete it before starting another.`,
|
|
84
114
|
}));
|
|
85
115
|
if (!fs.exists(ledgerPath))
|
|
86
|
-
return
|
|
116
|
+
return stateMismatch("Completed workflow is missing its approval ledger.");
|
|
87
117
|
const archive = join(aiw, `checkpoints/workflow-${previous.workflowId}-approvals.yml`);
|
|
88
118
|
if (fs.exists(archive))
|
|
89
119
|
return { exitCode: 1, error: `Workflow approval archive already exists: ${archive}` };
|
|
@@ -93,7 +123,7 @@ function beginWorkflowSession(fs, aiw, options) {
|
|
|
93
123
|
else if (fs.exists(ledgerPath)) {
|
|
94
124
|
return {
|
|
95
125
|
exitCode: 1,
|
|
96
|
-
error: "Approval ledger exists without a workflow session; run doctor.",
|
|
126
|
+
error: stateMismatch("Approval ledger exists without a workflow session; run doctor.").error,
|
|
97
127
|
};
|
|
98
128
|
}
|
|
99
129
|
const workflowId = options.id ?? randomUUID();
|
|
@@ -120,6 +150,7 @@ function beginWorkflowSession(fs, aiw, options) {
|
|
|
120
150
|
...(options.lane === "light" && options.laneReason
|
|
121
151
|
? { lane: "light", laneReason: options.laneReason }
|
|
122
152
|
: {}),
|
|
153
|
+
...(options.process && options.process !== "feature" ? { process: options.process } : {}),
|
|
123
154
|
});
|
|
124
155
|
fs.write(ledgerPath, serializeApprovalLedger(ledger));
|
|
125
156
|
fs.write(sessionPath, serializeWorkflowSession(session));
|
|
@@ -132,7 +163,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
132
163
|
const command = args[0] ?? "help";
|
|
133
164
|
if (command === "task") {
|
|
134
165
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
135
|
-
return
|
|
166
|
+
return installationRequired();
|
|
136
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";
|
|
137
168
|
const action = args[1];
|
|
138
169
|
const sessionPath = join(aiw, "workflow.yml");
|
|
@@ -143,10 +174,24 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
143
174
|
"id",
|
|
144
175
|
"lane",
|
|
145
176
|
"lane-reason",
|
|
177
|
+
"process",
|
|
146
178
|
]);
|
|
147
179
|
if (!options?.ticket)
|
|
148
180
|
return { exitCode: 1, error: usage };
|
|
149
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
|
+
}
|
|
150
195
|
if (options.lane !== undefined && options.lane !== "light")
|
|
151
196
|
return { exitCode: 1, error: "Only --lane=light exists; omit it for the full lane." };
|
|
152
197
|
const laneReason = options["lane-reason"];
|
|
@@ -176,14 +221,17 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
176
221
|
ticket: options.ticket,
|
|
177
222
|
...(options.title ? { title: options.title } : {}),
|
|
178
223
|
...(lane ? { lane, laneReason: laneReason } : {}),
|
|
224
|
+
...(processName ? { process: processName } : {}),
|
|
179
225
|
});
|
|
180
226
|
if (result.exitCode !== 0)
|
|
181
227
|
return result;
|
|
228
|
+
const advisory = laneAdvisory(root, fs, lane === "light");
|
|
182
229
|
const title = options.title ? ` titled "${options.title}"` : "";
|
|
183
230
|
if (lane)
|
|
184
231
|
return {
|
|
185
232
|
exitCode: 0,
|
|
186
233
|
output: `Task started for ticket ${options.ticket}${title} (light lane): ${result.output}\n` +
|
|
234
|
+
(advisory ? `${advisory}\n` : "") +
|
|
187
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" +
|
|
188
236
|
`Rationale recorded: ${laneReason}\n` +
|
|
189
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=...`.',
|
|
@@ -191,6 +239,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
191
239
|
return {
|
|
192
240
|
exitCode: 0,
|
|
193
241
|
output: `Task started for ticket ${options.ticket}${title}: ${result.output}\n` +
|
|
242
|
+
(advisory ? `${advisory}\n` : "") +
|
|
194
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=...\`.`,
|
|
195
244
|
};
|
|
196
245
|
}
|
|
@@ -287,7 +336,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
287
336
|
...lane,
|
|
288
337
|
`Workflow: ${session.workflowId} (${session.status})`,
|
|
289
338
|
"Stages:",
|
|
290
|
-
...
|
|
339
|
+
...processStages(ledger).map((stage) => `- ${stage}: ${ledger.stages[stage]?.status ?? "not-started"}`),
|
|
291
340
|
];
|
|
292
341
|
if (session.status === "active" && effectiveLane(session) === "light") {
|
|
293
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=...`.');
|
|
@@ -302,20 +351,32 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
302
351
|
}
|
|
303
352
|
if (command === "workflow") {
|
|
304
353
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
305
|
-
return
|
|
354
|
+
return installationRequired();
|
|
306
355
|
const action = args[1];
|
|
307
356
|
const sessionPath = join(aiw, "workflow.yml");
|
|
308
357
|
if (action === "start") {
|
|
309
|
-
const options = parseNamedOptions(args.slice(2).filter((argument) => argument !== "--json"), ["id"]);
|
|
358
|
+
const options = parseNamedOptions(args.slice(2).filter((argument) => argument !== "--json"), ["id", "process"]);
|
|
310
359
|
if (!options)
|
|
311
|
-
return {
|
|
312
|
-
|
|
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
|
+
});
|
|
313
374
|
}
|
|
314
375
|
if (action === "status") {
|
|
315
376
|
if (!fs.exists(sessionPath))
|
|
316
377
|
return { exitCode: 0, output: "No SDD workflow is active." };
|
|
317
378
|
const { session, ledger } = readWorkflowState(aiw, fs, false);
|
|
318
|
-
const stages =
|
|
379
|
+
const stages = processStages(ledger).map((stage) => `- ${stage}: ${ledger.stages[stage]?.status ?? "not-started"}`);
|
|
319
380
|
return {
|
|
320
381
|
exitCode: 0,
|
|
321
382
|
output: `Workflow ${session.workflowId} (${session.status})\n${stages.join("\n")}\n${workflowNextAction(ledger)}`,
|
|
@@ -382,7 +443,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
382
443
|
output: `Workflow completed (light lane): ${session.workflowId}. Evidence: ${evidencePath}`,
|
|
383
444
|
};
|
|
384
445
|
}
|
|
385
|
-
const unresolved =
|
|
446
|
+
const unresolved = processStages(ledger).filter((stage) => ledger.stages[stage]?.status !== "approved" &&
|
|
386
447
|
ledger.stages[stage]?.status !== "skipped");
|
|
387
448
|
if (unresolved.length)
|
|
388
449
|
return {
|
|
@@ -457,7 +518,12 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
457
518
|
updatedAt: new Date().toISOString(),
|
|
458
519
|
});
|
|
459
520
|
fs.write(join(aiw, "approvals.yml"), serializeApprovalLedger(updated));
|
|
460
|
-
|
|
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
|
+
};
|
|
461
527
|
}
|
|
462
528
|
const options = parseNamedOptions(rest, ["reason"]);
|
|
463
529
|
if (!options?.reason)
|
|
@@ -471,7 +537,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
471
537
|
}
|
|
472
538
|
if (command === "ui") {
|
|
473
539
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
474
|
-
return
|
|
540
|
+
return installationRequired();
|
|
475
541
|
const parsed = parseNamedOptions(args.slice(1), ["port"]);
|
|
476
542
|
if (!parsed || (parsed.port !== undefined && !/^\d+$/.test(parsed.port)))
|
|
477
543
|
return { exitCode: 1, error: "Usage: aiw ui [--port=number]" };
|
|
@@ -485,20 +551,36 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
485
551
|
}
|
|
486
552
|
if (command === "orchestrate") {
|
|
487
553
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
488
|
-
return
|
|
554
|
+
return installationRequired();
|
|
489
555
|
const executeCount = args.filter((arg) => arg === "--execute").length;
|
|
490
556
|
if (executeCount > 1)
|
|
491
557
|
return {
|
|
492
558
|
exitCode: 1,
|
|
493
|
-
error: "Usage: aiw orchestrate --plan=path [--execute] [--max-parallel=number]",
|
|
559
|
+
error: "Usage: aiw orchestrate --plan=path [--execute] [--isolate] [--cleanup] [--max-parallel=number]",
|
|
494
560
|
};
|
|
495
561
|
const execute = executeCount === 1;
|
|
496
|
-
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");
|
|
497
579
|
const parsed = parseNamedOptions(named, ["plan", "max-parallel"]);
|
|
498
580
|
if (!parsed?.plan)
|
|
499
581
|
return {
|
|
500
582
|
exitCode: 1,
|
|
501
|
-
error: "Usage: aiw orchestrate --plan=path [--execute] [--max-parallel=number]",
|
|
583
|
+
error: "Usage: aiw orchestrate --plan=path [--execute] [--isolate] [--cleanup] [--max-parallel=number]",
|
|
502
584
|
};
|
|
503
585
|
const planPath = safeProjectFile(root, parsed.plan, fs);
|
|
504
586
|
const plan = parseOrchestrationPlan(fs.read(planPath));
|
|
@@ -517,7 +599,19 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
517
599
|
error: `Orchestration checkpoint already exists; use a new plan id: ${checkpoint}`,
|
|
518
600
|
};
|
|
519
601
|
try {
|
|
520
|
-
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
|
+
}
|
|
521
615
|
fs.write(checkpoint, serializeOrchestrationResults(plan, results));
|
|
522
616
|
const failed = results.filter(({ status }) => status !== "passed");
|
|
523
617
|
return failed.length
|
|
@@ -640,7 +734,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
640
734
|
}
|
|
641
735
|
if (command === "telemetry") {
|
|
642
736
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
643
|
-
return
|
|
737
|
+
return installationRequired();
|
|
644
738
|
const usage = "Usage: aiw telemetry <status|show|enable|disable> [--verbose] [--commands=include|exclude] [--outcomes=include|exclude]";
|
|
645
739
|
const action = args[1];
|
|
646
740
|
if (!action)
|
|
@@ -753,7 +847,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
753
847
|
}
|
|
754
848
|
if (command === "context-bench") {
|
|
755
849
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
756
|
-
return
|
|
850
|
+
return installationRequired();
|
|
757
851
|
const saveCount = args.filter((arg) => arg === "--save").length;
|
|
758
852
|
if (saveCount > 1)
|
|
759
853
|
return { exitCode: 1, error: "Usage: aiw context-bench [--baseline=path] [--save]" };
|
|
@@ -798,7 +892,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
798
892
|
}
|
|
799
893
|
if (command === "parity") {
|
|
800
894
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
801
|
-
return
|
|
895
|
+
return installationRequired();
|
|
802
896
|
const parsed = parseNamedOptions(args.slice(1), ["tasks"]);
|
|
803
897
|
if (!parsed)
|
|
804
898
|
return { exitCode: 1, error: "Usage: aiw parity [--tasks=directory]" };
|
|
@@ -852,7 +946,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
852
946
|
}
|
|
853
947
|
if (command === "scan") {
|
|
854
948
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
855
|
-
return
|
|
949
|
+
return installationRequired();
|
|
856
950
|
const profile = await (services.profiler?.profile(root) ?? profileProject(root));
|
|
857
951
|
let interpreter = services.interpreter;
|
|
858
952
|
let interpreterModel;
|
|
@@ -1132,7 +1226,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
1132
1226
|
return { exitCode: 1, error: `Unsupported target: ${target}` };
|
|
1133
1227
|
const manifestPath = join(aiw, "manifest.yml");
|
|
1134
1228
|
if (!fs.exists(manifestPath))
|
|
1135
|
-
return
|
|
1229
|
+
return installationRequired();
|
|
1136
1230
|
const dryRun = args.includes("--dry-run");
|
|
1137
1231
|
const targetPath = join(root, renderAiInitPath(target));
|
|
1138
1232
|
const neutralAiInit = join(aiw, "resources/skills/ai-init.md");
|
|
@@ -1459,7 +1553,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
1459
1553
|
}
|
|
1460
1554
|
if (command === "confirm") {
|
|
1461
1555
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
1462
|
-
return
|
|
1556
|
+
return installationRequired();
|
|
1463
1557
|
const overrides = [];
|
|
1464
1558
|
const accept = args.indexOf("--accept");
|
|
1465
1559
|
if (accept >= 0 && args[accept + 1])
|
|
@@ -1483,7 +1577,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
1483
1577
|
}
|
|
1484
1578
|
if (command === "recommend") {
|
|
1485
1579
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
1486
|
-
return
|
|
1580
|
+
return installationRequired();
|
|
1487
1581
|
const profile = await (services.profiler?.profile(root) ?? profileProject(root));
|
|
1488
1582
|
const recommendations = services.recommender?.recommend(profile) ?? recommendCapabilities(profile);
|
|
1489
1583
|
let ordered = recommendations;
|
|
@@ -1551,7 +1645,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
1551
1645
|
}
|
|
1552
1646
|
if (command === "skills") {
|
|
1553
1647
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
1554
|
-
return
|
|
1648
|
+
return installationRequired();
|
|
1555
1649
|
const action = args[1];
|
|
1556
1650
|
const executor = services.externalSkills ?? nodeCommandExecutor;
|
|
1557
1651
|
const approvedPermissions = parseApprovedPermissions(args);
|
|
@@ -1615,7 +1709,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
1615
1709
|
}
|
|
1616
1710
|
if (command === "generate" || command === "sync") {
|
|
1617
1711
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
1618
|
-
return
|
|
1712
|
+
return installationRequired();
|
|
1619
1713
|
const profilePath = join(aiw, "profile.yml");
|
|
1620
1714
|
const persistedProfile = fs.exists(profilePath) ? fs.read(profilePath) : "";
|
|
1621
1715
|
const profile = /^status:\s*scanned$/m.test(persistedProfile)
|
|
@@ -1844,7 +1938,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
1844
1938
|
}
|
|
1845
1939
|
if (command === "brainstorm") {
|
|
1846
1940
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
1847
|
-
return
|
|
1941
|
+
return installationRequired();
|
|
1848
1942
|
assertStageReady(aiw, fs, "brainstorming");
|
|
1849
1943
|
const title = args.find((arg) => arg.startsWith("--title="))?.slice(8) || "Untitled Brainstorm";
|
|
1850
1944
|
const path = join(aiw, "generated/specs/brainstorm.md");
|
|
@@ -1853,7 +1947,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
1853
1947
|
}
|
|
1854
1948
|
if (command === "spec") {
|
|
1855
1949
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
1856
|
-
return
|
|
1950
|
+
return installationRequired();
|
|
1857
1951
|
assertStageReady(aiw, fs, "specification");
|
|
1858
1952
|
const title = args.find((arg) => arg.startsWith("--title="))?.slice(8) || "Untitled Specification";
|
|
1859
1953
|
const ticket = args.find((arg) => arg.startsWith("--ticket="))?.slice(9);
|
|
@@ -1865,7 +1959,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
1865
1959
|
}
|
|
1866
1960
|
if (command === "adr") {
|
|
1867
1961
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
1868
|
-
return
|
|
1962
|
+
return installationRequired();
|
|
1869
1963
|
assertStageReady(aiw, fs, "technical-design");
|
|
1870
1964
|
const id = args.find((arg) => arg.startsWith("--id="))?.slice(5) || "001";
|
|
1871
1965
|
const title = args.find((arg) => arg.startsWith("--title="))?.slice(8) || "Untitled Decision";
|
|
@@ -1875,26 +1969,27 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
1875
1969
|
}
|
|
1876
1970
|
if (command === "plan") {
|
|
1877
1971
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
1878
|
-
return
|
|
1972
|
+
return installationRequired();
|
|
1879
1973
|
assertStageReady(aiw, fs, "plan");
|
|
1880
1974
|
const title = args.find((arg) => arg.startsWith("--title="))?.slice(8) || "Untitled Implementation Plan";
|
|
1881
1975
|
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`);
|
|
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`);
|
|
1883
1977
|
return { exitCode: 0, output: `Implementation plan created: ${path}` };
|
|
1884
1978
|
}
|
|
1885
1979
|
if (command === "verify") {
|
|
1886
1980
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
1887
|
-
return
|
|
1981
|
+
return installationRequired();
|
|
1888
1982
|
assertStageReady(aiw, fs, "verification");
|
|
1889
1983
|
const specPath = join(aiw, "generated/specs/specification.md");
|
|
1890
1984
|
const planPath = join(aiw, "generated/plans/implementation-plan.md");
|
|
1891
1985
|
const issues = [];
|
|
1986
|
+
let regressionNotice;
|
|
1892
1987
|
let specification = "";
|
|
1893
1988
|
if (!fs.exists(specPath))
|
|
1894
1989
|
issues.push("Specification artifact is missing.");
|
|
1895
1990
|
else {
|
|
1896
1991
|
specification = fs.read(specPath);
|
|
1897
|
-
if (!specification.includes("
|
|
1992
|
+
if (!specification.toLowerCase().includes("acceptance criteria"))
|
|
1898
1993
|
issues.push("Requirements have no acceptance criteria.");
|
|
1899
1994
|
}
|
|
1900
1995
|
if (!fs.exists(planPath))
|
|
@@ -1907,17 +2002,24 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
1907
2002
|
const uncovered = requirements.filter((requirement) => !plan.includes(requirement));
|
|
1908
2003
|
if (uncovered.length > 0)
|
|
1909
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;
|
|
1910
2011
|
}
|
|
1911
2012
|
return issues.length
|
|
1912
2013
|
? { exitCode: 1, error: `Verification failed: ${issues.join(" ")}` }
|
|
1913
2014
|
: {
|
|
1914
2015
|
exitCode: 0,
|
|
1915
|
-
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}` : ""),
|
|
1916
2018
|
};
|
|
1917
2019
|
}
|
|
1918
2020
|
if (command === "trace") {
|
|
1919
2021
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
1920
|
-
return
|
|
2022
|
+
return installationRequired();
|
|
1921
2023
|
assertStageReady(aiw, fs, "traceability");
|
|
1922
2024
|
const specPath = join(aiw, "generated/specs/specification.md");
|
|
1923
2025
|
const planPath = join(aiw, "generated/plans/implementation-plan.md");
|
|
@@ -1946,6 +2048,144 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
1946
2048
|
fs.write(path, serialized);
|
|
1947
2049
|
return { exitCode: 0, output: `Traceability graph created: ${path}` };
|
|
1948
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
|
+
}
|
|
1949
2189
|
if (command === "risk") {
|
|
1950
2190
|
if (args[1] !== "assess")
|
|
1951
2191
|
return { exitCode: 1, error: "Usage: aiw risk assess [--base-ref=ref] [--json]" };
|
|
@@ -1953,7 +2193,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
1953
2193
|
}
|
|
1954
2194
|
if (command === "gate") {
|
|
1955
2195
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
1956
|
-
return
|
|
2196
|
+
return installationRequired();
|
|
1957
2197
|
const stage = args[1];
|
|
1958
2198
|
if (!stage || !APPROVAL_STAGES.includes(stage))
|
|
1959
2199
|
return {
|
|
@@ -1980,6 +2220,17 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
1980
2220
|
detail: `Quality gate blocked: ${issues.join(" ")}`,
|
|
1981
2221
|
}));
|
|
1982
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
|
+
}
|
|
1983
2234
|
const { ledger } = readActiveWorkflow(aiw, fs);
|
|
1984
2235
|
const updated = beginStageApproval(ledger, workflowStage, {
|
|
1985
2236
|
artifactHash: stageFingerprint(fs.read(artifactPath)),
|
|
@@ -2008,7 +2259,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
2008
2259
|
}
|
|
2009
2260
|
if (command === "resolve") {
|
|
2010
2261
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
2011
|
-
return
|
|
2262
|
+
return installationRequired();
|
|
2012
2263
|
const packageArg = args.find((arg) => arg.startsWith("--package="));
|
|
2013
2264
|
const sourceArg = args.find((arg) => arg.startsWith("--source="));
|
|
2014
2265
|
if (!packageArg && !sourceArg)
|
|
@@ -2070,7 +2321,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
2070
2321
|
}
|
|
2071
2322
|
if (command === "organization-policy") {
|
|
2072
2323
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
2073
|
-
return
|
|
2324
|
+
return installationRequired();
|
|
2074
2325
|
const fileArg = args.find((arg) => arg.startsWith("--file="));
|
|
2075
2326
|
if (!fileArg)
|
|
2076
2327
|
return { exitCode: 1, error: "Usage: aiw organization-policy --file=path [--replace]" };
|
|
@@ -2086,7 +2337,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
2086
2337
|
}
|
|
2087
2338
|
if (command === "policy-check") {
|
|
2088
2339
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
2089
|
-
return
|
|
2340
|
+
return installationRequired();
|
|
2090
2341
|
const policyPath = join(aiw, "organization.yml");
|
|
2091
2342
|
if (!fs.exists(policyPath))
|
|
2092
2343
|
return { exitCode: 1, error: "Organization policy is required for the CI policy gate." };
|
|
@@ -2124,7 +2375,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
2124
2375
|
}
|
|
2125
2376
|
if (command === "preset") {
|
|
2126
2377
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
2127
|
-
return
|
|
2378
|
+
return installationRequired();
|
|
2128
2379
|
const fileArg = args.find((arg) => arg.startsWith("--file="));
|
|
2129
2380
|
if (!fileArg)
|
|
2130
2381
|
return { exitCode: 1, error: "Usage: aiw preset --file=path [--replace]" };
|
|
@@ -2140,7 +2391,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
2140
2391
|
}
|
|
2141
2392
|
if (command === "update") {
|
|
2142
2393
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
2143
|
-
return
|
|
2394
|
+
return installationRequired();
|
|
2144
2395
|
const packageArg = args.find((arg) => arg.startsWith("--package="));
|
|
2145
2396
|
if (!packageArg)
|
|
2146
2397
|
return { exitCode: 1, error: "Usage: aiw update --package=path --target=target" };
|
|
@@ -2171,7 +2422,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
2171
2422
|
if (command === "registry") {
|
|
2172
2423
|
if (args[1] === "configure") {
|
|
2173
2424
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
2174
|
-
return
|
|
2425
|
+
return installationRequired();
|
|
2175
2426
|
const fileArg = args.find((arg) => arg.startsWith("--file="));
|
|
2176
2427
|
if (!fileArg)
|
|
2177
2428
|
return {
|
|
@@ -2257,9 +2508,39 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
2257
2508
|
};
|
|
2258
2509
|
}
|
|
2259
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
|
+
}
|
|
2260
2538
|
return { exitCode: 0, output };
|
|
2261
2539
|
}
|
|
2262
|
-
return {
|
|
2540
|
+
return {
|
|
2541
|
+
exitCode: 1,
|
|
2542
|
+
error: "Usage: aiw context [--layer=project --content=text] | aiw context --stage=<name> [--budget=tokens]",
|
|
2543
|
+
};
|
|
2263
2544
|
}
|
|
2264
2545
|
if (command === "context-summary") {
|
|
2265
2546
|
const summaryArg = args.find((arg) => arg.startsWith("--summary="))?.slice(10);
|
|
@@ -2277,7 +2558,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
2277
2558
|
}
|
|
2278
2559
|
if (command === "routing") {
|
|
2279
2560
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
2280
|
-
return
|
|
2561
|
+
return installationRequired();
|
|
2281
2562
|
const usage = "Usage: aiw routing init | show | resolve --category=name";
|
|
2282
2563
|
const action = args[1];
|
|
2283
2564
|
const routingPath = join(aiw, ROUTING_TABLE_PATH);
|
|
@@ -2333,7 +2614,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
2333
2614
|
}
|
|
2334
2615
|
if (command === "task-usage") {
|
|
2335
2616
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
2336
|
-
return
|
|
2617
|
+
return installationRequired();
|
|
2337
2618
|
const parsed = parseNamedOptions(args.slice(1), ["workflow"]);
|
|
2338
2619
|
if (!parsed)
|
|
2339
2620
|
return { exitCode: 1, error: "Usage: aiw task-usage [--workflow=id-or-ticket]" };
|
|
@@ -2362,7 +2643,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
2362
2643
|
}
|
|
2363
2644
|
if (command === "cost-report") {
|
|
2364
2645
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
2365
|
-
return
|
|
2646
|
+
return installationRequired();
|
|
2366
2647
|
const sidecarPath = join(aiw, USAGE_SIDECAR_PATH);
|
|
2367
2648
|
if (!fs.exists(sidecarPath))
|
|
2368
2649
|
return {
|
|
@@ -2384,7 +2665,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
2384
2665
|
}
|
|
2385
2666
|
if (command === "heartbeat") {
|
|
2386
2667
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
2387
|
-
return
|
|
2668
|
+
return installationRequired();
|
|
2388
2669
|
const parsed = parseNamedOptions(args.slice(1), ["session", "warn", "ttl"]);
|
|
2389
2670
|
if (!parsed)
|
|
2390
2671
|
return {
|
|
@@ -2434,7 +2715,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
2434
2715
|
}
|
|
2435
2716
|
if (command === "hooks") {
|
|
2436
2717
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
2437
|
-
return
|
|
2718
|
+
return installationRequired();
|
|
2438
2719
|
const action = args[1];
|
|
2439
2720
|
if (action !== "install")
|
|
2440
2721
|
return {
|
|
@@ -2478,7 +2759,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
2478
2759
|
}
|
|
2479
2760
|
if (command === "usage-sidecar") {
|
|
2480
2761
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
2481
|
-
return
|
|
2762
|
+
return installationRequired();
|
|
2482
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";
|
|
2483
2764
|
const action = args[1];
|
|
2484
2765
|
const sidecarPath = join(aiw, USAGE_SIDECAR_PATH);
|
|
@@ -2687,7 +2968,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
2687
2968
|
}
|
|
2688
2969
|
if (command === "self-validate") {
|
|
2689
2970
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
2690
|
-
return
|
|
2971
|
+
return installationRequired();
|
|
2691
2972
|
const ticket = args.find((arg) => arg.startsWith("--ticket="))?.slice(9);
|
|
2692
2973
|
if (!ticket)
|
|
2693
2974
|
return { exitCode: 1, error: "Usage: aiw self-validate --ticket=TYPE-000" };
|
|
@@ -2723,7 +3004,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
2723
3004
|
}
|
|
2724
3005
|
return {
|
|
2725
3006
|
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]",
|
|
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]",
|
|
2727
3008
|
};
|
|
2728
3009
|
}
|
|
2729
3010
|
catch (error) {
|
|
@@ -2741,12 +3022,14 @@ function readWorkflowState(aiw, fs, requireActive = true) {
|
|
|
2741
3022
|
const session = parseWorkflowSession(fs.read(sessionPath));
|
|
2742
3023
|
let ledger = parseApprovalLedger(fs.read(ledgerPath));
|
|
2743
3024
|
if (session.workflowId !== ledger.workflowId)
|
|
2744
|
-
throw new
|
|
3025
|
+
throw new BlockingError(describeBlockingReason("state_mismatch", {
|
|
3026
|
+
detail: "Workflow session and approval ledger IDs do not match; run doctor.",
|
|
3027
|
+
}));
|
|
2745
3028
|
if (requireActive && session.status !== "active")
|
|
2746
3029
|
throw new BlockingError(describeBlockingReason("workflow_complete", {
|
|
2747
3030
|
detail: `Workflow ${session.workflowId} is complete; start a new workflow first.`,
|
|
2748
3031
|
}));
|
|
2749
|
-
const changed =
|
|
3032
|
+
const changed = processStages(ledger).find((stage) => {
|
|
2750
3033
|
const record = ledger.stages[stage];
|
|
2751
3034
|
if (!record?.artifactHash || !["pending-human", "approved"].includes(record.status))
|
|
2752
3035
|
return false;
|
|
@@ -2774,6 +3057,85 @@ function workflowNextAction(ledger) {
|
|
|
2774
3057
|
return `Next action: revise ${next}, rerun its gate, and request human approval.`;
|
|
2775
3058
|
return `Next action: prepare ${next} evidence.`;
|
|
2776
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
|
+
}
|
|
2777
3139
|
function parseLightEvidence(content) {
|
|
2778
3140
|
const value = parseYaml(content);
|
|
2779
3141
|
if (!value || typeof value !== "object" || value.schema !== 1)
|
|
@@ -2794,6 +3156,11 @@ function parseLightEvidence(content) {
|
|
|
2794
3156
|
}
|
|
2795
3157
|
function assertStageReady(aiw, fs, stage) {
|
|
2796
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
|
+
}));
|
|
2797
3164
|
if (effectiveLane(session) === "light")
|
|
2798
3165
|
throw new BlockingError(describeBlockingReason("lane_escalation_required", {
|
|
2799
3166
|
stage,
|
|
@@ -2837,6 +3204,8 @@ function approvalArtifactPath(aiw, stage) {
|
|
|
2837
3204
|
verification: "generated/reports/verification-report.md",
|
|
2838
3205
|
review: "generated/reports/code-review.md",
|
|
2839
3206
|
traceability: "generated/artifacts/traceability.yml",
|
|
3207
|
+
diagnosis: "generated/reports/diagnosis.md",
|
|
3208
|
+
assessment: "generated/reports/assessment.md",
|
|
2840
3209
|
};
|
|
2841
3210
|
return resolve(aiw, paths[stage]);
|
|
2842
3211
|
}
|