@multileaf/ai-workflow 0.1.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 +22 -0
- package/LICENSE +21 -0
- package/README.md +32 -0
- package/dist/adapter-contract.d.ts +13 -0
- package/dist/adapter-contract.js +50 -0
- package/dist/adapter.d.ts +15 -0
- package/dist/adapter.js +62 -0
- package/dist/claude-layout.d.ts +9 -0
- package/dist/claude-layout.js +23 -0
- package/dist/cli.d.ts +4 -0
- package/dist/cli.js +30 -0
- package/dist/codex-layout.d.ts +10 -0
- package/dist/codex-layout.js +24 -0
- package/dist/confirmation.d.ts +9 -0
- package/dist/confirmation.js +41 -0
- package/dist/constants.d.ts +3 -0
- package/dist/constants.js +10 -0
- package/dist/context-cache.d.ts +8 -0
- package/dist/context-cache.js +17 -0
- package/dist/context-quality.d.ts +11 -0
- package/dist/context-quality.js +27 -0
- package/dist/context-retrieval.d.ts +3 -0
- package/dist/context-retrieval.js +13 -0
- package/dist/context-store.d.ts +9 -0
- package/dist/context-store.js +18 -0
- package/dist/copilot-layout.d.ts +9 -0
- package/dist/copilot-layout.js +24 -0
- package/dist/cursor-layout.d.ts +9 -0
- package/dist/cursor-layout.js +25 -0
- package/dist/dashboard.d.ts +37 -0
- package/dist/dashboard.js +251 -0
- package/dist/drift.d.ts +2 -0
- package/dist/drift.js +10 -0
- package/dist/evidence.d.ts +20 -0
- package/dist/evidence.js +32 -0
- package/dist/files.d.ts +2 -0
- package/dist/files.js +58 -0
- package/dist/gemini-layout.d.ts +9 -0
- package/dist/gemini-layout.js +23 -0
- package/dist/generator.d.ts +3 -0
- package/dist/generator.js +16 -0
- package/dist/intelligence.d.ts +2 -0
- package/dist/intelligence.js +13 -0
- package/dist/interpreter.d.ts +23 -0
- package/dist/interpreter.js +117 -0
- package/dist/lockfile.d.ts +12 -0
- package/dist/lockfile.js +59 -0
- package/dist/manifest.d.ts +15 -0
- package/dist/manifest.js +35 -0
- package/dist/migration.d.ts +22 -0
- package/dist/migration.js +50 -0
- package/dist/orchestration.d.ts +30 -0
- package/dist/orchestration.js +172 -0
- package/dist/organization-policy.d.ts +14 -0
- package/dist/organization-policy.js +47 -0
- package/dist/package-audit.d.ts +19 -0
- package/dist/package-audit.js +28 -0
- package/dist/package-contract.d.ts +30 -0
- package/dist/package-contract.js +93 -0
- package/dist/package-integrity.d.ts +8 -0
- package/dist/package-integrity.js +15 -0
- package/dist/package-updates.d.ts +11 -0
- package/dist/package-updates.js +32 -0
- package/dist/plugin-sdk.d.ts +20 -0
- package/dist/plugin-sdk.js +243 -0
- package/dist/policy-gate.d.ts +6 -0
- package/dist/policy-gate.js +20 -0
- package/dist/profile.d.ts +23 -0
- package/dist/profile.js +123 -0
- package/dist/providers.d.ts +20 -0
- package/dist/providers.js +67 -0
- package/dist/quality-gates.d.ts +2 -0
- package/dist/quality-gates.js +40 -0
- package/dist/recommendations.d.ts +12 -0
- package/dist/recommendations.js +63 -0
- package/dist/registry.d.ts +11 -0
- package/dist/registry.js +27 -0
- package/dist/scanner.d.ts +9 -0
- package/dist/scanner.js +83 -0
- package/dist/selection.d.ts +3 -0
- package/dist/selection.js +11 -0
- package/dist/self-validation.d.ts +9 -0
- package/dist/self-validation.js +9 -0
- package/dist/services.d.ts +42 -0
- package/dist/services.js +1 -0
- package/dist/team-configuration.d.ts +23 -0
- package/dist/team-configuration.js +104 -0
- package/dist/telemetry.d.ts +20 -0
- package/dist/telemetry.js +95 -0
- package/dist/token-usage.d.ts +9 -0
- package/dist/token-usage.js +38 -0
- package/dist/traceability.d.ts +23 -0
- package/dist/traceability.js +51 -0
- package/dist/types.d.ts +22 -0
- package/dist/types.js +1 -0
- package/dist/universal-layout.d.ts +9 -0
- package/dist/universal-layout.js +21 -0
- package/dist/vercel-skills.d.ts +17 -0
- package/dist/vercel-skills.js +49 -0
- package/dist/workflow.d.ts +4 -0
- package/dist/workflow.js +1130 -0
- package/docs/README.md +20 -0
- package/docs/adapter-model.md +16 -0
- package/docs/architecture.md +66 -0
- package/docs/backlog.md +93 -0
- package/docs/orchestration.md +50 -0
- package/docs/package-model.md +53 -0
- package/docs/plugin-authoring.md +31 -0
- package/docs/product-requirements.md +57 -0
- package/docs/product-vision.md +26 -0
- package/docs/project-intelligence.md +59 -0
- package/docs/quality.md +32 -0
- package/docs/release-backlog.md +7 -0
- package/docs/release-process.md +38 -0
- package/docs/security.md +18 -0
- package/docs/telemetry.md +28 -0
- package/docs/token-efficiency.md +16 -0
- package/package.json +76 -0
- package/resources/README.md +4 -0
- package/resources/agents/adapter-engineer.md +4 -0
- package/resources/agents/documentation-writer.md +4 -0
- package/resources/agents/quality-reviewer.md +4 -0
- package/resources/agents/requirements-analyst.md +4 -0
- package/resources/agents/security-reviewer.md +4 -0
- package/resources/agents/technical-architect.md +4 -0
- package/resources/agents/test-engineer.md +4 -0
- package/resources/agents/typescript-engineer.md +4 -0
- package/resources/hooks/post-change.md +4 -0
- package/resources/hooks/post-implementation.md +4 -0
- package/resources/hooks/pre-commit.md +4 -0
- package/resources/hooks/pre-implementation.md +4 -0
- package/resources/hooks/pre-release.md +4 -0
- package/resources/package.yaml +36 -0
- package/resources/rules/architecture-policy.md +4 -0
- package/resources/rules/completion-validation.md +19 -0
- package/resources/rules/dependency-policy.md +4 -0
- package/resources/rules/documentation-policy.md +4 -0
- package/resources/rules/generated-files.md +4 -0
- package/resources/rules/language-policy.md +4 -0
- package/resources/rules/quality-gates.md +4 -0
- package/resources/rules/security-policy.md +4 -0
- package/resources/rules/self-validation.md +12 -0
- package/resources/rules/tdd-policy.md +4 -0
- package/resources/skills/adapter-development.md +9 -0
- package/resources/skills/brainstorming.md +9 -0
- package/resources/skills/code-review.md +9 -0
- package/resources/skills/documentation-maintenance.md +9 -0
- package/resources/skills/implementation-planning.md +9 -0
- package/resources/skills/project-init.md +9 -0
- package/resources/skills/requirements-specification.md +9 -0
- package/resources/skills/security-review.md +9 -0
- package/resources/skills/skill-authoring.md +9 -0
- package/resources/skills/tdd-development.md +9 -0
- package/resources/skills/technical-design.md +9 -0
- package/resources/skills/verification.md +9 -0
- package/resources/templates/brainstorm.md +16 -0
- package/resources/templates/decision-record.md +12 -0
- package/resources/templates/implementation-plan.md +12 -0
- package/resources/templates/migration-report.md +14 -0
- package/resources/templates/security-review.md +14 -0
- package/resources/templates/specification.md +23 -0
- package/resources/templates/task.md +14 -0
- package/resources/templates/technical-design.md +14 -0
- package/resources/templates/test-plan.md +12 -0
- package/resources/templates/verification-report.md +12 -0
package/dist/workflow.js
ADDED
|
@@ -0,0 +1,1130 @@
|
|
|
1
|
+
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
2
|
+
import { BASE_DIRECTORIES, WORKFLOW_DIRECTORY } from "./constants.js";
|
|
3
|
+
import { detectTarget, generateAiInit, isTarget, renderAiInitPath } from "./adapter.js";
|
|
4
|
+
import { parseProjectProfile, profileProject } from "./profile.js";
|
|
5
|
+
import { serializeOverrides } from "./confirmation.js";
|
|
6
|
+
import { applyOverrides, parseOverrides } from "./confirmation.js";
|
|
7
|
+
import { mergeFacts } from "./intelligence.js";
|
|
8
|
+
import { createInterpretationRequest } from "./interpreter.js";
|
|
9
|
+
import { recommendCapabilities, serializeRecommendations } from "./recommendations.js";
|
|
10
|
+
import { selectRecommendations } from "./selection.js";
|
|
11
|
+
import { createInterface } from "node:readline/promises";
|
|
12
|
+
import { stdin, stdout } from "node:process";
|
|
13
|
+
import { generateProjectResources } from "./generator.js";
|
|
14
|
+
import { detectDrift } from "./drift.js";
|
|
15
|
+
import { parseManifest } from "./manifest.js";
|
|
16
|
+
import { validatePackageContract } from "./package-contract.js";
|
|
17
|
+
import { parseLock, resolvePackage, serializeLock } from "./lockfile.js";
|
|
18
|
+
import { executeVercelSkills, installVercelSkill, nodeCommandExecutor } from "./vercel-skills.js";
|
|
19
|
+
import { scanProject } from "./scanner.js";
|
|
20
|
+
import { assertKnownPermissions, assertPackagePermissions, serializePermissionReview, } from "./package-audit.js";
|
|
21
|
+
import { planPackageUpdate } from "./package-updates.js";
|
|
22
|
+
import { searchRegistry, serializeRegistry } from "./registry.js";
|
|
23
|
+
import { checksumPackage, verifyPackageProvenance } from "./package-integrity.js";
|
|
24
|
+
import { nodePackageSourceLoader, resolveProvider } from "./providers.js";
|
|
25
|
+
import { serializeSelfValidationEvidence, validateTicketId } from "./self-validation.js";
|
|
26
|
+
import { enforceOrganizationPolicy, parseOrganizationPolicy, serializeOrganizationPolicy, } from "./organization-policy.js";
|
|
27
|
+
import { composeContext, CONTEXT_LAYERS, readContextStore, writeContextLayer, } from "./context-store.js";
|
|
28
|
+
import { loadContextDelta, retrieveTaskContext } from "./context-retrieval.js";
|
|
29
|
+
import { createSummaryCache, isSummaryFresh, parseSummaryCache, serializeSummaryCache, } from "./context-cache.js";
|
|
30
|
+
import { parseTokenUsage, recordTokenUsage, serializeTokenUsage } from "./token-usage.js";
|
|
31
|
+
import { measureContextQuality, serializeContextQuality } from "./context-quality.js";
|
|
32
|
+
import { serializeAdapterCapabilities } from "./adapter-contract.js";
|
|
33
|
+
import { buildTraceabilityGraph, serializeTraceabilityGraph } from "./traceability.js";
|
|
34
|
+
import { evaluateQualityGate } from "./quality-gates.js";
|
|
35
|
+
import { parseMigrationSnapshots, planNeutralResourceMigration, readResourceBytes, serializeMigrationPreview, serializeMigrationSnapshots, writeResourceBytes, } from "./migration.js";
|
|
36
|
+
import { parsePrivateRegistries, parseTeamPreset, serializePrivateRegistries, serializeTeamPreset, fetchPrivateRegistryClient, } from "./team-configuration.js";
|
|
37
|
+
import { enforcePolicyGate, gitPolicyTracking } from "./policy-gate.js";
|
|
38
|
+
import { DEFAULT_TELEMETRY_CONFIG, parseTelemetryConfig, serializeTelemetryConfig, } from "./telemetry.js";
|
|
39
|
+
import { createPluginScaffold, validatePluginDirectory, writePluginScaffold, } from "./plugin-sdk.js";
|
|
40
|
+
import { executeOrchestrationPlan, parseOrchestrationPlan, serializeOrchestrationPreview, serializeOrchestrationResults, validateOrchestrationParallelism, } from "./orchestration.js";
|
|
41
|
+
export async function runCommand(args, root, fs, services = {}) {
|
|
42
|
+
const aiw = join(root, WORKFLOW_DIRECTORY);
|
|
43
|
+
try {
|
|
44
|
+
const command = args[0] ?? "help";
|
|
45
|
+
if (command === "ui") {
|
|
46
|
+
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
47
|
+
return { exitCode: 1, error: "Run `aiw install` first." };
|
|
48
|
+
const parsed = parseNamedOptions(args.slice(1), ["port"]);
|
|
49
|
+
if (!parsed || (parsed.port !== undefined && !/^\d+$/.test(parsed.port)))
|
|
50
|
+
return { exitCode: 1, error: "Usage: aiw ui [--port=number]" };
|
|
51
|
+
const port = parsed.port === undefined ? 0 : Number(parsed.port);
|
|
52
|
+
if (!Number.isSafeInteger(port) || port < 0 || port > 65535)
|
|
53
|
+
return { exitCode: 1, error: "Dashboard port must be between 0 and 65535." };
|
|
54
|
+
if (!services.dashboard)
|
|
55
|
+
return { exitCode: 1, error: "Dashboard service is unavailable." };
|
|
56
|
+
const dashboard = await services.dashboard.start(root, port);
|
|
57
|
+
return { exitCode: 0, output: `AI Workflow dashboard: ${dashboard.url}` };
|
|
58
|
+
}
|
|
59
|
+
if (command === "orchestrate") {
|
|
60
|
+
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
61
|
+
return { exitCode: 1, error: "Run `aiw install` first." };
|
|
62
|
+
const executeCount = args.filter((arg) => arg === "--execute").length;
|
|
63
|
+
if (executeCount > 1)
|
|
64
|
+
return {
|
|
65
|
+
exitCode: 1,
|
|
66
|
+
error: "Usage: aiw orchestrate --plan=path [--execute] [--max-parallel=number]",
|
|
67
|
+
};
|
|
68
|
+
const execute = executeCount === 1;
|
|
69
|
+
const named = args.slice(1).filter((arg) => arg !== "--execute");
|
|
70
|
+
const parsed = parseNamedOptions(named, ["plan", "max-parallel"]);
|
|
71
|
+
if (!parsed?.plan)
|
|
72
|
+
return {
|
|
73
|
+
exitCode: 1,
|
|
74
|
+
error: "Usage: aiw orchestrate --plan=path [--execute] [--max-parallel=number]",
|
|
75
|
+
};
|
|
76
|
+
const planPath = safeProjectFile(root, parsed.plan, fs);
|
|
77
|
+
const plan = parseOrchestrationPlan(fs.read(planPath));
|
|
78
|
+
const maxParallel = validateOrchestrationParallelism(parsed["max-parallel"] === undefined ? 2 : Number(parsed["max-parallel"]));
|
|
79
|
+
if (!execute)
|
|
80
|
+
return { exitCode: 0, output: serializeOrchestrationPreview(plan) };
|
|
81
|
+
if (!services.orchestrator)
|
|
82
|
+
return { exitCode: 1, error: "No multi-agent orchestrator is configured." };
|
|
83
|
+
const checkpoint = join(aiw, `checkpoints/orchestration-${plan.id}.yml`);
|
|
84
|
+
const reservation = `${checkpoint}.lock`;
|
|
85
|
+
if (!fs.createExclusive)
|
|
86
|
+
return { exitCode: 1, error: "The filesystem does not support atomic orchestration." };
|
|
87
|
+
if (fs.exists(checkpoint) || !fs.createExclusive(reservation, "schema: 1\nstate: running\n"))
|
|
88
|
+
return {
|
|
89
|
+
exitCode: 1,
|
|
90
|
+
error: `Orchestration checkpoint already exists; use a new plan id: ${checkpoint}`,
|
|
91
|
+
};
|
|
92
|
+
try {
|
|
93
|
+
const results = await executeOrchestrationPlan(plan, services.orchestrator, maxParallel);
|
|
94
|
+
fs.write(checkpoint, serializeOrchestrationResults(plan, results));
|
|
95
|
+
const failed = results.filter(({ status }) => status !== "passed");
|
|
96
|
+
return failed.length
|
|
97
|
+
? {
|
|
98
|
+
exitCode: 1,
|
|
99
|
+
error: `Orchestration finished with ${failed.length} failed or blocked tasks: ${checkpoint}`,
|
|
100
|
+
}
|
|
101
|
+
: { exitCode: 0, output: `Orchestration passed: ${checkpoint}` };
|
|
102
|
+
}
|
|
103
|
+
finally {
|
|
104
|
+
fs.remove?.(reservation);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (command === "install") {
|
|
108
|
+
const flag = args.indexOf("--target");
|
|
109
|
+
const requestedTarget = flag >= 0 ? args[flag + 1] : undefined;
|
|
110
|
+
const target = requestedTarget || detectTarget(root, fs) || "codex";
|
|
111
|
+
if (!isTarget(target))
|
|
112
|
+
throw new Error(`Unsupported target: ${target}`);
|
|
113
|
+
if (fs.exists(join(aiw, "manifest.yml"))) {
|
|
114
|
+
const manifest = fs.read(join(aiw, "manifest.yml"));
|
|
115
|
+
const active = manifest.match(/^\s*active:\s*(.+)$/m)?.[1]?.trim() ?? "unknown";
|
|
116
|
+
return {
|
|
117
|
+
exitCode: 0,
|
|
118
|
+
output: `AI Workflow is already installed for target: ${active}. Existing state preserved.`,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
fs.mkdir(aiw);
|
|
122
|
+
BASE_DIRECTORIES.forEach((dir) => fs.mkdir(join(aiw, dir)));
|
|
123
|
+
fs.mkdir(join(root, ".context/adrs"));
|
|
124
|
+
fs.write(join(root, ".context/adrs/INDEX.md"), "# Architecture Decision Records\n\n");
|
|
125
|
+
fs.write(join(aiw, "manifest.yml"), `schema: 1\nproject:\n name: ${root.split("/").pop()}\ntarget:\n active: ${target}\npolicies:\n artifact_language: en\n`);
|
|
126
|
+
fs.write(join(aiw, "profile.yml"), "schema: 1\nstatus: pending-scan\n");
|
|
127
|
+
fs.write(join(aiw, "lock.yml"), "schema: 1\npackages:\n");
|
|
128
|
+
generateAiInit(root, target, fs);
|
|
129
|
+
return { exitCode: 0, output: `AI Workflow installed for target: ${target}` };
|
|
130
|
+
}
|
|
131
|
+
if (command === "telemetry") {
|
|
132
|
+
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
133
|
+
return { exitCode: 1, error: "Run `aiw install` first." };
|
|
134
|
+
const usage = "Usage: aiw telemetry <status|enable|disable> [--commands=include|exclude] [--outcomes=include|exclude]";
|
|
135
|
+
const action = args[1];
|
|
136
|
+
if (!action)
|
|
137
|
+
return { exitCode: 1, error: usage };
|
|
138
|
+
const path = join(aiw, "telemetry.yml");
|
|
139
|
+
if (action === "status") {
|
|
140
|
+
if (args.length !== 2)
|
|
141
|
+
return { exitCode: 1, error: usage };
|
|
142
|
+
const config = fs.exists(path)
|
|
143
|
+
? parseTelemetryConfig(fs.read(path))
|
|
144
|
+
: DEFAULT_TELEMETRY_CONFIG;
|
|
145
|
+
return {
|
|
146
|
+
exitCode: 0,
|
|
147
|
+
output: `Telemetry is ${config.enabled ? "enabled" : "disabled"}. Command collection: ${config.includeCommand ? "included" : "excluded"}. Outcome collection: ${config.includeOutcome ? "included" : "excluded"}.`,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
if (action === "disable") {
|
|
151
|
+
if (args.length !== 2)
|
|
152
|
+
return { exitCode: 1, error: usage };
|
|
153
|
+
fs.write(path, serializeTelemetryConfig({ ...DEFAULT_TELEMETRY_CONFIG }));
|
|
154
|
+
return { exitCode: 0, output: "Telemetry disabled." };
|
|
155
|
+
}
|
|
156
|
+
if (action === "enable") {
|
|
157
|
+
const options = args.slice(2);
|
|
158
|
+
if (options.some((option) => !/^--(?:commands|outcomes)=(?:include|exclude)$/.test(option)) ||
|
|
159
|
+
new Set(options.map((option) => option.slice(0, option.indexOf("=")))).size !==
|
|
160
|
+
options.length)
|
|
161
|
+
return { exitCode: 1, error: usage };
|
|
162
|
+
const privacyValue = (name) => !options.includes(`--${name}=exclude`);
|
|
163
|
+
fs.write(path, serializeTelemetryConfig({
|
|
164
|
+
schema: 1,
|
|
165
|
+
enabled: true,
|
|
166
|
+
includeCommand: privacyValue("commands"),
|
|
167
|
+
includeOutcome: privacyValue("outcomes"),
|
|
168
|
+
}));
|
|
169
|
+
return { exitCode: 0, output: "Telemetry enabled with explicit privacy preferences." };
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
exitCode: 1,
|
|
173
|
+
error: usage,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
if (command === "plugin") {
|
|
177
|
+
const usage = "Usage: aiw plugin create --directory=path --id=owner/name --version=1.0.0 --description=text | aiw plugin validate --directory=path";
|
|
178
|
+
const action = args[1];
|
|
179
|
+
const options = args.slice(2);
|
|
180
|
+
if (action === "create") {
|
|
181
|
+
const allowed = ["directory", "id", "version", "description"];
|
|
182
|
+
const parsed = parseNamedOptions(options, allowed);
|
|
183
|
+
if (!parsed || allowed.some((name) => !parsed[name]))
|
|
184
|
+
return { exitCode: 1, error: usage };
|
|
185
|
+
const scaffold = createPluginScaffold(root, parsed.directory, {
|
|
186
|
+
id: parsed.id,
|
|
187
|
+
version: parsed.version,
|
|
188
|
+
description: parsed.description,
|
|
189
|
+
});
|
|
190
|
+
writePluginScaffold(fs, scaffold);
|
|
191
|
+
return { exitCode: 0, output: `Plugin scaffold created: ${scaffold.root}` };
|
|
192
|
+
}
|
|
193
|
+
if (action === "validate") {
|
|
194
|
+
const parsed = parseNamedOptions(options, ["directory"]);
|
|
195
|
+
if (!parsed?.directory)
|
|
196
|
+
return { exitCode: 1, error: usage };
|
|
197
|
+
const contract = validatePluginDirectory(fs, root, parsed.directory);
|
|
198
|
+
return {
|
|
199
|
+
exitCode: 0,
|
|
200
|
+
output: `Plugin package is valid: ${contract.id}@${contract.version}`,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
return { exitCode: 1, error: usage };
|
|
204
|
+
}
|
|
205
|
+
if (command === "scan") {
|
|
206
|
+
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
207
|
+
return { exitCode: 1, error: "Run `aiw install` first." };
|
|
208
|
+
const profile = await (services.profiler?.profile(root) ?? profileProject(root));
|
|
209
|
+
if (services.interpreter) {
|
|
210
|
+
const scan = await (services.scanner?.scan(root) ?? scanProject(root));
|
|
211
|
+
const inferred = await services.interpreter.interpret(createInterpretationRequest(profile, scan.files));
|
|
212
|
+
profile.facts = mergeFacts(profile.facts, inferred);
|
|
213
|
+
}
|
|
214
|
+
const overrides = fs.exists(join(aiw, "overrides.yml"))
|
|
215
|
+
? parseOverrides(fs.read(join(aiw, "overrides.yml")))
|
|
216
|
+
: [];
|
|
217
|
+
const rejectedKeys = new Set(overrides
|
|
218
|
+
.filter((override) => override.action === "reject")
|
|
219
|
+
.map((override) => override.key));
|
|
220
|
+
profile.facts = profile.facts.filter((fact) => !rejectedKeys.has(fact.key));
|
|
221
|
+
profile.facts = mergeFacts(profile.facts, applyOverrides(profile.facts, overrides));
|
|
222
|
+
profile.facts = profile.facts.filter((fact) => !rejectedKeys.has(fact.key));
|
|
223
|
+
const evidence = profile.facts
|
|
224
|
+
.map((fact) => ` - key: ${fact.key}\n value: ${fact.value}\n state: ${fact.state}\n method: ${fact.method}\n confidence: ${fact.confidence}${fact.requiresConfirmation === undefined ? "" : `\n requires_confirmation: ${fact.requiresConfirmation}`}\n evidence: [${fact.evidence.map((item) => `"${item.source}"`).join(", ")}]`)
|
|
225
|
+
.join("\n");
|
|
226
|
+
fs.write(join(aiw, "profile.yml"), `schema: 1\nstatus: scanned\nfacts:\n${evidence}\nruntime:\n languages: [${profile.runtime.languages.join(", ")}]\nframeworks: [${profile.frameworks.join(", ")}]\npackage_manager: ${profile.packageManager}\nquality:\n linter: ${profile.quality.linter?.name ?? "unknown"}\n linter_command: ${profile.quality.linter?.command ?? "unknown"}\n formatter: ${profile.quality.formatter?.name ?? "unknown"}\n formatter_command: ${profile.quality.formatter?.command ?? "unknown"}\n typecheck: ${profile.quality.typecheck?.name ?? "unknown"}\n typecheck_command: ${profile.quality.typecheck?.command ?? "unknown"}\ntesting: ${profile.testing?.name ?? "unknown"}\ntesting_command: ${profile.testing?.command ?? "unknown"}\nci: [${profile.ci.join(", ")}]\nworkspaces: [${profile.workspaces.join(", ")}]\npolicies:\n artifact_language: en\n`);
|
|
227
|
+
return { exitCode: 0, output: "Project profile generated." };
|
|
228
|
+
}
|
|
229
|
+
if (command === "target") {
|
|
230
|
+
const target = args[1];
|
|
231
|
+
if (!target)
|
|
232
|
+
return { exitCode: 1, error: "Usage: aiw target <target>" };
|
|
233
|
+
if (!isTarget(target))
|
|
234
|
+
return { exitCode: 1, error: `Unsupported target: ${target}` };
|
|
235
|
+
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
236
|
+
return { exitCode: 1, error: "Run `aiw install` first." };
|
|
237
|
+
const dryRun = args.includes("--dry-run");
|
|
238
|
+
const targetPath = join(root, renderAiInitPath(target));
|
|
239
|
+
const neutralAiInit = join(aiw, "resources/skills/ai-init.md");
|
|
240
|
+
const manifestPath = join(aiw, "manifest.yml");
|
|
241
|
+
const previousTarget = fs
|
|
242
|
+
.read(manifestPath)
|
|
243
|
+
.match(/^\s*active:\s*(.+)$/m)?.[1]
|
|
244
|
+
?.trim() ?? "universal";
|
|
245
|
+
const previousTargetPath = isTarget(previousTarget)
|
|
246
|
+
? join(root, renderAiInitPath(previousTarget))
|
|
247
|
+
: undefined;
|
|
248
|
+
const universalConflict = target === "universal" &&
|
|
249
|
+
previousTargetPath !== undefined &&
|
|
250
|
+
fs.exists(previousTargetPath) &&
|
|
251
|
+
fs.exists(neutralAiInit) &&
|
|
252
|
+
fs.read(previousTargetPath) !== fs.read(neutralAiInit);
|
|
253
|
+
const resourceMigrations = planNeutralResourceMigration(join(aiw, "resources"), fs.exists(join(aiw, "resources")) ? (fs.listFiles?.(join(aiw, "resources")) ?? []) : [], target);
|
|
254
|
+
const resourceConflicts = resourceMigrations.filter(({ source, destination }) => {
|
|
255
|
+
const absoluteDestination = join(root, destination);
|
|
256
|
+
return (source !== absoluteDestination &&
|
|
257
|
+
fs.exists(absoluteDestination) &&
|
|
258
|
+
Buffer.compare(Buffer.from(readResourceBytes(fs, source)), Buffer.from(readResourceBytes(fs, absoluteDestination))) !== 0);
|
|
259
|
+
});
|
|
260
|
+
if (dryRun) {
|
|
261
|
+
const existing = resourceMigrations
|
|
262
|
+
.filter(({ destination }) => fs.exists(join(root, destination)))
|
|
263
|
+
.map(({ destination }) => destination);
|
|
264
|
+
const conflictPaths = resourceConflicts.map(({ destination }) => destination);
|
|
265
|
+
if (universalConflict)
|
|
266
|
+
conflictPaths.push(renderAiInitPath(target));
|
|
267
|
+
return {
|
|
268
|
+
exitCode: 0,
|
|
269
|
+
output: `${serializeMigrationPreview(target, resourceMigrations, existing)}${conflictPaths.map((path) => `conflict: ${path}\n`).join("")}`,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
if (universalConflict ||
|
|
273
|
+
(target !== "universal" &&
|
|
274
|
+
fs.exists(neutralAiInit) &&
|
|
275
|
+
fs.exists(targetPath) &&
|
|
276
|
+
fs.read(neutralAiInit) !== fs.read(targetPath)) ||
|
|
277
|
+
resourceConflicts.length > 0)
|
|
278
|
+
return {
|
|
279
|
+
exitCode: 1,
|
|
280
|
+
error: `Migration conflict at ${resourceConflicts[0] ? join(root, resourceConflicts[0].destination) : targetPath}. Use --dry-run to inspect.`,
|
|
281
|
+
};
|
|
282
|
+
const removePrevious = previousTarget !== "universal" &&
|
|
283
|
+
previousTargetPath !== undefined &&
|
|
284
|
+
previousTargetPath !== targetPath &&
|
|
285
|
+
fs.exists(previousTargetPath);
|
|
286
|
+
if (removePrevious && !fs.remove)
|
|
287
|
+
return { exitCode: 1, error: "Filesystem cannot remove the previous target resource." };
|
|
288
|
+
const destinationExisted = fs.exists(targetPath);
|
|
289
|
+
const resourceSnapshots = resourceMigrations
|
|
290
|
+
.filter(({ source, destination }) => source !== join(root, destination))
|
|
291
|
+
.map(({ destination }) => {
|
|
292
|
+
const path = join(root, destination);
|
|
293
|
+
return {
|
|
294
|
+
path,
|
|
295
|
+
existed: fs.exists(path),
|
|
296
|
+
contentBase64: fs.exists(path)
|
|
297
|
+
? Buffer.from(readResourceBytes(fs, path)).toString("base64")
|
|
298
|
+
: "",
|
|
299
|
+
};
|
|
300
|
+
});
|
|
301
|
+
fs.write(join(aiw, "checkpoints/migration-backup.yml"), `previous_target: ${previousTarget}\nprevious_path: ${previousTargetPath ?? "none"}\nprevious_content_base64: ${previousTargetPath && fs.exists(previousTargetPath) ? Buffer.from(fs.read(previousTargetPath)).toString("base64") : ""}\npath: ${targetPath}\ndestination_existed: ${destinationExisted}\ncontent_base64: ${destinationExisted ? Buffer.from(fs.read(targetPath)).toString("base64") : ""}\nresources_base64: ${serializeMigrationSnapshots(resourceSnapshots)}\n`);
|
|
302
|
+
if (target !== "universal" || !fs.exists(neutralAiInit))
|
|
303
|
+
generateAiInit(root, target, fs);
|
|
304
|
+
if (fs.exists(neutralAiInit))
|
|
305
|
+
fs.write(targetPath, fs.read(neutralAiInit));
|
|
306
|
+
for (const migration of resourceMigrations) {
|
|
307
|
+
const destination = join(root, migration.destination);
|
|
308
|
+
if (migration.source !== destination)
|
|
309
|
+
writeResourceBytes(fs, destination, readResourceBytes(fs, migration.source));
|
|
310
|
+
}
|
|
311
|
+
if (removePrevious && previousTargetPath)
|
|
312
|
+
fs.remove?.(previousTargetPath);
|
|
313
|
+
fs.write(manifestPath, fs.read(manifestPath).replace(/active: .*\n/, `active: ${target}\n`));
|
|
314
|
+
return {
|
|
315
|
+
exitCode: 0,
|
|
316
|
+
output: `Target changed to ${target}; rendered ${resourceMigrations.length} neutral resources.`,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
if (command === "rollback") {
|
|
320
|
+
const backup = join(aiw, "checkpoints/migration-backup.yml");
|
|
321
|
+
if (!fs.exists(backup))
|
|
322
|
+
return { exitCode: 1, error: "No migration backup is available." };
|
|
323
|
+
const content = fs.read(backup);
|
|
324
|
+
const path = content.match(/^path: (.+)$/m)?.[1];
|
|
325
|
+
const encoded = content.match(/^content_base64: (.*)$/m)?.[1];
|
|
326
|
+
const previousTarget = content.match(/^previous_target: (.+)$/m)?.[1];
|
|
327
|
+
const previousPath = content.match(/^previous_path: (.+)$/m)?.[1];
|
|
328
|
+
const previousEncoded = content.match(/^previous_content_base64: (.*)$/m)?.[1];
|
|
329
|
+
const destinationExisted = content.match(/^destination_existed: (true|false)$/m)?.[1];
|
|
330
|
+
const resourcesEncoded = content.match(/^resources_base64: (.+)$/m)?.[1];
|
|
331
|
+
if (!path || encoded === undefined)
|
|
332
|
+
return { exitCode: 1, error: "Migration backup is invalid." };
|
|
333
|
+
if (!fs.remove)
|
|
334
|
+
return { exitCode: 1, error: "Filesystem cannot consume the migration checkpoint." };
|
|
335
|
+
if (destinationExisted === "false") {
|
|
336
|
+
if (fs.exists(path) && !fs.remove)
|
|
337
|
+
return { exitCode: 1, error: "Filesystem cannot remove the migrated target resource." };
|
|
338
|
+
if (fs.exists(path))
|
|
339
|
+
fs.remove?.(path);
|
|
340
|
+
}
|
|
341
|
+
else
|
|
342
|
+
fs.write(path, Buffer.from(encoded, "base64").toString("utf8"));
|
|
343
|
+
if (previousPath && previousPath !== "none" && previousEncoded)
|
|
344
|
+
fs.write(previousPath, Buffer.from(previousEncoded, "base64").toString("utf8"));
|
|
345
|
+
for (const snapshot of resourcesEncoded ? parseMigrationSnapshots(resourcesEncoded) : []) {
|
|
346
|
+
if (snapshot.existed)
|
|
347
|
+
writeResourceBytes(fs, snapshot.path, Buffer.from(snapshot.contentBase64, "base64"));
|
|
348
|
+
else {
|
|
349
|
+
if (fs.exists(snapshot.path) && !fs.remove)
|
|
350
|
+
return { exitCode: 1, error: "Filesystem cannot remove migrated resources." };
|
|
351
|
+
if (fs.exists(snapshot.path))
|
|
352
|
+
fs.remove?.(snapshot.path);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
const manifestPath = join(aiw, "manifest.yml");
|
|
356
|
+
if (previousTarget && fs.exists(manifestPath))
|
|
357
|
+
fs.write(manifestPath, fs.read(manifestPath).replace(/active: .*\n/, `active: ${previousTarget}\n`));
|
|
358
|
+
fs.remove(backup);
|
|
359
|
+
return { exitCode: 0, output: `Migration rolled back: ${path}` };
|
|
360
|
+
}
|
|
361
|
+
if (command === "confirm") {
|
|
362
|
+
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
363
|
+
return { exitCode: 1, error: "Run `aiw install` first." };
|
|
364
|
+
const overrides = [];
|
|
365
|
+
const accept = args.indexOf("--accept");
|
|
366
|
+
if (accept >= 0 && args[accept + 1])
|
|
367
|
+
overrides.push({ key: args[accept + 1], action: "accept" });
|
|
368
|
+
const reject = args.indexOf("--reject");
|
|
369
|
+
if (reject >= 0 && args[reject + 1])
|
|
370
|
+
overrides.push({ key: args[reject + 1], action: "reject" });
|
|
371
|
+
const edit = args.indexOf("--edit");
|
|
372
|
+
if (edit >= 0 && args[edit + 1]?.includes("=")) {
|
|
373
|
+
const [key, value] = args[edit + 1].split("=", 2);
|
|
374
|
+
if (key && value)
|
|
375
|
+
overrides.push({ key, action: "edit", value });
|
|
376
|
+
}
|
|
377
|
+
if (overrides.length === 0)
|
|
378
|
+
return {
|
|
379
|
+
exitCode: 1,
|
|
380
|
+
error: "Usage: aiw confirm --accept key | --reject key | --edit key=value",
|
|
381
|
+
};
|
|
382
|
+
fs.write(join(aiw, "overrides.yml"), serializeOverrides(overrides));
|
|
383
|
+
return { exitCode: 0, output: "Fact overrides saved." };
|
|
384
|
+
}
|
|
385
|
+
if (command === "recommend") {
|
|
386
|
+
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
387
|
+
return { exitCode: 1, error: "Run `aiw install` first." };
|
|
388
|
+
const profile = await (services.profiler?.profile(root) ?? profileProject(root));
|
|
389
|
+
const recommendations = services.recommender?.recommend(profile) ?? recommendCapabilities(profile);
|
|
390
|
+
const selectedArg = args.find((arg) => arg.startsWith("--select="))?.split("=", 2)[1];
|
|
391
|
+
if (!selectedArg && !stdin.isTTY) {
|
|
392
|
+
return {
|
|
393
|
+
exitCode: 1,
|
|
394
|
+
error: "Interactive selection requires a TTY. Use --select=id,id in non-interactive environments.",
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
const terminal = createInterface({ input: stdin, output: stdout });
|
|
398
|
+
const selection = await selectRecommendations(recommendations, (question) => terminal.question(question), selectedArg?.split(","));
|
|
399
|
+
terminal.close();
|
|
400
|
+
fs.write(join(aiw, "recommendations.yml"), serializeRecommendations(recommendations, selection));
|
|
401
|
+
return { exitCode: 0, output: `${recommendations.length} recommendations generated.` };
|
|
402
|
+
}
|
|
403
|
+
if (command === "skills") {
|
|
404
|
+
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
405
|
+
return { exitCode: 1, error: "Run `aiw install` first." };
|
|
406
|
+
const action = args[1];
|
|
407
|
+
const executor = services.externalSkills ?? nodeCommandExecutor;
|
|
408
|
+
const approvedPermissions = parseApprovedPermissions(args);
|
|
409
|
+
assertKnownPermissions(approvedPermissions);
|
|
410
|
+
if (action === "search") {
|
|
411
|
+
const query = args.find((arg) => arg.startsWith("--query="))?.slice(8) ?? "";
|
|
412
|
+
return {
|
|
413
|
+
exitCode: 0,
|
|
414
|
+
output: await executeVercelSkills(executor, { command: "find", skill: query }),
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
const source = args.find((arg) => arg.startsWith("--source="))?.slice(9);
|
|
418
|
+
const skill = args.find((arg) => arg.startsWith("--skill="))?.slice(8);
|
|
419
|
+
if (action === "inspect") {
|
|
420
|
+
if (!source)
|
|
421
|
+
return { exitCode: 1, error: "Usage: aiw skills inspect --source=owner/repository" };
|
|
422
|
+
return {
|
|
423
|
+
exitCode: 0,
|
|
424
|
+
output: await executeVercelSkills(executor, { command: "inspect", source }),
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
if (action === "install") {
|
|
428
|
+
if (!source || !skill)
|
|
429
|
+
return {
|
|
430
|
+
exitCode: 1,
|
|
431
|
+
error: "Usage: aiw skills install --source=owner/repository --skill=name --allow=network:external",
|
|
432
|
+
};
|
|
433
|
+
if (!approvedPermissions.includes("network:external"))
|
|
434
|
+
return { exitCode: 1, error: "Package permissions require approval: network:external" };
|
|
435
|
+
enforceConfiguredOrganizationPolicy(fs, aiw, {
|
|
436
|
+
provider: "vercel-skills",
|
|
437
|
+
source,
|
|
438
|
+
permissions: ["network:external"],
|
|
439
|
+
});
|
|
440
|
+
const target = parseManifest(fs.read(join(aiw, "manifest.yml"))).target.active;
|
|
441
|
+
const output = await installVercelSkill(executor, source, skill, target);
|
|
442
|
+
writeVercelSkillLock(fs, root, aiw, source, skill);
|
|
443
|
+
return { exitCode: 0, output };
|
|
444
|
+
}
|
|
445
|
+
if (action === "check" || action === "update") {
|
|
446
|
+
if (!approvedPermissions.includes("network:external"))
|
|
447
|
+
return { exitCode: 1, error: "Package permissions require approval: network:external" };
|
|
448
|
+
const lockPath = join(aiw, "lock.yml");
|
|
449
|
+
if (fs.exists(lockPath)) {
|
|
450
|
+
parseLock(fs.read(lockPath))
|
|
451
|
+
.filter(({ provider }) => provider === "vercel-skills")
|
|
452
|
+
.forEach((locked) => enforceConfiguredOrganizationPolicy(fs, aiw, {
|
|
453
|
+
...locked,
|
|
454
|
+
permissions: [...new Set([...(locked.permissions ?? []), "network:external"])],
|
|
455
|
+
}));
|
|
456
|
+
}
|
|
457
|
+
const output = await executeVercelSkills(executor, { command: action });
|
|
458
|
+
if (action === "update")
|
|
459
|
+
refreshVercelSkillLocks(fs, root, aiw);
|
|
460
|
+
return { exitCode: 0, output };
|
|
461
|
+
}
|
|
462
|
+
return {
|
|
463
|
+
exitCode: 1,
|
|
464
|
+
error: "Usage: aiw skills <search|inspect|install|check|update> [options]",
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
if (command === "generate" || command === "sync") {
|
|
468
|
+
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
469
|
+
return { exitCode: 1, error: "Run `aiw install` first." };
|
|
470
|
+
const profilePath = join(aiw, "profile.yml");
|
|
471
|
+
const persistedProfile = fs.exists(profilePath) ? fs.read(profilePath) : "";
|
|
472
|
+
const profile = /^status:\s*scanned$/m.test(persistedProfile)
|
|
473
|
+
? parseProjectProfile(persistedProfile)
|
|
474
|
+
: await profileProject(root);
|
|
475
|
+
const selectedArg = args.find((arg) => arg.startsWith("--select="))?.split("=", 2)[1];
|
|
476
|
+
const selected = selectedArg?.split(",") ?? (command === "sync" ? readSelectedRecommendations(fs, aiw) : []);
|
|
477
|
+
if (selected.includes("react-best-practices")) {
|
|
478
|
+
const approvedPermissions = parseApprovedPermissions(args);
|
|
479
|
+
if (!approvedPermissions.includes("network:external"))
|
|
480
|
+
return { exitCode: 1, error: "Package permissions require approval: network:external" };
|
|
481
|
+
enforceConfiguredOrganizationPolicy(fs, aiw, {
|
|
482
|
+
provider: "vercel-skills",
|
|
483
|
+
source: "vercel-labs/agent-skills",
|
|
484
|
+
permissions: ["network:external"],
|
|
485
|
+
});
|
|
486
|
+
const target = fs
|
|
487
|
+
.read(join(aiw, "manifest.yml"))
|
|
488
|
+
.match(/^\s*active:\s*(.+)$/m)?.[1]
|
|
489
|
+
?.trim() ?? "universal";
|
|
490
|
+
await installVercelSkill(services.externalSkills ?? nodeCommandExecutor, "vercel-labs/agent-skills", "vercel-react-best-practices", target);
|
|
491
|
+
writeVercelSkillLock(fs, root, aiw, "vercel-labs/agent-skills", "vercel-react-best-practices");
|
|
492
|
+
}
|
|
493
|
+
const conflicts = [];
|
|
494
|
+
generateProjectResources(profile, selected, (path, content) => {
|
|
495
|
+
const destination = join(aiw, path);
|
|
496
|
+
if (fs.exists(destination)) {
|
|
497
|
+
if (detectDrift(content, fs.read(destination)))
|
|
498
|
+
conflicts.push(path);
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
fs.write(destination, content);
|
|
502
|
+
});
|
|
503
|
+
return {
|
|
504
|
+
exitCode: conflicts.length ? 1 : 0,
|
|
505
|
+
output: conflicts.length
|
|
506
|
+
? `Generated resources have manual changes: ${conflicts.join(", ")}`
|
|
507
|
+
: "Project-specific resources generated.",
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
if (command === "status") {
|
|
511
|
+
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
512
|
+
return { exitCode: 0, output: "AI Workflow is not installed." };
|
|
513
|
+
try {
|
|
514
|
+
const manifest = fs.read(join(aiw, "manifest.yml"));
|
|
515
|
+
parseManifest(manifest);
|
|
516
|
+
return { exitCode: 0, output: manifest };
|
|
517
|
+
}
|
|
518
|
+
catch (error) {
|
|
519
|
+
return { exitCode: 1, error: error instanceof Error ? error.message : String(error) };
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
if (command === "doctor") {
|
|
523
|
+
const required = ["manifest.yml", "profile.yml", "lock.yml"];
|
|
524
|
+
const missing = required.filter((file) => !fs.exists(join(aiw, file)));
|
|
525
|
+
if (missing.length)
|
|
526
|
+
return { exitCode: 1, error: `Missing AI Workflow files: ${missing.join(", ")}` };
|
|
527
|
+
try {
|
|
528
|
+
parseManifest(fs.read(join(aiw, "manifest.yml")));
|
|
529
|
+
if (fs.exists(join(aiw, "team-preset.yml")))
|
|
530
|
+
parseTeamPreset(fs.read(join(aiw, "team-preset.yml")));
|
|
531
|
+
if (fs.exists(join(aiw, "registries.yml")))
|
|
532
|
+
parsePrivateRegistries(fs.read(join(aiw, "registries.yml")));
|
|
533
|
+
if (fs.exists(join(aiw, "telemetry.yml")))
|
|
534
|
+
parseTelemetryConfig(fs.read(join(aiw, "telemetry.yml")));
|
|
535
|
+
}
|
|
536
|
+
catch (error) {
|
|
537
|
+
return { exitCode: 1, error: error instanceof Error ? error.message : String(error) };
|
|
538
|
+
}
|
|
539
|
+
return { exitCode: 0, output: "AI Workflow health check passed." };
|
|
540
|
+
}
|
|
541
|
+
if (command === "repair") {
|
|
542
|
+
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
543
|
+
return { exitCode: 1, error: "AI Workflow is not installed." };
|
|
544
|
+
BASE_DIRECTORIES.forEach((dir) => fs.mkdir(join(aiw, dir)));
|
|
545
|
+
if (!fs.exists(join(root, ".context/adrs/INDEX.md")))
|
|
546
|
+
fs.write(join(root, ".context/adrs/INDEX.md"), "# Architecture Decision Records\n\n");
|
|
547
|
+
return { exitCode: 0, output: "AI Workflow state repaired." };
|
|
548
|
+
}
|
|
549
|
+
if (command === "uninstall") {
|
|
550
|
+
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
551
|
+
return { exitCode: 1, error: "AI Workflow is not installed." };
|
|
552
|
+
const files = [
|
|
553
|
+
"manifest.yml",
|
|
554
|
+
"profile.yml",
|
|
555
|
+
"lock.yml",
|
|
556
|
+
"overrides.yml",
|
|
557
|
+
"recommendations.yml",
|
|
558
|
+
"team-preset.yml",
|
|
559
|
+
"registries.yml",
|
|
560
|
+
"telemetry.yml",
|
|
561
|
+
];
|
|
562
|
+
const dryRun = args.includes("--dry-run");
|
|
563
|
+
if (!dryRun && !fs.remove)
|
|
564
|
+
return { exitCode: 1, error: "Filesystem cannot remove AIW-owned files." };
|
|
565
|
+
if (!dryRun)
|
|
566
|
+
files
|
|
567
|
+
.filter((file) => fs.exists(join(aiw, file)))
|
|
568
|
+
.forEach((file) => fs.remove?.(join(aiw, file)));
|
|
569
|
+
return {
|
|
570
|
+
exitCode: 0,
|
|
571
|
+
output: dryRun ? `Would remove: ${files.join(", ")}` : "AI Workflow-owned files removed.",
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
if (command === "brainstorm") {
|
|
575
|
+
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
576
|
+
return { exitCode: 1, error: "Run `aiw install` first." };
|
|
577
|
+
const title = args.find((arg) => arg.startsWith("--title="))?.slice(8) || "Untitled Brainstorm";
|
|
578
|
+
const path = join(aiw, "generated/specs/brainstorm.md");
|
|
579
|
+
fs.write(path, `# ${title}\n\n## Goal\n\n## Users\n\n## Facts\n\n## Assumptions\n\n## Hypotheses\n\n## Constraints\n\n## Risks\n\n## Non-goals\n\n## Open Questions\n\n`);
|
|
580
|
+
return { exitCode: 0, output: `Brainstorm artifact created: ${path}` };
|
|
581
|
+
}
|
|
582
|
+
if (command === "spec") {
|
|
583
|
+
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
584
|
+
return { exitCode: 1, error: "Run `aiw install` first." };
|
|
585
|
+
const title = args.find((arg) => arg.startsWith("--title="))?.slice(8) || "Untitled Specification";
|
|
586
|
+
const path = join(aiw, "generated/specs/specification.md");
|
|
587
|
+
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`);
|
|
588
|
+
return { exitCode: 0, output: `Specification artifact created: ${path}` };
|
|
589
|
+
}
|
|
590
|
+
if (command === "adr") {
|
|
591
|
+
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
592
|
+
return { exitCode: 1, error: "Run `aiw install` first." };
|
|
593
|
+
const id = args.find((arg) => arg.startsWith("--id="))?.slice(5) || "001";
|
|
594
|
+
const title = args.find((arg) => arg.startsWith("--title="))?.slice(8) || "Untitled Decision";
|
|
595
|
+
const path = join(root, ".context/adrs", `ADR-${id}-${title.toLowerCase().replaceAll(" ", "-")}.md`);
|
|
596
|
+
fs.write(path, `# ADR-${id}: ${title}\n\n- **Status:** proposed\n- **Related artifacts:**\n - .aiw/generated/specs/\n\n## Context\n\n## Alternatives\n\n1. **Alternative A** — Description and trade-offs.\n2. **Alternative B** — Description and trade-offs.\n\n## Decision\n\n## Rationale\n\n## Consequences\n\n`);
|
|
597
|
+
return { exitCode: 0, output: `Architecture decision record created: ${path}` };
|
|
598
|
+
}
|
|
599
|
+
if (command === "plan") {
|
|
600
|
+
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
601
|
+
return { exitCode: 1, error: "Run `aiw install` first." };
|
|
602
|
+
const title = args.find((arg) => arg.startsWith("--title="))?.slice(8) || "Untitled Implementation Plan";
|
|
603
|
+
const path = join(aiw, "generated/plans/implementation-plan.md");
|
|
604
|
+
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`);
|
|
605
|
+
return { exitCode: 0, output: `Implementation plan created: ${path}` };
|
|
606
|
+
}
|
|
607
|
+
if (command === "verify") {
|
|
608
|
+
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
609
|
+
return { exitCode: 1, error: "Run `aiw install` first." };
|
|
610
|
+
const specPath = join(aiw, "generated/specs/specification.md");
|
|
611
|
+
const planPath = join(aiw, "generated/plans/implementation-plan.md");
|
|
612
|
+
const issues = [];
|
|
613
|
+
let specification = "";
|
|
614
|
+
if (!fs.exists(specPath))
|
|
615
|
+
issues.push("Specification artifact is missing.");
|
|
616
|
+
else {
|
|
617
|
+
specification = fs.read(specPath);
|
|
618
|
+
if (!specification.includes("Acceptance criteria"))
|
|
619
|
+
issues.push("Requirements have no acceptance criteria.");
|
|
620
|
+
}
|
|
621
|
+
if (!fs.exists(planPath))
|
|
622
|
+
issues.push("Implementation plan is missing.");
|
|
623
|
+
else {
|
|
624
|
+
const plan = fs.read(planPath);
|
|
625
|
+
if (!plan.includes("Validation"))
|
|
626
|
+
issues.push("Requirements have no linked validation commands.");
|
|
627
|
+
const requirements = [...new Set([...specification.matchAll(/REQ-\d+/g)].map(String))];
|
|
628
|
+
const uncovered = requirements.filter((requirement) => !plan.includes(requirement));
|
|
629
|
+
if (uncovered.length > 0)
|
|
630
|
+
issues.push(`Untested requirements: ${uncovered.join(", ")}.`);
|
|
631
|
+
}
|
|
632
|
+
return issues.length
|
|
633
|
+
? { exitCode: 1, error: `Verification failed: ${issues.join(" ")}` }
|
|
634
|
+
: {
|
|
635
|
+
exitCode: 0,
|
|
636
|
+
output: "Verification passed: requirements have acceptance criteria and validation coverage.",
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
if (command === "trace") {
|
|
640
|
+
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
641
|
+
return { exitCode: 1, error: "Run `aiw install` first." };
|
|
642
|
+
const specPath = join(aiw, "generated/specs/specification.md");
|
|
643
|
+
const planPath = join(aiw, "generated/plans/implementation-plan.md");
|
|
644
|
+
if (!fs.exists(specPath) || !fs.exists(planPath))
|
|
645
|
+
return {
|
|
646
|
+
exitCode: 1,
|
|
647
|
+
error: "Traceability requires a specification and implementation plan.",
|
|
648
|
+
};
|
|
649
|
+
const adrDirectory = join(root, ".context/adrs");
|
|
650
|
+
const decisions = (fs.list?.(adrDirectory) ?? [])
|
|
651
|
+
.filter((file) => /^ADR-.+\.md$/.test(file))
|
|
652
|
+
.map((file) => ({
|
|
653
|
+
id: file.replace(/\.md$/, "").split("-").slice(0, 2).join("-"),
|
|
654
|
+
content: fs.read(join(adrDirectory, file)),
|
|
655
|
+
}));
|
|
656
|
+
const graph = buildTraceabilityGraph({
|
|
657
|
+
specification: fs.read(specPath),
|
|
658
|
+
plan: fs.read(planPath),
|
|
659
|
+
decisions,
|
|
660
|
+
});
|
|
661
|
+
const requirement = args.find((arg) => arg.startsWith("--requirement="))?.slice(14);
|
|
662
|
+
const serialized = serializeTraceabilityGraph(graph, requirement);
|
|
663
|
+
if (requirement)
|
|
664
|
+
return { exitCode: 0, output: serialized };
|
|
665
|
+
const path = join(aiw, "generated/artifacts/traceability.yml");
|
|
666
|
+
fs.write(path, serialized);
|
|
667
|
+
return { exitCode: 0, output: `Traceability graph created: ${path}` };
|
|
668
|
+
}
|
|
669
|
+
if (command === "gate") {
|
|
670
|
+
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
671
|
+
return { exitCode: 1, error: "Run `aiw install` first." };
|
|
672
|
+
const stage = args[1];
|
|
673
|
+
const requirements = {
|
|
674
|
+
specification: "generated/specs/brainstorm.md",
|
|
675
|
+
plan: "generated/specs/specification.md",
|
|
676
|
+
verification: "generated/plans/implementation-plan.md",
|
|
677
|
+
};
|
|
678
|
+
if (!stage || !(stage in requirements))
|
|
679
|
+
return { exitCode: 1, error: "Usage: aiw gate <specification|plan|verification>" };
|
|
680
|
+
const gateStage = stage;
|
|
681
|
+
const artifact = requirements[gateStage];
|
|
682
|
+
if (!fs.exists(join(aiw, artifact)))
|
|
683
|
+
return { exitCode: 1, error: `Quality gate blocked: missing ${artifact}` };
|
|
684
|
+
const issues = evaluateQualityGate(gateStage, fs.read(join(aiw, artifact)));
|
|
685
|
+
return issues.length
|
|
686
|
+
? { exitCode: 1, error: `Quality gate blocked: ${issues.join(" ")}` }
|
|
687
|
+
: { exitCode: 0, output: `Quality gate passed for ${stage}.` };
|
|
688
|
+
}
|
|
689
|
+
if (command === "validate") {
|
|
690
|
+
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
691
|
+
return { exitCode: 1, error: "AI Workflow is not installed." };
|
|
692
|
+
parseManifest(fs.read(join(aiw, "manifest.yml")));
|
|
693
|
+
const packageArg = args.find((arg) => arg.startsWith("--package="));
|
|
694
|
+
if (packageArg)
|
|
695
|
+
validatePackageContract(fs.read(join(root, packageArg.slice(10))), (path) => !fs.exists(join(root, "resources")) ||
|
|
696
|
+
fs.exists(join(root, path)) ||
|
|
697
|
+
fs.exists(join(root, "resources", path)));
|
|
698
|
+
return {
|
|
699
|
+
exitCode: 0,
|
|
700
|
+
output: "Configuration is valid.",
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
if (command === "resolve") {
|
|
704
|
+
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
705
|
+
return { exitCode: 1, error: "Run `aiw install` first." };
|
|
706
|
+
const packageArg = args.find((arg) => arg.startsWith("--package="));
|
|
707
|
+
const sourceArg = args.find((arg) => arg.startsWith("--source="));
|
|
708
|
+
if (!packageArg && !sourceArg)
|
|
709
|
+
return { exitCode: 1, error: "Usage: aiw resolve --package=path | --source=source" };
|
|
710
|
+
if (sourceArg) {
|
|
711
|
+
const source = resolveProvider(sourceArg.slice(9), root);
|
|
712
|
+
enforceConfiguredOrganizationPolicy(fs, aiw, { ...source, permissions: [] });
|
|
713
|
+
}
|
|
714
|
+
const loaded = sourceArg
|
|
715
|
+
? (services.packageSources ?? nodePackageSourceLoader).load(sourceArg.slice(9), root)
|
|
716
|
+
: undefined;
|
|
717
|
+
try {
|
|
718
|
+
const pkg = validatePackageContract(loaded?.manifest ?? fs.read(join(root, packageArg.slice(10))), loaded ? (path) => fs.exists(join(loaded.root, path)) : undefined);
|
|
719
|
+
const resolved = loaded
|
|
720
|
+
? {
|
|
721
|
+
...pkg,
|
|
722
|
+
provider: loaded.provider,
|
|
723
|
+
source: loaded.source,
|
|
724
|
+
provenance: { ...pkg.provenance, source: loaded.source },
|
|
725
|
+
}
|
|
726
|
+
: pkg;
|
|
727
|
+
enforceConfiguredOrganizationPolicy(fs, aiw, resolved);
|
|
728
|
+
assertPackagePermissions(resolved, parseApprovedPermissions(args));
|
|
729
|
+
fs.write(join(aiw, "lock.yml"), serializeLock([resolvePackage(resolved)]));
|
|
730
|
+
return { exitCode: 0, output: `Package resolved: ${resolved.id}@${resolved.version}` };
|
|
731
|
+
}
|
|
732
|
+
finally {
|
|
733
|
+
loaded?.release();
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
if (command === "audit-package") {
|
|
737
|
+
const packageArg = args.find((arg) => arg.startsWith("--package="));
|
|
738
|
+
if (!packageArg)
|
|
739
|
+
return { exitCode: 1, error: "Usage: aiw audit-package --package=path" };
|
|
740
|
+
const pkg = validatePackageContract(fs.read(join(root, packageArg.slice(10))));
|
|
741
|
+
return { exitCode: 0, output: serializePermissionReview(pkg) };
|
|
742
|
+
}
|
|
743
|
+
if (command === "organization-policy") {
|
|
744
|
+
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
745
|
+
return { exitCode: 1, error: "Run `aiw install` first." };
|
|
746
|
+
const fileArg = args.find((arg) => arg.startsWith("--file="));
|
|
747
|
+
if (!fileArg)
|
|
748
|
+
return { exitCode: 1, error: "Usage: aiw organization-policy --file=path [--replace]" };
|
|
749
|
+
const destination = join(aiw, "organization.yml");
|
|
750
|
+
if (fs.exists(destination) && !args.includes("--replace"))
|
|
751
|
+
return {
|
|
752
|
+
exitCode: 1,
|
|
753
|
+
error: "Organization policy already exists. Use --replace to update it explicitly.",
|
|
754
|
+
};
|
|
755
|
+
const policy = parseOrganizationPolicy(fs.read(join(root, fileArg.slice(7))));
|
|
756
|
+
fs.write(destination, serializeOrganizationPolicy(policy));
|
|
757
|
+
return { exitCode: 0, output: `Organization policy configured: ${policy.name}` };
|
|
758
|
+
}
|
|
759
|
+
if (command === "policy-check") {
|
|
760
|
+
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
761
|
+
return { exitCode: 1, error: "Run `aiw install` first." };
|
|
762
|
+
const policyPath = join(aiw, "organization.yml");
|
|
763
|
+
if (!fs.exists(policyPath))
|
|
764
|
+
return { exitCode: 1, error: "Organization policy is required for the CI policy gate." };
|
|
765
|
+
if (!(services.policyTracking ?? gitPolicyTracking).isTracked(root, policyPath))
|
|
766
|
+
return { exitCode: 1, error: "Organization policy must be tracked by Git." };
|
|
767
|
+
const packageArgs = args.filter((arg) => arg.startsWith("--package="));
|
|
768
|
+
if (!packageArgs.length)
|
|
769
|
+
return { exitCode: 1, error: "Usage: aiw policy-check --package=path [--package=path]" };
|
|
770
|
+
const packages = packageArgs.map((argument) => validatePackageContract(fs.read(join(root, argument.slice(10)))));
|
|
771
|
+
const subjects = packages.map((pkg) => ({
|
|
772
|
+
provider: pkg.provider,
|
|
773
|
+
source: pkg.source,
|
|
774
|
+
permissions: pkg.permissions,
|
|
775
|
+
}));
|
|
776
|
+
const lock = fs.read(join(aiw, "lock.yml"));
|
|
777
|
+
subjects.push(...parseLock(lock).map((locked) => ({
|
|
778
|
+
provider: locked.provider,
|
|
779
|
+
source: locked.source,
|
|
780
|
+
permissions: locked.permissions ?? [],
|
|
781
|
+
})));
|
|
782
|
+
const registriesPath = join(aiw, "registries.yml");
|
|
783
|
+
if (fs.exists(registriesPath))
|
|
784
|
+
subjects.push(...parsePrivateRegistries(fs.read(registriesPath)).registries.map((registry) => ({
|
|
785
|
+
provider: "private-registry",
|
|
786
|
+
source: registry.url,
|
|
787
|
+
permissions: ["network:external"],
|
|
788
|
+
})));
|
|
789
|
+
const policy = parseOrganizationPolicy(fs.read(policyPath));
|
|
790
|
+
subjects.forEach((subject) => assertKnownPermissions(subject.permissions));
|
|
791
|
+
enforcePolicyGate(policy, subjects);
|
|
792
|
+
return {
|
|
793
|
+
exitCode: 0,
|
|
794
|
+
output: `Organization policy gate passed: ${policy.name} (${subjects.length} subjects).`,
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
if (command === "preset") {
|
|
798
|
+
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
799
|
+
return { exitCode: 1, error: "Run `aiw install` first." };
|
|
800
|
+
const fileArg = args.find((arg) => arg.startsWith("--file="));
|
|
801
|
+
if (!fileArg)
|
|
802
|
+
return { exitCode: 1, error: "Usage: aiw preset --file=path [--replace]" };
|
|
803
|
+
const destination = join(aiw, "team-preset.yml");
|
|
804
|
+
if (fs.exists(destination) && !args.includes("--replace"))
|
|
805
|
+
return {
|
|
806
|
+
exitCode: 1,
|
|
807
|
+
error: "Team preset already exists. Use --replace to update it explicitly.",
|
|
808
|
+
};
|
|
809
|
+
const preset = parseTeamPreset(fs.read(join(root, fileArg.slice(7))));
|
|
810
|
+
fs.write(destination, serializeTeamPreset(preset));
|
|
811
|
+
return { exitCode: 0, output: `Team preset configured: ${preset.name}` };
|
|
812
|
+
}
|
|
813
|
+
if (command === "update") {
|
|
814
|
+
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
815
|
+
return { exitCode: 1, error: "Run `aiw install` first." };
|
|
816
|
+
const packageArg = args.find((arg) => arg.startsWith("--package="));
|
|
817
|
+
if (!packageArg)
|
|
818
|
+
return { exitCode: 1, error: "Usage: aiw update --package=path --target=target" };
|
|
819
|
+
const pkg = validatePackageContract(fs.read(join(root, packageArg.slice(10))));
|
|
820
|
+
enforceConfiguredOrganizationPolicy(fs, aiw, pkg);
|
|
821
|
+
assertPackagePermissions(pkg, parseApprovedPermissions(args));
|
|
822
|
+
const target = args.find((arg) => arg.startsWith("--target="))?.slice(9) ?? "universal";
|
|
823
|
+
if (!isTarget(target))
|
|
824
|
+
return { exitCode: 1, error: `Unsupported target: ${target}` };
|
|
825
|
+
const lock = fs.exists(join(aiw, "lock.yml")) ? fs.read(join(aiw, "lock.yml")) : "";
|
|
826
|
+
const lockedPackages = lock ? parseLock(lock) : [];
|
|
827
|
+
const current = lockedPackages.find(({ id }) => id === pkg.id);
|
|
828
|
+
const plan = planPackageUpdate(current ? { id: pkg.id, version: current.version } : undefined, pkg, target, new Map(lockedPackages.filter(({ id }) => id !== pkg.id).map(({ id, version }) => [id, version])));
|
|
829
|
+
if (plan.status !== "update")
|
|
830
|
+
return { exitCode: 1, error: plan.reason ?? `Update blocked: ${plan.status}.` };
|
|
831
|
+
const existingPackages = lockedPackages.filter(({ id }) => id !== pkg.id);
|
|
832
|
+
fs.write(join(aiw, "lock.yml"), serializeLock([...existingPackages, resolvePackage(pkg)]));
|
|
833
|
+
return { exitCode: 0, output: `Package updated: ${pkg.id}@${pkg.version}` };
|
|
834
|
+
}
|
|
835
|
+
if (command === "registry") {
|
|
836
|
+
if (args[1] === "configure") {
|
|
837
|
+
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
838
|
+
return { exitCode: 1, error: "Run `aiw install` first." };
|
|
839
|
+
const fileArg = args.find((arg) => arg.startsWith("--file="));
|
|
840
|
+
if (!fileArg)
|
|
841
|
+
return {
|
|
842
|
+
exitCode: 1,
|
|
843
|
+
error: "Usage: aiw registry configure --file=path [--replace]",
|
|
844
|
+
};
|
|
845
|
+
const destination = join(aiw, "registries.yml");
|
|
846
|
+
if (fs.exists(destination) && !args.includes("--replace"))
|
|
847
|
+
return {
|
|
848
|
+
exitCode: 1,
|
|
849
|
+
error: "Private registry configuration already exists. Use --replace to update it explicitly.",
|
|
850
|
+
};
|
|
851
|
+
const registries = parsePrivateRegistries(fs.read(join(root, fileArg.slice(7))));
|
|
852
|
+
fs.write(destination, serializePrivateRegistries(registries));
|
|
853
|
+
return {
|
|
854
|
+
exitCode: 0,
|
|
855
|
+
output: `Private registries configured: ${registries.registries.map(({ name }) => name).join(", ")}`,
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
const query = args.find((arg) => arg.startsWith("--search="))?.slice(9) ?? "";
|
|
859
|
+
const privateName = args.find((arg) => arg.startsWith("--private="))?.slice(10);
|
|
860
|
+
if (privateName) {
|
|
861
|
+
const configurationPath = join(aiw, "registries.yml");
|
|
862
|
+
if (!fs.exists(configurationPath))
|
|
863
|
+
return { exitCode: 1, error: "Configure private registries first." };
|
|
864
|
+
const configuration = parsePrivateRegistries(fs.read(configurationPath));
|
|
865
|
+
const registry = configuration.registries.find(({ name }) => name === privateName);
|
|
866
|
+
if (!registry)
|
|
867
|
+
return { exitCode: 1, error: "Unknown private registry." };
|
|
868
|
+
enforceConfiguredOrganizationPolicy(fs, aiw, {
|
|
869
|
+
provider: "private-registry",
|
|
870
|
+
source: registry.url,
|
|
871
|
+
permissions: ["network:external"],
|
|
872
|
+
});
|
|
873
|
+
const token = (services.environment ?? process.env)[registry.tokenEnv];
|
|
874
|
+
if (!token)
|
|
875
|
+
return {
|
|
876
|
+
exitCode: 1,
|
|
877
|
+
error: `Private registry credential is missing from environment variable: ${registry.tokenEnv}`,
|
|
878
|
+
};
|
|
879
|
+
let payload;
|
|
880
|
+
try {
|
|
881
|
+
payload = await (services.privateRegistries ?? fetchPrivateRegistryClient).search(registry, query, token);
|
|
882
|
+
}
|
|
883
|
+
catch (error) {
|
|
884
|
+
const message = error instanceof Error ? error.message : "Private registry request failed.";
|
|
885
|
+
throw new Error(message.split(token).join("[REDACTED]"), { cause: error });
|
|
886
|
+
}
|
|
887
|
+
if (containsCredential(payload, token))
|
|
888
|
+
throw new Error("Private registry response contains sensitive credential data.");
|
|
889
|
+
const packages = parsePrivateRegistryPackages(payload, registry.url).sort((a, b) => `${a.id}@${a.version}`.localeCompare(`${b.id}@${b.version}`));
|
|
890
|
+
packages.forEach((pkg) => assertKnownPermissions(pkg.permissions));
|
|
891
|
+
return { exitCode: 0, output: serializeRegistry(packages) };
|
|
892
|
+
}
|
|
893
|
+
return { exitCode: 0, output: serializeRegistry(searchRegistry(query)) };
|
|
894
|
+
}
|
|
895
|
+
if (command === "verify-package") {
|
|
896
|
+
const packageArg = args.find((arg) => arg.startsWith("--package="));
|
|
897
|
+
const checksumArg = args.find((arg) => arg.startsWith("--checksum="));
|
|
898
|
+
if (!packageArg || !checksumArg)
|
|
899
|
+
return { exitCode: 1, error: "Usage: aiw verify-package --package=path --checksum=sha256" };
|
|
900
|
+
const content = fs.read(join(root, packageArg.slice(10)));
|
|
901
|
+
verifyPackageProvenance(content, { checksum: checksumArg.slice(11) });
|
|
902
|
+
return { exitCode: 0, output: `Package provenance verified: ${checksumPackage(content)}` };
|
|
903
|
+
}
|
|
904
|
+
if (command === "context") {
|
|
905
|
+
const layer = args.find((arg) => arg.startsWith("--layer="))?.slice(8);
|
|
906
|
+
const content = args.find((arg) => arg.startsWith("--content="))?.slice(10);
|
|
907
|
+
const store = readContextStore(root, fs);
|
|
908
|
+
if (layer && CONTEXT_LAYERS.includes(layer) && content !== undefined) {
|
|
909
|
+
writeContextLayer(root, fs, layer, content);
|
|
910
|
+
return { exitCode: 0, output: `Context layer written: ${layer}` };
|
|
911
|
+
}
|
|
912
|
+
if (!layer && content === undefined) {
|
|
913
|
+
const delta = args.find((arg) => arg.startsWith("--delta-from="));
|
|
914
|
+
if (delta) {
|
|
915
|
+
const reusableLayers = [store.layers.project, store.layers.task]
|
|
916
|
+
.filter(Boolean)
|
|
917
|
+
.join("\n\n");
|
|
918
|
+
return {
|
|
919
|
+
exitCode: 0,
|
|
920
|
+
output: loadContextDelta(store.layers[delta.slice(13)] ?? "", reusableLayers),
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
const output = args.includes("--task") ? retrieveTaskContext(store) : composeContext(store);
|
|
924
|
+
return { exitCode: 0, output };
|
|
925
|
+
}
|
|
926
|
+
return { exitCode: 1, error: "Usage: aiw context [--layer=project --content=text]" };
|
|
927
|
+
}
|
|
928
|
+
if (command === "context-summary") {
|
|
929
|
+
const summaryArg = args.find((arg) => arg.startsWith("--summary="))?.slice(10);
|
|
930
|
+
if (summaryArg === undefined)
|
|
931
|
+
return { exitCode: 1, error: "Usage: aiw context-summary --summary=text" };
|
|
932
|
+
const context = composeContext(readContextStore(root, fs));
|
|
933
|
+
const cachePath = join(aiw, "context/summary.yml");
|
|
934
|
+
if (fs.exists(cachePath)) {
|
|
935
|
+
const cache = parseSummaryCache(fs.read(cachePath));
|
|
936
|
+
if (isSummaryFresh(context, cache))
|
|
937
|
+
return { exitCode: 0, output: cache.summary };
|
|
938
|
+
}
|
|
939
|
+
fs.write(cachePath, serializeSummaryCache(createSummaryCache(context, summaryArg)));
|
|
940
|
+
return { exitCode: 0, output: summaryArg };
|
|
941
|
+
}
|
|
942
|
+
if (command === "token-usage") {
|
|
943
|
+
const usagePath = join(aiw, "usage.yml");
|
|
944
|
+
const entries = fs.exists(usagePath) ? parseTokenUsage(fs.read(usagePath)) : [];
|
|
945
|
+
const stage = args.find((arg) => arg.startsWith("--stage="))?.slice(8);
|
|
946
|
+
const budget = args.find((arg) => arg.startsWith("--budget="))?.slice(9);
|
|
947
|
+
const used = args.find((arg) => arg.startsWith("--used="))?.slice(7);
|
|
948
|
+
if (stage !== undefined || budget !== undefined || used !== undefined) {
|
|
949
|
+
if (stage === undefined || budget === undefined || used === undefined)
|
|
950
|
+
return {
|
|
951
|
+
exitCode: 1,
|
|
952
|
+
error: "Usage: aiw token-usage --stage=name --budget=number --used=number",
|
|
953
|
+
};
|
|
954
|
+
const next = recordTokenUsage(entries, {
|
|
955
|
+
stage,
|
|
956
|
+
budget: Number(budget),
|
|
957
|
+
used: Number(used),
|
|
958
|
+
});
|
|
959
|
+
fs.write(usagePath, serializeTokenUsage(next));
|
|
960
|
+
return { exitCode: 0, output: `Token usage recorded: ${stage} (${used}/${budget})` };
|
|
961
|
+
}
|
|
962
|
+
return { exitCode: 0, output: serializeTokenUsage(entries) };
|
|
963
|
+
}
|
|
964
|
+
if (command === "context-quality") {
|
|
965
|
+
const metrics = measureContextQuality(readContextStore(root, fs));
|
|
966
|
+
const output = serializeContextQuality(metrics);
|
|
967
|
+
fs.write(join(aiw, "context/quality.yml"), output);
|
|
968
|
+
return { exitCode: 0, output };
|
|
969
|
+
}
|
|
970
|
+
if (command === "self-validate") {
|
|
971
|
+
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
972
|
+
return { exitCode: 1, error: "Run `aiw install` first." };
|
|
973
|
+
const ticket = args.find((arg) => arg.startsWith("--ticket="))?.slice(9);
|
|
974
|
+
if (!ticket)
|
|
975
|
+
return { exitCode: 1, error: "Usage: aiw self-validate --ticket=TYPE-000" };
|
|
976
|
+
validateTicketId(ticket);
|
|
977
|
+
const requiredState = ["manifest.yml", "profile.yml", "lock.yml"];
|
|
978
|
+
const missing = requiredState.filter((path) => !fs.exists(join(aiw, path)));
|
|
979
|
+
if (missing.length)
|
|
980
|
+
return { exitCode: 1, error: `Self-validation state is missing: ${missing.join(", ")}` };
|
|
981
|
+
const result = await (services.selfValidation ?? nodeCommandExecutor).execute([
|
|
982
|
+
"npm",
|
|
983
|
+
"--prefix",
|
|
984
|
+
root,
|
|
985
|
+
"run",
|
|
986
|
+
"check",
|
|
987
|
+
]);
|
|
988
|
+
const generatedRoot = join(aiw, "generated");
|
|
989
|
+
const generatedArtifacts = (fs.listFiles?.(generatedRoot) ?? []).map((path) => `.aiw/generated/${path}`);
|
|
990
|
+
const evidencePath = join(aiw, `checkpoints/self-validation-${ticket.toLowerCase()}.yml`);
|
|
991
|
+
fs.write(evidencePath, serializeSelfValidationEvidence({
|
|
992
|
+
ticket,
|
|
993
|
+
command: "npm run check",
|
|
994
|
+
exitCode: result.exitCode,
|
|
995
|
+
requiredState: requiredState.map((path) => `.aiw/${path}`),
|
|
996
|
+
generatedArtifacts,
|
|
997
|
+
}));
|
|
998
|
+
return result.exitCode === 0
|
|
999
|
+
? { exitCode: 0, output: `Self-validation passed: ${evidencePath}` }
|
|
1000
|
+
: { exitCode: 1, error: `Self-validation failed: ${result.stdout}` };
|
|
1001
|
+
}
|
|
1002
|
+
if (command === "capabilities") {
|
|
1003
|
+
const target = args.find((arg) => arg.startsWith("--target="))?.slice(9) ?? "universal";
|
|
1004
|
+
return { exitCode: 0, output: serializeAdapterCapabilities(target) };
|
|
1005
|
+
}
|
|
1006
|
+
return {
|
|
1007
|
+
exitCode: 0,
|
|
1008
|
+
output: "aiw install [--target target] | ui [--port=number] | orchestrate --plan=path [--execute] [--max-parallel=number] | telemetry <status|enable|disable> [privacy options] | plugin <create|validate> [options] | scan | 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]",
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
catch (error) {
|
|
1012
|
+
return { exitCode: 1, error: error instanceof Error ? error.message : String(error) };
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
function parseNamedOptions(args, allowed) {
|
|
1016
|
+
const entries = args.map((arg) => arg.match(/^--([a-z-]+)=(.+)$/)?.slice(1));
|
|
1017
|
+
if (args.some((arg) => arg.indexOf("=") !== arg.lastIndexOf("=")) ||
|
|
1018
|
+
entries.some((entry) => !entry || !allowed.includes(entry[0])) ||
|
|
1019
|
+
new Set(entries.map((entry) => entry?.[0])).size !== entries.length)
|
|
1020
|
+
return undefined;
|
|
1021
|
+
return Object.fromEntries(entries);
|
|
1022
|
+
}
|
|
1023
|
+
function safeProjectFile(root, path, fs) {
|
|
1024
|
+
if (!path ||
|
|
1025
|
+
isAbsolute(path) ||
|
|
1026
|
+
[...path].some((character) => {
|
|
1027
|
+
const code = character.codePointAt(0) ?? 0;
|
|
1028
|
+
return code <= 31 || code === 127;
|
|
1029
|
+
}) ||
|
|
1030
|
+
path.split(/[\\/]/).includes(".."))
|
|
1031
|
+
throw new Error("Orchestration plan must be a safe project-relative file.");
|
|
1032
|
+
const projectRoot = fs.realpath?.(resolve(root)) ?? resolve(root);
|
|
1033
|
+
const candidate = resolve(root, path);
|
|
1034
|
+
const candidateType = fs.pathType?.(candidate) ?? (fs.exists(candidate) ? "file" : "missing");
|
|
1035
|
+
if (!isInsidePath(resolve(root), candidate) || candidateType !== "file")
|
|
1036
|
+
throw new Error(`Orchestration plan does not exist or is not a file: ${path}`);
|
|
1037
|
+
const realCandidate = fs.realpath?.(candidate) ?? candidate;
|
|
1038
|
+
if (!isInsidePath(projectRoot, realCandidate))
|
|
1039
|
+
throw new Error("Orchestration plan must stay inside the project.");
|
|
1040
|
+
return candidate;
|
|
1041
|
+
}
|
|
1042
|
+
function isInsidePath(root, destination) {
|
|
1043
|
+
const path = relative(root, destination);
|
|
1044
|
+
return path === "" || (path !== ".." && !path.startsWith(`..${sep}`) && !isAbsolute(path));
|
|
1045
|
+
}
|
|
1046
|
+
function parsePrivateRegistryPackages(payload, registryUrl) {
|
|
1047
|
+
if (!Array.isArray(payload))
|
|
1048
|
+
throw new Error("Private registry response must be an array.");
|
|
1049
|
+
return payload.map((value, index) => {
|
|
1050
|
+
if (!value || typeof value !== "object")
|
|
1051
|
+
throw new Error(`Private registry package ${index} is invalid.`);
|
|
1052
|
+
const item = value;
|
|
1053
|
+
if (typeof item.id !== "string" ||
|
|
1054
|
+
typeof item.version !== "string" ||
|
|
1055
|
+
typeof item.description !== "string" ||
|
|
1056
|
+
!Array.isArray(item.permissions) ||
|
|
1057
|
+
!item.permissions.every((permission) => typeof permission === "string"))
|
|
1058
|
+
throw new Error(`Private registry package ${index} is invalid.`);
|
|
1059
|
+
if (!item.id ||
|
|
1060
|
+
!item.version ||
|
|
1061
|
+
/[\r\n]/.test(item.id) ||
|
|
1062
|
+
/[\r\n]/.test(item.version) ||
|
|
1063
|
+
/[\r\n]/.test(item.description))
|
|
1064
|
+
throw new Error(`Private registry package ${index} contains unsafe text.`);
|
|
1065
|
+
return {
|
|
1066
|
+
id: item.id,
|
|
1067
|
+
version: item.version,
|
|
1068
|
+
description: item.description,
|
|
1069
|
+
provider: "private-registry",
|
|
1070
|
+
source: `${registryUrl}/${item.id}`,
|
|
1071
|
+
permissions: item.permissions,
|
|
1072
|
+
};
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
function containsCredential(value, credential, visited = new Set()) {
|
|
1076
|
+
if (typeof value === "string")
|
|
1077
|
+
return value.includes(credential);
|
|
1078
|
+
if (!value || typeof value !== "object" || visited.has(value))
|
|
1079
|
+
return false;
|
|
1080
|
+
visited.add(value);
|
|
1081
|
+
return Object.values(value).some((entry) => containsCredential(entry, credential, visited));
|
|
1082
|
+
}
|
|
1083
|
+
function readSelectedRecommendations(fs, aiw) {
|
|
1084
|
+
if (!fs.exists(join(aiw, "recommendations.yml")))
|
|
1085
|
+
return [];
|
|
1086
|
+
const content = fs.read(join(aiw, "recommendations.yml"));
|
|
1087
|
+
return content
|
|
1088
|
+
.split(/^\s{2}- id: /m)
|
|
1089
|
+
.slice(1)
|
|
1090
|
+
.filter((block) => block.includes("selected: true"))
|
|
1091
|
+
.map((block) => block.split("\n", 1)[0].trim());
|
|
1092
|
+
}
|
|
1093
|
+
function parseApprovedPermissions(args) {
|
|
1094
|
+
return args
|
|
1095
|
+
.filter((arg) => arg.startsWith("--allow="))
|
|
1096
|
+
.flatMap((arg) => arg.slice("--allow=".length).split(","))
|
|
1097
|
+
.map((permission) => permission.trim())
|
|
1098
|
+
.filter(Boolean);
|
|
1099
|
+
}
|
|
1100
|
+
function escapeRegExp(value) {
|
|
1101
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1102
|
+
}
|
|
1103
|
+
function enforceConfiguredOrganizationPolicy(fs, aiw, subject) {
|
|
1104
|
+
const path = join(aiw, "organization.yml");
|
|
1105
|
+
if (fs.exists(path))
|
|
1106
|
+
enforceOrganizationPolicy(parseOrganizationPolicy(fs.read(path)), subject);
|
|
1107
|
+
}
|
|
1108
|
+
function skillIntegrity(fs, root, skill) {
|
|
1109
|
+
if (!fs.exists(join(root, "skills-lock.json")))
|
|
1110
|
+
return "unknown";
|
|
1111
|
+
const lock = fs.read(join(root, "skills-lock.json"));
|
|
1112
|
+
const escaped = escapeRegExp(skill);
|
|
1113
|
+
return (lock.match(new RegExp(`"${escaped}"\\s*:\\s*\\{[\\s\\S]*?"(?:computedHash|skillFolderHash)"\\s*:\\s*"([^"]+)"`))?.[1] ?? "unknown");
|
|
1114
|
+
}
|
|
1115
|
+
function writeVercelSkillLock(fs, root, aiw, source, skill) {
|
|
1116
|
+
const lockPath = join(aiw, "lock.yml");
|
|
1117
|
+
const current = fs.exists(lockPath) ? fs.read(lockPath) : "schema: 1\npackages:\n";
|
|
1118
|
+
const id = `${source}/${skill}`;
|
|
1119
|
+
const withoutExisting = current.replace(new RegExp(` - id: ${escapeRegExp(id)}\\n(?: [^\\n]+\\n){4,5}`, "g"), "");
|
|
1120
|
+
fs.write(lockPath, `${withoutExisting.replace(/\n*$/, "\n")} - id: ${id}\n version: latest\n provider: vercel-skills\n source: ${source}\n integrity: ${skillIntegrity(fs, root, skill)}\n permissions: [network:external]\n`);
|
|
1121
|
+
}
|
|
1122
|
+
function refreshVercelSkillLocks(fs, root, aiw) {
|
|
1123
|
+
const lockPath = join(aiw, "lock.yml");
|
|
1124
|
+
if (!fs.exists(lockPath))
|
|
1125
|
+
return;
|
|
1126
|
+
const content = fs
|
|
1127
|
+
.read(lockPath)
|
|
1128
|
+
.replace(/( {2}- id: ([^\n]+)\n {4}version: latest\n {4}provider: vercel-skills\n {4}source: ([^\n]+)\n {4}integrity: )([^\n]+)/g, (_entry, prefix, id) => `${prefix}${skillIntegrity(fs, root, id.split("/").at(-1) ?? id)}`);
|
|
1129
|
+
fs.write(lockPath, content);
|
|
1130
|
+
}
|