@multileaf/ai-workflow 0.6.1 → 0.8.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 +19 -0
- package/README.md +9 -1
- package/dist/context-benchmark.d.ts +52 -0
- package/dist/context-benchmark.js +170 -0
- package/dist/cost-attribution.d.ts +65 -0
- package/dist/cost-attribution.js +119 -0
- package/dist/human-approval.d.ts +4 -0
- package/dist/human-approval.js +22 -2
- package/dist/interpreter.d.ts +8 -1
- package/dist/interpreter.js +8 -1
- package/dist/model-routing.d.ts +38 -0
- package/dist/model-routing.js +116 -0
- package/dist/recommendations.js +13 -0
- package/dist/task-parity.d.ts +67 -0
- package/dist/task-parity.js +202 -0
- package/dist/token-usage.d.ts +6 -0
- package/dist/token-usage.js +32 -5
- package/dist/traceability.d.ts +2 -0
- package/dist/traceability.js +35 -0
- package/dist/usage-sidecar.d.ts +56 -0
- package/dist/usage-sidecar.js +187 -0
- package/dist/workflow.js +470 -50
- package/docs/README.md +2 -0
- package/docs/telemetry.md +3 -1
- package/docs/token-efficiency-research.md +135 -0
- package/docs/token-efficiency-study-notes.md +128 -0
- package/docs/token-efficiency.md +104 -0
- package/package.json +1 -1
- package/resources/package.yaml +1 -0
- package/resources/skills/ticket.md +18 -0
package/dist/workflow.js
CHANGED
|
@@ -34,9 +34,15 @@ import { enforceOrganizationPolicy, parseOrganizationPolicy, serializeOrganizati
|
|
|
34
34
|
import { composeContext, CONTEXT_LAYERS, readContextStore, writeContextLayer, } from "./context-store.js";
|
|
35
35
|
import { loadContextDelta, retrieveTaskContext } from "./context-retrieval.js";
|
|
36
36
|
import { createSummaryCache, isSummaryFresh, parseSummaryCache, serializeSummaryCache, } from "./context-cache.js";
|
|
37
|
-
import { parseTokenUsage, recordTokenUsage, serializeTokenUsage } from "./token-usage.js";
|
|
38
|
-
import { APPROVAL_STAGES, approveStage, beginStageApproval, createApprovalLedger, getUnresolvedPrerequisites, invalidateStageAndDescendants, parseApprovalLedger, parseWorkflowSession, rejectStage, serializeApprovalLedger, serializeWorkflowSession, skipOptionalStage, stageDefinition, stageFingerprint, } from "./human-approval.js";
|
|
37
|
+
import { parseTokenUsage, recordTokenUsage, serializeTokenUsage, summarizeUsageByModel, } from "./token-usage.js";
|
|
38
|
+
import { APPROVAL_STAGES, assertWorkflowId, validateTaskTicket, approveStage, beginStageApproval, createApprovalLedger, getUnresolvedPrerequisites, invalidateStageAndDescendants, parseApprovalLedger, parseWorkflowSession, rejectStage, serializeApprovalLedger, serializeWorkflowSession, skipOptionalStage, stageDefinition, stageFingerprint, } from "./human-approval.js";
|
|
39
39
|
import { measureContextQuality, serializeContextQuality } from "./context-quality.js";
|
|
40
|
+
import { collectContextBench, compareContextBench, parseContextBench, serializeContextBench, } from "./context-benchmark.js";
|
|
41
|
+
import { materializeParityTask, parseParityTask, runParitySuite, serializeParitySuite, TASK_ASSEMBLER, } from "./task-parity.js";
|
|
42
|
+
import { appendUsageSidecarEntry, parseUsageSidecar, serializeUsageSidecar, serializeUsageSidecarSummary, summarizeUsageSidecar, USAGE_SIDECAR_PATH, USAGE_TIERS, } from "./usage-sidecar.js";
|
|
43
|
+
import { computeCostAttribution, serializeCostAttribution, serializeTaskUsage, summarizeTaskUsage, } from "./cost-attribution.js";
|
|
44
|
+
import { defaultRoutingTable, parseRoutingTable, resolveRoute, ROUTING_TABLE_PATH, serializeResolvedRoute, serializeRoutingTable, } from "./model-routing.js";
|
|
45
|
+
import { parseTraceabilityGraph } from "./traceability.js";
|
|
40
46
|
import { serializeAdapterCapabilities } from "./adapter-contract.js";
|
|
41
47
|
import { buildTraceabilityGraph, serializeTraceabilityGraph } from "./traceability.js";
|
|
42
48
|
import { evaluateQualityGate } from "./quality-gates.js";
|
|
@@ -46,61 +52,122 @@ import { enforcePolicyGate, gitPolicyTracking } from "./policy-gate.js";
|
|
|
46
52
|
import { DEFAULT_TELEMETRY_CONFIG, loadTelemetryConfig, offerTelemetryConsent, parseTelemetryConfig, purgeTelemetryBuffer, readTelemetryBuffer, serializeTelemetryMapping, TELEMETRY_BUFFER_CAP, TELEMETRY_BUFFER_FILE, } from "./telemetry.js";
|
|
47
53
|
import { createPluginScaffold, validatePluginDirectory, writePluginScaffold, } from "./plugin-sdk.js";
|
|
48
54
|
import { executeOrchestrationPlan, parseOrchestrationPlan, serializeOrchestrationPreview, serializeOrchestrationResults, validateOrchestrationParallelism, } from "./orchestration.js";
|
|
55
|
+
function beginWorkflowSession(fs, aiw, options) {
|
|
56
|
+
const sessionPath = join(aiw, "workflow.yml");
|
|
57
|
+
const ledgerPath = join(aiw, "approvals.yml");
|
|
58
|
+
if (fs.exists(sessionPath)) {
|
|
59
|
+
const previous = parseWorkflowSession(fs.read(sessionPath));
|
|
60
|
+
if (previous.status === "active")
|
|
61
|
+
return {
|
|
62
|
+
exitCode: 1,
|
|
63
|
+
error: `Workflow ${previous.workflowId} is still active. Complete it before starting another.`,
|
|
64
|
+
};
|
|
65
|
+
if (!fs.exists(ledgerPath))
|
|
66
|
+
return { exitCode: 1, error: "Completed workflow is missing its approval ledger." };
|
|
67
|
+
const archive = join(aiw, `checkpoints/workflow-${previous.workflowId}-approvals.yml`);
|
|
68
|
+
if (fs.exists(archive))
|
|
69
|
+
return { exitCode: 1, error: `Workflow approval archive already exists: ${archive}` };
|
|
70
|
+
fs.mkdir(join(aiw, "checkpoints"));
|
|
71
|
+
fs.write(archive, fs.read(ledgerPath));
|
|
72
|
+
}
|
|
73
|
+
else if (fs.exists(ledgerPath)) {
|
|
74
|
+
return {
|
|
75
|
+
exitCode: 1,
|
|
76
|
+
error: "Approval ledger exists without a workflow session; run doctor.",
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
const workflowId = options.id ?? randomUUID();
|
|
80
|
+
const priorRun = join(aiw, `checkpoints/workflow-${workflowId}-approvals.yml`);
|
|
81
|
+
if (fs.exists(priorRun))
|
|
82
|
+
return {
|
|
83
|
+
exitCode: 1,
|
|
84
|
+
error: `Workflow ID has already been used: ${workflowId}. Choose a new ID.`,
|
|
85
|
+
};
|
|
86
|
+
const now = new Date().toISOString();
|
|
87
|
+
const session = {
|
|
88
|
+
schema: 1,
|
|
89
|
+
workflowId,
|
|
90
|
+
status: "active",
|
|
91
|
+
startedAt: now,
|
|
92
|
+
updatedAt: now,
|
|
93
|
+
...(options.ticket ? { ticket: options.ticket } : {}),
|
|
94
|
+
...(options.title ? { title: options.title } : {}),
|
|
95
|
+
};
|
|
96
|
+
const ledger = createApprovalLedger(workflowId);
|
|
97
|
+
fs.write(ledgerPath, serializeApprovalLedger(ledger));
|
|
98
|
+
fs.write(sessionPath, serializeWorkflowSession(session));
|
|
99
|
+
return { exitCode: 0, output: `Workflow started: ${workflowId}.` };
|
|
100
|
+
}
|
|
49
101
|
export async function runCommand(args, root, fs, services = {}) {
|
|
50
102
|
const aiw = join(root, WORKFLOW_DIRECTORY);
|
|
51
103
|
try {
|
|
52
104
|
fs = withSafeProjectWrites(fs, root);
|
|
53
105
|
const command = args[0] ?? "help";
|
|
106
|
+
if (command === "task") {
|
|
107
|
+
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
108
|
+
return { exitCode: 1, error: "Run `aiw install` first." };
|
|
109
|
+
const usage = "Usage: aiw task start --ticket=TEAM-123 [--title=text] [--id=workflow-id] | aiw task status";
|
|
110
|
+
const action = args[1];
|
|
111
|
+
const sessionPath = join(aiw, "workflow.yml");
|
|
112
|
+
if (action === "start") {
|
|
113
|
+
const options = parseNamedOptions(args.slice(2), ["ticket", "title", "id"]);
|
|
114
|
+
if (!options?.ticket)
|
|
115
|
+
return { exitCode: 1, error: usage };
|
|
116
|
+
validateTaskTicket(options.ticket);
|
|
117
|
+
if (options.id !== undefined) {
|
|
118
|
+
try {
|
|
119
|
+
assertWorkflowId(options.id);
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return { exitCode: 1, error: "Workflow ID must be a UUID." };
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const result = beginWorkflowSession(fs, aiw, {
|
|
126
|
+
...(options.id ? { id: options.id } : {}),
|
|
127
|
+
ticket: options.ticket,
|
|
128
|
+
...(options.title ? { title: options.title } : {}),
|
|
129
|
+
});
|
|
130
|
+
if (result.exitCode !== 0)
|
|
131
|
+
return result;
|
|
132
|
+
const title = options.title ? ` titled "${options.title}"` : "";
|
|
133
|
+
return {
|
|
134
|
+
exitCode: 0,
|
|
135
|
+
output: `Task started for ticket ${options.ticket}${title}: ${result.output}\n` +
|
|
136
|
+
`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=...\`.`,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
if (action === "status") {
|
|
140
|
+
if (!fs.exists(sessionPath))
|
|
141
|
+
return {
|
|
142
|
+
exitCode: 0,
|
|
143
|
+
output: "No SDD workflow is active. Start one with `aiw task start --ticket=TEAM-123`.",
|
|
144
|
+
};
|
|
145
|
+
const { session, ledger } = readWorkflowState(aiw, fs, false);
|
|
146
|
+
const lines = [
|
|
147
|
+
...(session.ticket
|
|
148
|
+
? [`Ticket: ${session.ticket}${session.title ? ` — ${session.title}` : ""}`]
|
|
149
|
+
: []),
|
|
150
|
+
`Workflow: ${session.workflowId} (${session.status})`,
|
|
151
|
+
"Stages:",
|
|
152
|
+
...APPROVAL_STAGES.map((stage) => `- ${stage}: ${ledger.stages[stage]?.status ?? "not-started"}`),
|
|
153
|
+
];
|
|
154
|
+
const next = APPROVAL_STAGES.find((stage) => (ledger.stages[stage]?.status ?? "not-started") === "not-started");
|
|
155
|
+
if (session.status === "active" && next)
|
|
156
|
+
lines.push(`Next: ${next === "brainstorming" && session.ticket ? `decide brainstorm or record an accepted skip (ticket ${session.ticket} already provides the requirement)` : `advance ${next} (gate, approval)`}.`);
|
|
157
|
+
return { exitCode: 0, output: lines.join("\n") };
|
|
158
|
+
}
|
|
159
|
+
return { exitCode: 1, error: usage };
|
|
160
|
+
}
|
|
54
161
|
if (command === "workflow") {
|
|
55
162
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
56
163
|
return { exitCode: 1, error: "Run `aiw install` first." };
|
|
57
164
|
const action = args[1];
|
|
58
165
|
const sessionPath = join(aiw, "workflow.yml");
|
|
59
|
-
const ledgerPath = join(aiw, "approvals.yml");
|
|
60
166
|
if (action === "start") {
|
|
61
167
|
const options = parseNamedOptions(args.slice(2), ["id"]);
|
|
62
168
|
if (!options)
|
|
63
169
|
return { exitCode: 1, error: "Usage: aiw workflow start [--id=workflow-id]" };
|
|
64
|
-
|
|
65
|
-
const previous = parseWorkflowSession(fs.read(sessionPath));
|
|
66
|
-
if (previous.status === "active")
|
|
67
|
-
return {
|
|
68
|
-
exitCode: 1,
|
|
69
|
-
error: `Workflow ${previous.workflowId} is still active. Complete it before starting another.`,
|
|
70
|
-
};
|
|
71
|
-
if (!fs.exists(ledgerPath))
|
|
72
|
-
return { exitCode: 1, error: "Completed workflow is missing its approval ledger." };
|
|
73
|
-
const archive = join(aiw, `checkpoints/workflow-${previous.workflowId}-approvals.yml`);
|
|
74
|
-
if (fs.exists(archive))
|
|
75
|
-
return { exitCode: 1, error: `Workflow approval archive already exists: ${archive}` };
|
|
76
|
-
fs.mkdir(join(aiw, "checkpoints"));
|
|
77
|
-
fs.write(archive, fs.read(ledgerPath));
|
|
78
|
-
}
|
|
79
|
-
else if (fs.exists(ledgerPath)) {
|
|
80
|
-
return {
|
|
81
|
-
exitCode: 1,
|
|
82
|
-
error: "Approval ledger exists without a workflow session; run doctor.",
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
const workflowId = options.id ?? randomUUID();
|
|
86
|
-
const priorRun = join(aiw, `checkpoints/workflow-${workflowId}-approvals.yml`);
|
|
87
|
-
if (fs.exists(priorRun))
|
|
88
|
-
return {
|
|
89
|
-
exitCode: 1,
|
|
90
|
-
error: `Workflow ID has already been used: ${workflowId}. Choose a new ID.`,
|
|
91
|
-
};
|
|
92
|
-
const now = new Date().toISOString();
|
|
93
|
-
const session = {
|
|
94
|
-
schema: 1,
|
|
95
|
-
workflowId,
|
|
96
|
-
status: "active",
|
|
97
|
-
startedAt: now,
|
|
98
|
-
updatedAt: now,
|
|
99
|
-
};
|
|
100
|
-
const ledger = createApprovalLedger(workflowId);
|
|
101
|
-
fs.write(ledgerPath, serializeApprovalLedger(ledger));
|
|
102
|
-
fs.write(sessionPath, serializeWorkflowSession(session));
|
|
103
|
-
return { exitCode: 0, output: `Workflow started: ${workflowId}.` };
|
|
170
|
+
return beginWorkflowSession(fs, aiw, { id: options.id });
|
|
104
171
|
}
|
|
105
172
|
if (action === "status") {
|
|
106
173
|
if (!fs.exists(sessionPath))
|
|
@@ -359,13 +426,44 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
359
426
|
if (command === "telemetry") {
|
|
360
427
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
361
428
|
return { exitCode: 1, error: "Run `aiw install` first." };
|
|
362
|
-
const usage = "Usage: aiw telemetry <status|enable|disable> [--verbose] [--commands=include|exclude] [--outcomes=include|exclude]";
|
|
429
|
+
const usage = "Usage: aiw telemetry <status|show|enable|disable> [--verbose] [--commands=include|exclude] [--outcomes=include|exclude]";
|
|
363
430
|
const action = args[1];
|
|
364
431
|
if (!action)
|
|
365
432
|
return { exitCode: 1, error: usage };
|
|
366
433
|
const telemetryConfig = loadTelemetryConfig(fs, aiw);
|
|
367
434
|
const actionArgs = args.slice(2).filter((arg) => arg !== "--verbose");
|
|
368
435
|
const verbose = args.includes("--verbose");
|
|
436
|
+
if (action === "show") {
|
|
437
|
+
if (actionArgs.length !== 0)
|
|
438
|
+
return { exitCode: 1, error: usage };
|
|
439
|
+
const buffered = readTelemetryBuffer(fs, aiw);
|
|
440
|
+
const byCommand = new Map();
|
|
441
|
+
for (const event of buffered) {
|
|
442
|
+
const key = event.command ?? "unknown";
|
|
443
|
+
const entry = byCommand.get(key) ?? { success: 0, failure: 0 };
|
|
444
|
+
if (event.outcome === "failure")
|
|
445
|
+
entry.failure += 1;
|
|
446
|
+
else
|
|
447
|
+
entry.success += 1;
|
|
448
|
+
byCommand.set(key, entry);
|
|
449
|
+
}
|
|
450
|
+
const lines = [
|
|
451
|
+
`Telemetry is ${telemetryConfig.enabled ? "enabled" : "disabled"}.`,
|
|
452
|
+
`Buffered events: ${buffered.length}/${TELEMETRY_BUFFER_CAP} (.aiw/${TELEMETRY_BUFFER_FILE}).`,
|
|
453
|
+
];
|
|
454
|
+
if (buffered.length > 0) {
|
|
455
|
+
lines.push("By command:");
|
|
456
|
+
for (const [command, { success, failure }] of [...byCommand].sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0))
|
|
457
|
+
lines.push(`- ${command}: ${success} success, ${failure} failure`);
|
|
458
|
+
}
|
|
459
|
+
if (verbose && buffered.length > 0) {
|
|
460
|
+
lines.push("Events:");
|
|
461
|
+
lines.push(...buffered.map((event) => `- ${JSON.stringify(event)}`));
|
|
462
|
+
}
|
|
463
|
+
if (!telemetryConfig.enabled)
|
|
464
|
+
lines.push("Note: telemetry is disabled, so no new events are recorded; the buffer keeps previously collected events until purged.");
|
|
465
|
+
return { exitCode: 0, output: lines.join("\n") };
|
|
466
|
+
}
|
|
369
467
|
if (action === "status") {
|
|
370
468
|
if (actionArgs.length !== 0)
|
|
371
469
|
return { exitCode: 1, error: usage };
|
|
@@ -438,16 +536,120 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
438
536
|
}
|
|
439
537
|
return { exitCode: 1, error: usage };
|
|
440
538
|
}
|
|
539
|
+
if (command === "context-bench") {
|
|
540
|
+
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
541
|
+
return { exitCode: 1, error: "Run `aiw install` first." };
|
|
542
|
+
const saveCount = args.filter((arg) => arg === "--save").length;
|
|
543
|
+
if (saveCount > 1)
|
|
544
|
+
return { exitCode: 1, error: "Usage: aiw context-bench [--baseline=path] [--save]" };
|
|
545
|
+
const save = saveCount === 1;
|
|
546
|
+
const parsed = parseNamedOptions(args.slice(1).filter((arg) => arg !== "--save"), ["baseline"]);
|
|
547
|
+
if (!parsed)
|
|
548
|
+
return { exitCode: 1, error: "Usage: aiw context-bench [--baseline=path] [--save]" };
|
|
549
|
+
const report = collectContextBench(root, fs);
|
|
550
|
+
const serialized = serializeContextBench(report);
|
|
551
|
+
const lines = [serialized.trimEnd()];
|
|
552
|
+
if (parsed.baseline) {
|
|
553
|
+
const baselinePath = safeProjectFile(root, parsed.baseline, fs);
|
|
554
|
+
if (!fs.exists(baselinePath))
|
|
555
|
+
return { exitCode: 1, error: `Baseline not found: ${parsed.baseline}` };
|
|
556
|
+
const comparison = compareContextBench(parseContextBench(fs.read(baselinePath)), report);
|
|
557
|
+
lines.push(`Baseline: ${parsed.baseline} (bundle ${comparison.bundleChanged ? "changed" : "identical"}, token delta ${comparison.totalTokenDelta >= 0 ? "+" : ""}${comparison.totalTokenDelta})`);
|
|
558
|
+
if (comparison.regressions.length)
|
|
559
|
+
return {
|
|
560
|
+
exitCode: 1,
|
|
561
|
+
error: `Standing context regressed against baseline:\n${comparison.regressions.map((regression) => `- ${regression}`).join("\n")}`,
|
|
562
|
+
output: lines.join("\n"),
|
|
563
|
+
};
|
|
564
|
+
lines.push("No regressions against baseline.");
|
|
565
|
+
}
|
|
566
|
+
if (save) {
|
|
567
|
+
const checkpoint = join(aiw, `checkpoints/context-bench-${report.bundleHash}.yml`);
|
|
568
|
+
if (!fs.createExclusive)
|
|
569
|
+
return { exitCode: 1, error: "The filesystem does not support atomic checkpoints." };
|
|
570
|
+
if (!fs.createExclusive(checkpoint, serialized)) {
|
|
571
|
+
if (fs.read(checkpoint) !== serialized)
|
|
572
|
+
return {
|
|
573
|
+
exitCode: 1,
|
|
574
|
+
error: `Checkpoint already exists with different content: ${checkpoint}`,
|
|
575
|
+
};
|
|
576
|
+
lines.push(`Checkpoint already recorded: ${checkpoint}`);
|
|
577
|
+
}
|
|
578
|
+
else {
|
|
579
|
+
lines.push(`Checkpoint saved: ${checkpoint}`);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
return { exitCode: 0, output: lines.join("\n") };
|
|
583
|
+
}
|
|
584
|
+
if (command === "parity") {
|
|
585
|
+
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
586
|
+
return { exitCode: 1, error: "Run `aiw install` first." };
|
|
587
|
+
const parsed = parseNamedOptions(args.slice(1), ["tasks"]);
|
|
588
|
+
if (!parsed)
|
|
589
|
+
return { exitCode: 1, error: "Usage: aiw parity [--tasks=directory]" };
|
|
590
|
+
const tasksPath = parsed.tasks;
|
|
591
|
+
if (tasksPath && (isAbsolute(tasksPath) || tasksPath.split(/[\\/]/).includes("..")))
|
|
592
|
+
return {
|
|
593
|
+
exitCode: 1,
|
|
594
|
+
error: "Parity tasks directory must be a safe project-relative path.",
|
|
595
|
+
};
|
|
596
|
+
const tasksDirectory = tasksPath ? join(root, tasksPath) : join(aiw, "parity");
|
|
597
|
+
if (fs.pathType?.(tasksDirectory) !== "directory" || !fs.list)
|
|
598
|
+
return {
|
|
599
|
+
exitCode: 1,
|
|
600
|
+
error: `Parity tasks directory not found: ${relative(root, tasksDirectory).split(sep).join("/")}. Add YAML tasks (see docs/token-efficiency.md).`,
|
|
601
|
+
};
|
|
602
|
+
const taskFiles = (fs.list(tasksDirectory) ?? [])
|
|
603
|
+
.filter((name) => name.endsWith(".yml"))
|
|
604
|
+
.sort();
|
|
605
|
+
if (!taskFiles.length)
|
|
606
|
+
return {
|
|
607
|
+
exitCode: 1,
|
|
608
|
+
error: `No parity tasks found in ${relative(root, tasksDirectory).split(sep).join("/")}. Each task is one YAML file with schema, id, layers, critical_facts, and completion_markers.`,
|
|
609
|
+
};
|
|
610
|
+
const tasks = taskFiles.map((name) => {
|
|
611
|
+
const path = join(tasksDirectory, name);
|
|
612
|
+
try {
|
|
613
|
+
return materializeParityTask(parseParityTask(fs.read(path)), (source) => {
|
|
614
|
+
const absolute = join(root, source);
|
|
615
|
+
if (fs.pathType?.(absolute) !== "file")
|
|
616
|
+
throw new Error("source is not a file");
|
|
617
|
+
return fs.read(absolute);
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
catch (error) {
|
|
621
|
+
throw new Error(`${name}: ${error.message}`, { cause: error });
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
const suite = runParitySuite(tasks, TASK_ASSEMBLER);
|
|
625
|
+
const serialized = serializeParitySuite(suite);
|
|
626
|
+
if (suite.failed)
|
|
627
|
+
return {
|
|
628
|
+
exitCode: 1,
|
|
629
|
+
output: serialized.trimEnd(),
|
|
630
|
+
error: `Task parity failed for ${suite.failed} of ${suite.total} task(s): critical facts or completion markers were lost by the ${TASK_ASSEMBLER.name} assembler. Fix context assembly or move the facts to a retrieved layer; do not delete the expectations.`,
|
|
631
|
+
};
|
|
632
|
+
return {
|
|
633
|
+
exitCode: 0,
|
|
634
|
+
output: serialized.trimEnd() +
|
|
635
|
+
`\nTask parity passed: every critical fact survived the ${TASK_ASSEMBLER.name} assembler.`,
|
|
636
|
+
};
|
|
637
|
+
}
|
|
441
638
|
if (command === "scan") {
|
|
442
639
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
443
640
|
return { exitCode: 1, error: "Run `aiw install` first." };
|
|
444
641
|
const profile = await (services.profiler?.profile(root) ?? profileProject(root));
|
|
445
642
|
let interpreter = services.interpreter;
|
|
446
643
|
let interpreterModel;
|
|
644
|
+
let interpreterUsage;
|
|
447
645
|
if (!interpreter) {
|
|
448
646
|
const aiConfig = loadAiProviderConfig(fs, aiw);
|
|
449
647
|
if (aiConfig?.enabled) {
|
|
450
|
-
interpreter = createProjectInterpreter(root, services.aiProvider ?? createOpenAiCompatibleProvider(aiConfig)
|
|
648
|
+
interpreter = createProjectInterpreter(root, services.aiProvider ?? createOpenAiCompatibleProvider(aiConfig), undefined, {
|
|
649
|
+
onUsage: (usage) => {
|
|
650
|
+
interpreterUsage = usage;
|
|
651
|
+
},
|
|
652
|
+
});
|
|
451
653
|
interpreterModel = aiConfig.model;
|
|
452
654
|
}
|
|
453
655
|
}
|
|
@@ -477,6 +679,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
477
679
|
label: "embedding",
|
|
478
680
|
tokens: Math.ceil(buildInterpreterQuery(profile).length / 4),
|
|
479
681
|
detail: `${contextConfig.model} — scoped interpreter context to ${matches.length} semantic matches`,
|
|
682
|
+
model: contextConfig.model,
|
|
480
683
|
});
|
|
481
684
|
}
|
|
482
685
|
}
|
|
@@ -488,10 +691,14 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
488
691
|
const interpretationRequest = createInterpretationRequest(profile, scopedFiles, 4000, priorityFiles);
|
|
489
692
|
const inferred = await interpreter.interpret(interpretationRequest);
|
|
490
693
|
profile.facts = mergeFacts(profile.facts, inferred);
|
|
694
|
+
const actualTokens = interpreterUsage
|
|
695
|
+
? interpreterUsage.promptTokens + interpreterUsage.completionTokens
|
|
696
|
+
: undefined;
|
|
491
697
|
modelUsage.push({
|
|
492
698
|
label: "interpreter",
|
|
493
|
-
tokens: interpretationRequest.estimatedTokens,
|
|
494
|
-
detail: `${interpreterModel} — ${inferred.length} facts from ${interpretationRequest.files.length} files`,
|
|
699
|
+
tokens: actualTokens ?? interpretationRequest.estimatedTokens,
|
|
700
|
+
detail: `${interpreterModel} — ${inferred.length} facts from ${interpretationRequest.files.length} files${actualTokens !== undefined ? " (measured)" : ""}`,
|
|
701
|
+
model: interpreterModel ?? "unknown",
|
|
495
702
|
});
|
|
496
703
|
}
|
|
497
704
|
const overrides = readFactOverrides(fs, aiw);
|
|
@@ -530,6 +737,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
530
737
|
label: "embedding",
|
|
531
738
|
tokens: Math.ceil(indexedChars / 4),
|
|
532
739
|
detail: `${semanticConfig.model} — indexed ${index.chunks.length} chunks in ${Math.ceil(index.chunks.length / 64)} batch requests`,
|
|
740
|
+
model: semanticConfig.model,
|
|
533
741
|
});
|
|
534
742
|
}
|
|
535
743
|
if (modelUsage.length > 0) {
|
|
@@ -541,8 +749,31 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
541
749
|
stage: `scan.${usage.label}`,
|
|
542
750
|
budget: usage.tokens,
|
|
543
751
|
used: usage.tokens,
|
|
752
|
+
model: usage.model,
|
|
544
753
|
});
|
|
545
754
|
fs.write(usagePath, serializeTokenUsage(next));
|
|
755
|
+
const sidecarPath = join(aiw, USAGE_SIDECAR_PATH);
|
|
756
|
+
const ledger = fs.exists(sidecarPath) ? parseUsageSidecar(fs.read(sidecarPath)) : [];
|
|
757
|
+
let sidecar = ledger;
|
|
758
|
+
for (const usage of modelUsage) {
|
|
759
|
+
const measured = usage.detail.includes("(measured)");
|
|
760
|
+
const isInterpreter = usage.label === "interpreter" && measured && interpreterUsage;
|
|
761
|
+
sidecar = appendUsageSidecarEntry(sidecar, {
|
|
762
|
+
command: `scan.${usage.label}`,
|
|
763
|
+
label: measured ? "measured" : "estimated",
|
|
764
|
+
tiers: isInterpreter
|
|
765
|
+
? {
|
|
766
|
+
"cache-read": 0,
|
|
767
|
+
"fresh-input": interpreterUsage.promptTokens,
|
|
768
|
+
"cache-write": 0,
|
|
769
|
+
output: interpreterUsage.completionTokens,
|
|
770
|
+
}
|
|
771
|
+
: { "cache-read": 0, "fresh-input": usage.tokens, "cache-write": 0, output: 0 },
|
|
772
|
+
model: usage.model,
|
|
773
|
+
harness: "aiw-cli",
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
fs.write(sidecarPath, serializeUsageSidecar(sidecar));
|
|
546
777
|
}
|
|
547
778
|
return {
|
|
548
779
|
exitCode: 0,
|
|
@@ -1076,6 +1307,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
1076
1307
|
stage: "recommend.decision",
|
|
1077
1308
|
budget: decisionTokens,
|
|
1078
1309
|
used: decisionTokens,
|
|
1310
|
+
model: rankingConfig.model,
|
|
1079
1311
|
})));
|
|
1080
1312
|
ordered = ranked.items;
|
|
1081
1313
|
}
|
|
@@ -1409,8 +1641,11 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
1409
1641
|
return { exitCode: 1, error: "Run `aiw install` first." };
|
|
1410
1642
|
assertStageReady(aiw, fs, "specification");
|
|
1411
1643
|
const title = args.find((arg) => arg.startsWith("--title="))?.slice(8) || "Untitled Specification";
|
|
1644
|
+
const ticket = args.find((arg) => arg.startsWith("--ticket="))?.slice(9);
|
|
1645
|
+
if (ticket !== undefined)
|
|
1646
|
+
validateTaskTicket(ticket);
|
|
1412
1647
|
const path = join(aiw, "generated/specs/specification.md");
|
|
1413
|
-
fs.write(path, `# ${title}\n\n## Context\n\n## Requirements\n\n### REQ-001: Requirement title\n\n- **Description:**\n- **Acceptance criteria:**\n - Given\n - When\n - Then\n\n## Non-functional Requirements\n\n## Constraints\n\n## Open Questions\n\n`);
|
|
1648
|
+
fs.write(path, `# ${title}\n${ticket ? `\n> Ticket: ${ticket}\n` : ""}\n## Context\n\n## Requirements\n\n### REQ-001: Requirement title\n\n- **Description:**\n- **Acceptance criteria:**\n - Given\n - When\n - Then\n\n## Non-functional Requirements\n\n## Constraints\n\n## Open Questions\n\n`);
|
|
1414
1649
|
return { exitCode: 0, output: `Specification artifact created: ${path}` };
|
|
1415
1650
|
}
|
|
1416
1651
|
if (command === "adr") {
|
|
@@ -1814,6 +2049,183 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
1814
2049
|
fs.write(cachePath, serializeSummaryCache(createSummaryCache(context, summaryArg)));
|
|
1815
2050
|
return { exitCode: 0, output: summaryArg };
|
|
1816
2051
|
}
|
|
2052
|
+
if (command === "routing") {
|
|
2053
|
+
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
2054
|
+
return { exitCode: 1, error: "Run `aiw install` first." };
|
|
2055
|
+
const usage = "Usage: aiw routing init | show | resolve --category=name";
|
|
2056
|
+
const action = args[1];
|
|
2057
|
+
const routingPath = join(aiw, ROUTING_TABLE_PATH);
|
|
2058
|
+
if (action === "init") {
|
|
2059
|
+
if (fs.exists(routingPath))
|
|
2060
|
+
return {
|
|
2061
|
+
exitCode: 1,
|
|
2062
|
+
error: `Routing table already exists: .aiw/${ROUTING_TABLE_PATH}. Edit it directly instead of re-initializing.`,
|
|
2063
|
+
};
|
|
2064
|
+
fs.write(routingPath, serializeRoutingTable(defaultRoutingTable()) +
|
|
2065
|
+
"# Add one rule per task category (dot-separated kebab-case, e.g. scan.interpreter).\n" +
|
|
2066
|
+
"# Name the evidence behind every rule — a benchmark, parity run, or ADR — before trusting it.\n" +
|
|
2067
|
+
"# Routing is advisory: humans can always override the resolved model.\n");
|
|
2068
|
+
return {
|
|
2069
|
+
exitCode: 0,
|
|
2070
|
+
output: `Routing table created: .aiw/${ROUTING_TABLE_PATH}. Add rules with named evidence, then resolve with \`aiw routing resolve --category=...\`.`,
|
|
2071
|
+
};
|
|
2072
|
+
}
|
|
2073
|
+
if (action === "show") {
|
|
2074
|
+
if (!fs.exists(routingPath))
|
|
2075
|
+
return {
|
|
2076
|
+
exitCode: 1,
|
|
2077
|
+
error: `No routing table found. Run \`aiw routing init\` to create .aiw/${ROUTING_TABLE_PATH}.`,
|
|
2078
|
+
};
|
|
2079
|
+
return {
|
|
2080
|
+
exitCode: 0,
|
|
2081
|
+
output: serializeRoutingTable(parseRoutingTable(fs.read(routingPath))).trimEnd(),
|
|
2082
|
+
};
|
|
2083
|
+
}
|
|
2084
|
+
if (action === "resolve") {
|
|
2085
|
+
const category = args.find((arg) => arg.startsWith("--category="))?.slice(11);
|
|
2086
|
+
if (!category)
|
|
2087
|
+
return { exitCode: 1, error: usage };
|
|
2088
|
+
if (!fs.exists(routingPath))
|
|
2089
|
+
return {
|
|
2090
|
+
exitCode: 1,
|
|
2091
|
+
error: `No routing table found. Run \`aiw routing init\` first.`,
|
|
2092
|
+
};
|
|
2093
|
+
let route;
|
|
2094
|
+
try {
|
|
2095
|
+
route = resolveRoute(parseRoutingTable(fs.read(routingPath)), category);
|
|
2096
|
+
}
|
|
2097
|
+
catch (error) {
|
|
2098
|
+
throw new Error(`Routing failed: ${error.message}`, { cause: error });
|
|
2099
|
+
}
|
|
2100
|
+
return {
|
|
2101
|
+
exitCode: 0,
|
|
2102
|
+
output: serializeResolvedRoute(route) +
|
|
2103
|
+
"\noverride: advisory only — a human may choose a different model at any time.",
|
|
2104
|
+
};
|
|
2105
|
+
}
|
|
2106
|
+
return { exitCode: 1, error: usage };
|
|
2107
|
+
}
|
|
2108
|
+
if (command === "task-usage") {
|
|
2109
|
+
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
2110
|
+
return { exitCode: 1, error: "Run `aiw install` first." };
|
|
2111
|
+
const parsed = parseNamedOptions(args.slice(1), ["workflow"]);
|
|
2112
|
+
if (!parsed)
|
|
2113
|
+
return { exitCode: 1, error: "Usage: aiw task-usage [--workflow=id-or-ticket]" };
|
|
2114
|
+
const sidecarPath = join(aiw, USAGE_SIDECAR_PATH);
|
|
2115
|
+
if (!fs.exists(sidecarPath))
|
|
2116
|
+
return {
|
|
2117
|
+
exitCode: 1,
|
|
2118
|
+
error: "No usage sidecar recorded yet. Correlate runs with `aiw usage-sidecar record --workflow=<id-or-ticket>` or run `aiw scan`.",
|
|
2119
|
+
};
|
|
2120
|
+
const sessionPath = join(aiw, "workflow.yml");
|
|
2121
|
+
const workflow = parsed.workflow ??
|
|
2122
|
+
(fs.exists(sessionPath)
|
|
2123
|
+
? (parseWorkflowSession(fs.read(sessionPath)).ticket ??
|
|
2124
|
+
parseWorkflowSession(fs.read(sessionPath)).workflowId)
|
|
2125
|
+
: undefined);
|
|
2126
|
+
if (!workflow)
|
|
2127
|
+
return {
|
|
2128
|
+
exitCode: 1,
|
|
2129
|
+
error: "No workflow specified and no active SDD task. Pass --workflow=<id-or-ticket>.",
|
|
2130
|
+
};
|
|
2131
|
+
const summary = summarizeTaskUsage(parseUsageSidecar(fs.read(sidecarPath)), workflow);
|
|
2132
|
+
return {
|
|
2133
|
+
exitCode: 0,
|
|
2134
|
+
output: serializeTaskUsage(summary),
|
|
2135
|
+
};
|
|
2136
|
+
}
|
|
2137
|
+
if (command === "cost-report") {
|
|
2138
|
+
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
2139
|
+
return { exitCode: 1, error: "Run `aiw install` first." };
|
|
2140
|
+
const sidecarPath = join(aiw, USAGE_SIDECAR_PATH);
|
|
2141
|
+
if (!fs.exists(sidecarPath))
|
|
2142
|
+
return {
|
|
2143
|
+
exitCode: 1,
|
|
2144
|
+
error: "No usage sidecar recorded yet. Record runs with `aiw usage-sidecar record` or run `aiw scan`.",
|
|
2145
|
+
};
|
|
2146
|
+
const tracePath = join(aiw, "generated/artifacts/traceability.yml");
|
|
2147
|
+
if (!fs.exists(tracePath))
|
|
2148
|
+
return {
|
|
2149
|
+
exitCode: 1,
|
|
2150
|
+
error: "Traceability graph is missing. Run `aiw trace` to build it from the approved specification and plan.",
|
|
2151
|
+
};
|
|
2152
|
+
const attribution = computeCostAttribution(parseUsageSidecar(fs.read(sidecarPath)), parseTraceabilityGraph(fs.read(tracePath)));
|
|
2153
|
+
return {
|
|
2154
|
+
exitCode: 0,
|
|
2155
|
+
output: serializeCostAttribution(attribution) +
|
|
2156
|
+
"\nSources: .aiw/checkpoints/usage-sidecar.yml + .aiw/generated/artifacts/traceability.yml (verified = linked evidence present).",
|
|
2157
|
+
};
|
|
2158
|
+
}
|
|
2159
|
+
if (command === "usage-sidecar") {
|
|
2160
|
+
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
2161
|
+
return { exitCode: 1, error: "Run `aiw install` first." };
|
|
2162
|
+
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";
|
|
2163
|
+
const action = args[1];
|
|
2164
|
+
const sidecarPath = join(aiw, USAGE_SIDECAR_PATH);
|
|
2165
|
+
if (action === "report") {
|
|
2166
|
+
const entries = fs.exists(sidecarPath) ? parseUsageSidecar(fs.read(sidecarPath)) : [];
|
|
2167
|
+
return {
|
|
2168
|
+
exitCode: 0,
|
|
2169
|
+
output: serializeUsageSidecarSummary(summarizeUsageSidecar(entries)) +
|
|
2170
|
+
(entries.length
|
|
2171
|
+
? `\nLedger: .aiw/${USAGE_SIDECAR_PATH} (${entries.length} entries, sequence-ordered, no timestamps)`
|
|
2172
|
+
: ""),
|
|
2173
|
+
};
|
|
2174
|
+
}
|
|
2175
|
+
if (action === "record") {
|
|
2176
|
+
const recordArgs = args.slice(2).filter((arg) => arg !== "--estimated");
|
|
2177
|
+
if (recordArgs.some((arg) => arg !== "--estimated" && !arg.startsWith("--")))
|
|
2178
|
+
return { exitCode: 1, error: usage };
|
|
2179
|
+
const parsed = parseNamedOptions(recordArgs, [
|
|
2180
|
+
"command",
|
|
2181
|
+
"model",
|
|
2182
|
+
"harness",
|
|
2183
|
+
"harness-version",
|
|
2184
|
+
"requirement",
|
|
2185
|
+
"workflow",
|
|
2186
|
+
...USAGE_TIERS,
|
|
2187
|
+
]);
|
|
2188
|
+
if (!parsed?.command)
|
|
2189
|
+
return { exitCode: 1, error: usage };
|
|
2190
|
+
const tierValues = USAGE_TIERS.map((tier) => [tier, parsed[tier]]).filter(([, value]) => value !== undefined);
|
|
2191
|
+
if (!tierValues.length)
|
|
2192
|
+
return {
|
|
2193
|
+
exitCode: 1,
|
|
2194
|
+
error: "At least one token tier is required: --cache-read, --fresh-input, --cache-write, or --output.",
|
|
2195
|
+
};
|
|
2196
|
+
for (const [, value] of tierValues)
|
|
2197
|
+
if (!/^\d+$/.test(value))
|
|
2198
|
+
return { exitCode: 1, error: "Token tiers must be non-negative integers." };
|
|
2199
|
+
const entry = {
|
|
2200
|
+
command: parsed.command,
|
|
2201
|
+
label: (args.includes("--estimated") ? "estimated" : "measured"),
|
|
2202
|
+
tiers: Object.fromEntries(USAGE_TIERS.map((tier) => [tier, Number(parsed[tier] ?? 0)])),
|
|
2203
|
+
...(parsed.model ? { model: parsed.model } : {}),
|
|
2204
|
+
...(parsed.harness ? { harness: parsed.harness } : {}),
|
|
2205
|
+
...(parsed["harness-version"] ? { harnessVersion: parsed["harness-version"] } : {}),
|
|
2206
|
+
...(parsed.requirement ? { requirement: parsed.requirement } : {}),
|
|
2207
|
+
...(parsed.workflow ? { workflow: parsed.workflow } : {}),
|
|
2208
|
+
};
|
|
2209
|
+
let ledger;
|
|
2210
|
+
try {
|
|
2211
|
+
ledger = fs.exists(sidecarPath) ? parseUsageSidecar(fs.read(sidecarPath)) : [];
|
|
2212
|
+
}
|
|
2213
|
+
catch (error) {
|
|
2214
|
+
throw new Error(`Usage sidecar is unreadable: ${error.message}`, {
|
|
2215
|
+
cause: error,
|
|
2216
|
+
});
|
|
2217
|
+
}
|
|
2218
|
+
const next = appendUsageSidecarEntry(ledger, entry);
|
|
2219
|
+
fs.write(sidecarPath, serializeUsageSidecar(next));
|
|
2220
|
+
return {
|
|
2221
|
+
exitCode: 0,
|
|
2222
|
+
output: `Usage recorded (seq ${next[next.length - 1].seq}, ${entry.label}): ${entry.command} — ${USAGE_TIERS.filter((tier) => entry.tiers[tier] > 0)
|
|
2223
|
+
.map((tier) => `${tier} ${entry.tiers[tier].toLocaleString("en-US")}`)
|
|
2224
|
+
.join(", ")}`,
|
|
2225
|
+
};
|
|
2226
|
+
}
|
|
2227
|
+
return { exitCode: 1, error: usage };
|
|
2228
|
+
}
|
|
1817
2229
|
if (command === "token-usage") {
|
|
1818
2230
|
const usagePath = join(aiw, "usage.yml");
|
|
1819
2231
|
const entries = fs.exists(usagePath) ? parseTokenUsage(fs.read(usagePath)) : [];
|
|
@@ -1834,7 +2246,15 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
1834
2246
|
fs.write(usagePath, serializeTokenUsage(next));
|
|
1835
2247
|
return { exitCode: 0, output: `Token usage recorded: ${stage} (${used}/${budget})` };
|
|
1836
2248
|
}
|
|
1837
|
-
|
|
2249
|
+
if (entries.length === 0)
|
|
2250
|
+
return { exitCode: 0, output: "No token usage recorded yet." };
|
|
2251
|
+
const summary = summarizeUsageByModel(entries);
|
|
2252
|
+
return {
|
|
2253
|
+
exitCode: 0,
|
|
2254
|
+
output: `Token usage by model:\n${summary
|
|
2255
|
+
.map((entry) => `- ${entry.model}: ${entry.tokens.toLocaleString("en-US")} tokens (${entry.stages.join(", ")})`)
|
|
2256
|
+
.join("\n")}\n\nRaw records:\n${serializeTokenUsage(entries)}`,
|
|
2257
|
+
};
|
|
1838
2258
|
}
|
|
1839
2259
|
if (command === "context-quality") {
|
|
1840
2260
|
const metrics = measureContextQuality(readContextStore(root, fs));
|
|
@@ -1880,7 +2300,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
1880
2300
|
}
|
|
1881
2301
|
return {
|
|
1882
2302
|
exitCode: 0,
|
|
1883
|
-
output: "aiw install [--target target] | workflow <start|status|complete> [--id=id] | approve <stage> | reject <stage> --reason=text | skip <optional-stage> --reason=text | ui [--port=number] | orchestrate --plan=path [--execute] [--max-parallel=number] | telemetry <status|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]",
|
|
2303
|
+
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]",
|
|
1884
2304
|
};
|
|
1885
2305
|
}
|
|
1886
2306
|
catch (error) {
|
package/docs/README.md
CHANGED
|
@@ -12,6 +12,8 @@ AI Workflow is a portable, spec-driven development system for AI coding agents.
|
|
|
12
12
|
- [Security and trust](security.md)
|
|
13
13
|
- [Quality and testing](quality.md)
|
|
14
14
|
- [Token efficiency](token-efficiency.md)
|
|
15
|
+
- [Token efficiency research](token-efficiency-research.md)
|
|
16
|
+
- [Token efficiency study notes](token-efficiency-study-notes.md)
|
|
15
17
|
- [Project intelligence](project-intelligence.md)
|
|
16
18
|
- [Semantic mapping](semantic-mapping.md)
|
|
17
19
|
- [Decision model evaluation](decision-model-evaluation.md)
|