@williambeto/ai-workflow 2.4.9 → 2.6.3
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/bin/ai-workflow.d.ts +3 -0
- package/bin/ai-workflow.js +2669 -1
- package/bin/ai-workflow.js.map +1 -0
- package/chunk-2AOV2ATY.js +749 -0
- package/chunk-5WRI5ZAA.js +31 -0
- package/chunk-6EMG6FAO.js +734 -0
- package/chunk-UHLZX2EG.js +1827 -0
- package/core/index.d.ts +440 -0
- package/core/index.js +32 -0
- package/core/index.js.map +1 -0
- package/dist-assets/AGENTS.md +23 -6
- package/dist-assets/agents/atlas.md +8 -8
- package/dist-assets/agents/nexus.md +4 -4
- package/dist-assets/agents/orion.md +1 -1
- package/dist-assets/agents/phoenix.md +1 -1
- package/dist-assets/agents/sage.md +1 -1
- package/dist-assets/commands/implement.md +1 -1
- package/dist-assets/docs/QUICKSTART.md +8 -8
- package/dist-assets/docs/cli-reference.md +7 -7
- package/dist-assets/docs/compatibility/provider-usage.md +1 -9
- package/dist-assets/docs/compatibility/runtime-matrix.md +5 -6
- package/dist-assets/docs/consumer-onboarding.md +2 -2
- package/dist-assets/docs/design-patterns-policy.md +1 -1
- package/dist-assets/docs/npm-consumer-quickstart.md +70 -109
- package/dist-assets/docs/policies/01-BRANCH_GATE.md +1 -1
- package/dist-assets/docs/policies/06-FINAL_EVIDENCE_CONTRACT.md +2 -2
- package/dist-assets/docs/policies/08-PRODUCT_TRUTHFULNESS_AND_PROJECT_DOCS.md +1 -1
- package/dist-assets/docs/policies/10-BEHAVIORAL_CONTRACT_HARDENING.md +1 -1
- package/dist-assets/docs/policies/ORCHESTRATION_PROTOCOL.md +8 -9
- package/dist-assets/docs/policies/PROCEDURE_DELIVERY_ARTIFACTS.md +4 -10
- package/dist-assets/docs/policies/SKILLS_COMMON_GOVERNANCE.md +1 -1
- package/dist-assets/docs/profiles/README.md +2 -2
- package/dist-assets/docs/specs/runtime-operational-contract.md +12 -12
- package/dist-assets/docs/troubleshooting-guide.md +2 -2
- package/dist-assets/docs/visual-validation-guide.md +3 -3
- package/dist-assets/schemas/evidence.schema.json +109 -0
- package/dist-assets/templates/ANTIGRAVITY.md.template +7 -5
- package/dist-assets/templates/CLAUDE.md.template +7 -5
- package/dist-assets/templates/CODEX.md.template +7 -5
- package/evidence-validator-76ZQQYDU.js +6661 -0
- package/package.json +7 -64
- package/{src/core/workspace/read-only-workspace.js → read-only-workspace-PZBE7KAX.js} +14 -31
- package/{src/commands/validate.js → validate-Q2NLLQ5G.js} +30 -22
- package/AGENTS.md +0 -34
- package/CHANGELOG.md +0 -259
- package/LICENSE +0 -21
- package/PUBLISH_MANIFEST.json +0 -34
- package/README.md +0 -129
- package/dist-assets/templates/.geminiignore.template +0 -8
- package/dist-assets/templates/GEMINI.md.template +0 -20
- package/docs/compatibility/provider-usage.md +0 -46
- package/docs/compatibility/runtime-matrix.md +0 -31
- package/docs/getting-started/DESKTOP_PROMPT.md +0 -52
- package/docs/getting-started/authoring-custom-skills.md +0 -80
- package/docs/getting-started/quickstart.md +0 -117
- package/docs/getting-started/troubleshooting.md +0 -123
- package/docs/getting-started/upgrading-to-v2.md +0 -62
- package/src/adapters/index.js +0 -4
- package/src/adapters/platforms/antigravity.js +0 -382
- package/src/adapters/platforms/claude.js +0 -126
- package/src/adapters/platforms/codex.js +0 -113
- package/src/adapters/platforms/gemini.js +0 -245
- package/src/cli.js +0 -155
- package/src/commands/clean.js +0 -106
- package/src/commands/collect-evidence.js +0 -25
- package/src/commands/doctor.js +0 -202
- package/src/commands/execute.js +0 -382
- package/src/commands/init.js +0 -359
- package/src/commands/run.js +0 -112
- package/src/core/backup.js +0 -65
- package/src/core/completion-contract.js +0 -35
- package/src/core/delegation-controller.js +0 -193
- package/src/core/evidence/evidence-ledger.js +0 -98
- package/src/core/execution-planner.js +0 -62
- package/src/core/filesystem.js +0 -141
- package/src/core/finalization/finalizer.js +0 -165
- package/src/core/finalization/workspace-snapshot.js +0 -110
- package/src/core/gates/branch-gate.js +0 -134
- package/src/core/gates/merge-gate.js +0 -74
- package/src/core/handoff/handoff-engine.js +0 -104
- package/src/core/healing/cli-remediation-executor.js +0 -153
- package/src/core/healing/healer-engine.js +0 -212
- package/src/core/healing/runtime-remediation-executor.js +0 -136
- package/src/core/identity.js +0 -43
- package/src/core/install-plan.js +0 -34
- package/src/core/opencode-merge.js +0 -150
- package/src/core/package-assets.js +0 -141
- package/src/core/request-classifier.js +0 -73
- package/src/core/request-router.js +0 -289
- package/src/core/runtime/opencode-adapter.js +0 -202
- package/src/core/sdd/validator.js +0 -67
- package/src/core/statuses.js +0 -29
- package/src/core/symlink-layout.js +0 -93
- package/src/core/templates.js +0 -229
- package/src/core/validation/artifact-fidelity-gate.js +0 -445
- package/src/core/validation/canonical-finalization.js +0 -43
- package/src/core/validation/delivery-decision-engine.js +0 -344
- package/src/core/validation/evidence-collector.js +0 -245
- package/src/core/validation/quality-guard.js +0 -251
- package/src/core/validation/stack-detector.js +0 -65
- package/src/core/validation/validation-planner.js +0 -134
- package/src/core/validation/visual-verifier.js +0 -175
- package/src/core/workflow-profiles.js +0 -107
- package/src/core/workflow-state-machine.js +0 -46
package/bin/ai-workflow.js
CHANGED
|
@@ -1,8 +1,2676 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
BranchGate,
|
|
4
|
+
COMPLETION_STATUS_TEXT,
|
|
5
|
+
DelegationController,
|
|
6
|
+
EvidenceLedger,
|
|
7
|
+
ExecutionPlanner,
|
|
8
|
+
Finalizer,
|
|
9
|
+
HandoffEngine,
|
|
10
|
+
HealerEngine,
|
|
11
|
+
OpenCodeAdapter,
|
|
12
|
+
RequestClassifier,
|
|
13
|
+
SpecValidator,
|
|
14
|
+
WorkflowStateMachine,
|
|
15
|
+
discoverPackageFiles,
|
|
16
|
+
getFullAgentContent,
|
|
17
|
+
getFullSkillFiles,
|
|
18
|
+
getPackageVersion,
|
|
19
|
+
getWorkflowProfile,
|
|
20
|
+
isRecoverableGateFailure,
|
|
21
|
+
isTerminalFailure,
|
|
22
|
+
readPackageFile,
|
|
23
|
+
resolveWorkflowProfile
|
|
24
|
+
} from "../chunk-UHLZX2EG.js";
|
|
25
|
+
import {
|
|
26
|
+
EvidenceCollector,
|
|
27
|
+
QualityGuard,
|
|
28
|
+
ValidationPlanner
|
|
29
|
+
} from "../chunk-6EMG6FAO.js";
|
|
30
|
+
import "../chunk-2AOV2ATY.js";
|
|
31
|
+
import "../chunk-5WRI5ZAA.js";
|
|
2
32
|
|
|
3
|
-
|
|
33
|
+
// src/cli/commands/init.ts
|
|
34
|
+
import fs7 from "fs/promises";
|
|
35
|
+
import path9 from "path";
|
|
4
36
|
|
|
37
|
+
// src/core/install-plan.ts
|
|
38
|
+
import path from "path";
|
|
39
|
+
|
|
40
|
+
// src/core/templates.ts
|
|
41
|
+
var COMMON_FILES = {
|
|
42
|
+
"opencode/README.md": `# OpenCode Setup
|
|
43
|
+
|
|
44
|
+
This directory is managed by \`ai-workflow\` init.
|
|
45
|
+
Add agent and command files required by your project workflow.
|
|
46
|
+
`
|
|
47
|
+
};
|
|
48
|
+
var FULL_PRIMARY_AGENTS = [
|
|
49
|
+
"atlas",
|
|
50
|
+
"nexus",
|
|
51
|
+
"orion",
|
|
52
|
+
"astra",
|
|
53
|
+
"sage",
|
|
54
|
+
"phoenix"
|
|
55
|
+
];
|
|
56
|
+
var FULL_SKILLS = [
|
|
57
|
+
"project-memory",
|
|
58
|
+
"optimize-tokens",
|
|
59
|
+
"documentation",
|
|
60
|
+
"architecture",
|
|
61
|
+
"technical-leadership",
|
|
62
|
+
"product-discovery",
|
|
63
|
+
"product-planning",
|
|
64
|
+
"spec-driven-development",
|
|
65
|
+
"pr-workflow",
|
|
66
|
+
"qa-workflow",
|
|
67
|
+
"release-workflow",
|
|
68
|
+
"deployment",
|
|
69
|
+
"ui-ux-design",
|
|
70
|
+
"design-principles",
|
|
71
|
+
"frontend-development",
|
|
72
|
+
"backend-development",
|
|
73
|
+
"full-stack-development",
|
|
74
|
+
"refactoring",
|
|
75
|
+
"prompt-engineer"
|
|
76
|
+
];
|
|
77
|
+
function buildRuntimeFiles({ includeFormalEvidence = false } = {}) {
|
|
78
|
+
const files = {};
|
|
79
|
+
for (const agent of FULL_PRIMARY_AGENTS) {
|
|
80
|
+
const content = getFullAgentContent(agent);
|
|
81
|
+
if (content === null) {
|
|
82
|
+
throw new Error(`CRITICAL FAILURE: Missing required agent asset: dist-assets/agents/${agent}.md`);
|
|
83
|
+
}
|
|
84
|
+
files[`opencode/agents/${agent}.md`] = content;
|
|
85
|
+
}
|
|
86
|
+
for (const skill of FULL_SKILLS) {
|
|
87
|
+
const skillFiles = getFullSkillFiles(skill);
|
|
88
|
+
if (Object.keys(skillFiles).length === 0) {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
for (const [relPath, content] of Object.entries(skillFiles)) {
|
|
92
|
+
const targetPath = relPath.replace(/^dist-assets\//, "opencode/");
|
|
93
|
+
if (!files[targetPath]) {
|
|
94
|
+
files[targetPath] = content;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const opencodeGovernancePolicy = readPackageFile("dist-assets/docs/policies/SKILLS_COMMON_GOVERNANCE.md");
|
|
99
|
+
if (opencodeGovernancePolicy !== null) {
|
|
100
|
+
files["opencode/docs/policies/SKILLS_COMMON_GOVERNANCE.md"] = opencodeGovernancePolicy;
|
|
101
|
+
}
|
|
102
|
+
const commandFiles = discoverPackageFiles("dist-assets/commands");
|
|
103
|
+
for (const [relPath, content] of Object.entries(commandFiles)) {
|
|
104
|
+
const targetPath = relPath.replace(/^dist-assets\//, "opencode/");
|
|
105
|
+
if (!files[targetPath]) {
|
|
106
|
+
files[targetPath] = content;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const opencodeSkillFiles = discoverPackageFiles("dist-assets/skills");
|
|
110
|
+
for (const [relPath, content] of Object.entries(opencodeSkillFiles)) {
|
|
111
|
+
const targetPath = relPath.replace(/^dist-assets\//, "opencode/");
|
|
112
|
+
if (!files[targetPath]) {
|
|
113
|
+
files[targetPath] = content;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const harnessWorkflowFiles = discoverPackageFiles("dist-assets/harness/workflows");
|
|
117
|
+
for (const [relPath, content] of Object.entries(harnessWorkflowFiles)) {
|
|
118
|
+
const targetPath = relPath.replace(/^dist-assets\//, "");
|
|
119
|
+
files[targetPath] = content;
|
|
120
|
+
}
|
|
121
|
+
const harnessHandoffFiles = discoverPackageFiles("dist-assets/harness/handoffs");
|
|
122
|
+
for (const [relPath, content] of Object.entries(harnessHandoffFiles)) {
|
|
123
|
+
const targetPath = relPath.replace(/^dist-assets\//, "");
|
|
124
|
+
files[targetPath] = content;
|
|
125
|
+
}
|
|
126
|
+
const agentsContent = readPackageFile("dist-assets/AGENTS.md");
|
|
127
|
+
if (agentsContent !== null) files["AGENTS.md"] = agentsContent;
|
|
128
|
+
const consumerQuickstart = readPackageFile("dist-assets/docs/QUICKSTART.md");
|
|
129
|
+
if (consumerQuickstart !== null) files["QUICKSTART.md"] = consumerQuickstart;
|
|
130
|
+
const policyContent = readPackageFile("dist-assets/docs/architecture-policy.md");
|
|
131
|
+
if (policyContent !== null) files["opencode/docs/architecture-policy.md"] = policyContent;
|
|
132
|
+
const dpContent = readPackageFile("dist-assets/docs/design-patterns-policy.md");
|
|
133
|
+
if (dpContent !== null) files["opencode/docs/design-patterns-policy.md"] = dpContent;
|
|
134
|
+
const visualValidationGuide = readPackageFile("dist-assets/docs/visual-validation-guide.md");
|
|
135
|
+
if (visualValidationGuide !== null) files["opencode/docs/visual-validation-guide.md"] = visualValidationGuide;
|
|
136
|
+
const governancePolicies = discoverPackageFiles("dist-assets/docs/policies");
|
|
137
|
+
for (const [relPath, content] of Object.entries(governancePolicies)) {
|
|
138
|
+
const targetPath = relPath.replace(/^dist-assets\//, "opencode/");
|
|
139
|
+
files[targetPath] = content;
|
|
140
|
+
}
|
|
141
|
+
const compatibilityDocs = discoverPackageFiles("dist-assets/docs/compatibility");
|
|
142
|
+
for (const [relPath, content] of Object.entries(compatibilityDocs)) {
|
|
143
|
+
const targetPath = relPath.replace(/^dist-assets\//, "opencode/");
|
|
144
|
+
files[targetPath] = content;
|
|
145
|
+
}
|
|
146
|
+
const qualityReferenceFiles = discoverPackageFiles("dist-assets/docs/references");
|
|
147
|
+
for (const [relPath, content] of Object.entries(qualityReferenceFiles)) {
|
|
148
|
+
const targetPath = relPath.replace(/^dist-assets\//, "opencode/");
|
|
149
|
+
files[targetPath] = content;
|
|
150
|
+
}
|
|
151
|
+
const workflowProfileFiles = discoverPackageFiles("dist-assets/docs/profiles");
|
|
152
|
+
for (const [relPath, content] of Object.entries(workflowProfileFiles)) {
|
|
153
|
+
const targetPath = relPath.replace(/^dist-assets\//, "opencode/");
|
|
154
|
+
files[targetPath] = content;
|
|
155
|
+
}
|
|
156
|
+
const templateFiles = discoverPackageFiles("dist-assets/templates");
|
|
157
|
+
for (const [relPath, content] of Object.entries(templateFiles)) {
|
|
158
|
+
if (!includeFormalEvidence && relPath.includes("/owner-evidence/")) continue;
|
|
159
|
+
const targetPath = relPath.replace(/^dist-assets\//, "");
|
|
160
|
+
files[targetPath] = content;
|
|
161
|
+
}
|
|
162
|
+
const schemaFiles = discoverPackageFiles("dist-assets/schemas");
|
|
163
|
+
for (const [relPath, content] of Object.entries(schemaFiles)) {
|
|
164
|
+
const targetPath = relPath.replace(/^dist-assets\//, "");
|
|
165
|
+
files[targetPath] = content;
|
|
166
|
+
}
|
|
167
|
+
return files;
|
|
168
|
+
}
|
|
169
|
+
function buildExtraFiles() {
|
|
170
|
+
const files = {};
|
|
171
|
+
const exampleFiles = discoverPackageFiles("dist-assets/examples");
|
|
172
|
+
for (const [relPath, content] of Object.entries(exampleFiles)) {
|
|
173
|
+
const targetPath = relPath.replace(/^dist-assets\//, "");
|
|
174
|
+
files[targetPath] = content;
|
|
175
|
+
}
|
|
176
|
+
return files;
|
|
177
|
+
}
|
|
178
|
+
var STANDARD_FILES = buildRuntimeFiles();
|
|
179
|
+
var FULL_FILES = { ...buildRuntimeFiles({ includeFormalEvidence: true }), ...buildExtraFiles() };
|
|
180
|
+
var PROFILE_FILES = {
|
|
181
|
+
standard: {
|
|
182
|
+
...COMMON_FILES,
|
|
183
|
+
...STANDARD_FILES
|
|
184
|
+
},
|
|
185
|
+
full: {
|
|
186
|
+
...COMMON_FILES,
|
|
187
|
+
...FULL_FILES
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
function isValidProfile(profile) {
|
|
191
|
+
return profile === "standard" || profile === "full";
|
|
192
|
+
}
|
|
193
|
+
function getTemplateFiles(profile = "standard") {
|
|
194
|
+
return PROFILE_FILES[profile] ?? PROFILE_FILES.standard;
|
|
195
|
+
}
|
|
196
|
+
function buildAiWorkflowConfig({ profile }) {
|
|
197
|
+
const version = getPackageVersion();
|
|
198
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
199
|
+
return {
|
|
200
|
+
package: "@williambeto/ai-workflow",
|
|
201
|
+
version,
|
|
202
|
+
runtime: "opencode",
|
|
203
|
+
profile,
|
|
204
|
+
installMode: "project-local",
|
|
205
|
+
generatedAt: now,
|
|
206
|
+
paths: {
|
|
207
|
+
agents: ".ai-workflow/opencode/agents",
|
|
208
|
+
commands: ".ai-workflow/opencode/commands",
|
|
209
|
+
skills: ".ai-workflow/opencode/skills",
|
|
210
|
+
policies: ".ai-workflow/opencode/docs/policies",
|
|
211
|
+
harness: ".ai-workflow/harness",
|
|
212
|
+
schemas: ".ai-workflow/schemas"
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// src/core/install-plan.ts
|
|
218
|
+
async function createInstallPlan({
|
|
219
|
+
cwd,
|
|
220
|
+
exists: exists3,
|
|
221
|
+
profile = "standard"
|
|
222
|
+
}) {
|
|
223
|
+
const actions = [];
|
|
224
|
+
const templateFiles = getTemplateFiles(profile);
|
|
225
|
+
const installRoot = ".ai-workflow";
|
|
226
|
+
for (const [relativePath, content] of Object.entries(templateFiles)) {
|
|
227
|
+
if (relativePath === "opencode.jsonc") {
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
const installedRelativePath = path.join(installRoot, relativePath);
|
|
231
|
+
const absolutePath = path.join(cwd, installedRelativePath);
|
|
232
|
+
const action = await exists3(absolutePath) ? "conflict" : "create";
|
|
233
|
+
actions.push({
|
|
234
|
+
type: action,
|
|
235
|
+
relativePath: installedRelativePath,
|
|
236
|
+
absolutePath,
|
|
237
|
+
content
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
const configPath = path.join(cwd, ".ai-workflow/config.json");
|
|
241
|
+
actions.push({
|
|
242
|
+
type: await exists3(configPath) ? "conflict" : "create",
|
|
243
|
+
relativePath: ".ai-workflow/config.json",
|
|
244
|
+
absolutePath: configPath,
|
|
245
|
+
content: null
|
|
246
|
+
});
|
|
247
|
+
return actions;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// src/core/filesystem.ts
|
|
251
|
+
import fs from "fs/promises";
|
|
252
|
+
import path2 from "path";
|
|
253
|
+
import { execSync } from "child_process";
|
|
254
|
+
async function exists(filePath) {
|
|
255
|
+
try {
|
|
256
|
+
await fs.access(filePath);
|
|
257
|
+
return true;
|
|
258
|
+
} catch {
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
async function writeFileSafe(filePath, content) {
|
|
263
|
+
const absolutePath = path2.resolve(filePath);
|
|
264
|
+
const repoCwd = path2.resolve(process.cwd());
|
|
265
|
+
const relativeToRepo = path2.relative(repoCwd, absolutePath).replaceAll("\\", "/");
|
|
266
|
+
const isOperational = relativeToRepo === "EVIDENCE.json" || relativeToRepo.startsWith(".ai-workflow/") || relativeToRepo.startsWith(".ai-workflow\\");
|
|
267
|
+
if (!isOperational) {
|
|
268
|
+
let currentBranch = "";
|
|
269
|
+
const fileCwd = path2.dirname(absolutePath);
|
|
270
|
+
try {
|
|
271
|
+
currentBranch = execSync("git branch --show-current", { cwd: fileCwd, encoding: "utf8", stdio: "pipe" }).trim();
|
|
272
|
+
} catch {
|
|
273
|
+
}
|
|
274
|
+
const protectedBranches = ["main", "master"];
|
|
275
|
+
const isInitCommand = process.argv.includes("init");
|
|
276
|
+
if (protectedBranches.includes(currentBranch) && !isInitCommand) {
|
|
277
|
+
if (process.env.AI_WORKFLOW_BYPASS_PROTECTED_BRANCH !== "true") {
|
|
278
|
+
throw new Error("FAIL_PROTECTED_BRANCH: Direct writes to protected branch are prohibited.");
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
await fs.mkdir(path2.dirname(filePath), { recursive: true });
|
|
283
|
+
await fs.writeFile(filePath, content, "utf8");
|
|
284
|
+
}
|
|
285
|
+
async function readJson(filePath) {
|
|
286
|
+
const content = await fs.readFile(filePath, "utf8");
|
|
287
|
+
return JSON.parse(content);
|
|
288
|
+
}
|
|
289
|
+
function stripJsonComments(content) {
|
|
290
|
+
let output = "";
|
|
291
|
+
let inString = false;
|
|
292
|
+
let quote = "";
|
|
293
|
+
let escaped = false;
|
|
294
|
+
let inLineComment = false;
|
|
295
|
+
let inBlockComment = false;
|
|
296
|
+
for (let index = 0; index < content.length; index += 1) {
|
|
297
|
+
const char = content[index];
|
|
298
|
+
const next = content[index + 1];
|
|
299
|
+
if (inLineComment) {
|
|
300
|
+
if (char === "\n" || char === "\r") {
|
|
301
|
+
inLineComment = false;
|
|
302
|
+
output += char;
|
|
303
|
+
}
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
if (inBlockComment) {
|
|
307
|
+
if (char === "*" && next === "/") {
|
|
308
|
+
inBlockComment = false;
|
|
309
|
+
index += 1;
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
if (char === "\n" || char === "\r") {
|
|
313
|
+
output += char;
|
|
314
|
+
}
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
if (inString) {
|
|
318
|
+
output += char;
|
|
319
|
+
if (escaped) {
|
|
320
|
+
escaped = false;
|
|
321
|
+
} else if (char === "\\") {
|
|
322
|
+
escaped = true;
|
|
323
|
+
} else if (char === quote) {
|
|
324
|
+
inString = false;
|
|
325
|
+
quote = "";
|
|
326
|
+
}
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
if (char === '"' || char === "'") {
|
|
330
|
+
inString = true;
|
|
331
|
+
quote = char;
|
|
332
|
+
output += char;
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
if (char === "/" && next === "/") {
|
|
336
|
+
inLineComment = true;
|
|
337
|
+
index += 1;
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
if (char === "/" && next === "*") {
|
|
341
|
+
inBlockComment = true;
|
|
342
|
+
index += 1;
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
output += char;
|
|
346
|
+
}
|
|
347
|
+
return output;
|
|
348
|
+
}
|
|
349
|
+
function stripTrailingCommas(content) {
|
|
350
|
+
let output = "";
|
|
351
|
+
let inString = false;
|
|
352
|
+
let quote = "";
|
|
353
|
+
let escaped = false;
|
|
354
|
+
for (let index = 0; index < content.length; index += 1) {
|
|
355
|
+
const char = content[index];
|
|
356
|
+
if (inString) {
|
|
357
|
+
output += char;
|
|
358
|
+
if (escaped) {
|
|
359
|
+
escaped = false;
|
|
360
|
+
} else if (char === "\\") {
|
|
361
|
+
escaped = true;
|
|
362
|
+
} else if (char === quote) {
|
|
363
|
+
inString = false;
|
|
364
|
+
quote = "";
|
|
365
|
+
}
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
if (char === '"' || char === "'") {
|
|
369
|
+
inString = true;
|
|
370
|
+
quote = char;
|
|
371
|
+
output += char;
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
if (char === ",") {
|
|
375
|
+
let nextIndex = index + 1;
|
|
376
|
+
while (/\s/.test(content[nextIndex] ?? "")) {
|
|
377
|
+
nextIndex += 1;
|
|
378
|
+
}
|
|
379
|
+
if (content[nextIndex] === "}" || content[nextIndex] === "]") {
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
output += char;
|
|
384
|
+
}
|
|
385
|
+
return output;
|
|
386
|
+
}
|
|
387
|
+
async function readJsonc(filePath) {
|
|
388
|
+
const content = await fs.readFile(filePath, "utf8");
|
|
389
|
+
return JSON.parse(stripTrailingCommas(stripJsonComments(content)));
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// src/core/backup.ts
|
|
393
|
+
import fs2 from "fs/promises";
|
|
394
|
+
import path3 from "path";
|
|
395
|
+
function escapeRelativePath(relativePath) {
|
|
396
|
+
return relativePath.replace(/[\\/]/g, "__");
|
|
397
|
+
}
|
|
398
|
+
async function createManagedBackup(filePath, { cwd, backupRoot, maxPerFile = 20 }) {
|
|
399
|
+
const relativePath = path3.relative(cwd, filePath);
|
|
400
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
401
|
+
const backupName = `${escapeRelativePath(relativePath)}.${stamp}.bak`;
|
|
402
|
+
const absoluteBackupRoot = path3.join(cwd, backupRoot);
|
|
403
|
+
const backupPath = path3.join(absoluteBackupRoot, backupName);
|
|
404
|
+
await fs2.mkdir(absoluteBackupRoot, { recursive: true });
|
|
405
|
+
await fs2.copyFile(filePath, backupPath);
|
|
406
|
+
const prefix = `${escapeRelativePath(relativePath)}.`;
|
|
407
|
+
const entries = await fs2.readdir(absoluteBackupRoot);
|
|
408
|
+
const matching = entries.filter((entry) => entry.startsWith(prefix) && entry.endsWith(".bak")).sort();
|
|
409
|
+
if (matching.length > maxPerFile) {
|
|
410
|
+
const excess = matching.slice(0, matching.length - maxPerFile);
|
|
411
|
+
await Promise.all(excess.map((entry) => fs2.unlink(path3.join(absoluteBackupRoot, entry))));
|
|
412
|
+
}
|
|
413
|
+
return backupPath;
|
|
414
|
+
}
|
|
415
|
+
async function createManagedPathBackup(targetPath, { cwd, backupRoot, maxPerFile = 20 }) {
|
|
416
|
+
const relativePath = path3.relative(cwd, targetPath);
|
|
417
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
418
|
+
const backupName = `${escapeRelativePath(relativePath)}.${stamp}.bak`;
|
|
419
|
+
const absoluteBackupRoot = path3.join(cwd, backupRoot);
|
|
420
|
+
const backupPath = path3.join(absoluteBackupRoot, backupName);
|
|
421
|
+
await fs2.mkdir(absoluteBackupRoot, { recursive: true });
|
|
422
|
+
await fs2.rename(targetPath, backupPath);
|
|
423
|
+
const prefix = `${escapeRelativePath(relativePath)}.`;
|
|
424
|
+
const entries = await fs2.readdir(absoluteBackupRoot);
|
|
425
|
+
const matching = entries.filter((entry) => entry.startsWith(prefix) && entry.endsWith(".bak")).sort();
|
|
426
|
+
if (matching.length > maxPerFile) {
|
|
427
|
+
const excess = matching.slice(0, matching.length - maxPerFile);
|
|
428
|
+
await Promise.all(excess.map((entry) => fs2.rm(path3.join(absoluteBackupRoot, entry), { recursive: true, force: true })));
|
|
429
|
+
}
|
|
430
|
+
return backupPath;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// src/core/opencode-merge.ts
|
|
434
|
+
import path4 from "path";
|
|
435
|
+
var LEGACY_MARKER = "_aiWorkflowManaged";
|
|
436
|
+
var AGENTS = {
|
|
437
|
+
Atlas: { description: "Router and workflow coordinator for AI Workflow Kit" },
|
|
438
|
+
Nexus: { description: "Discovery, requirement, scope, and specification owner" },
|
|
439
|
+
Orion: { description: "Planning, PR sequencing, release, and deployment strategy owner" },
|
|
440
|
+
Astra: { description: "Implementation owner for incremental, scoped changes" },
|
|
441
|
+
Sage: { description: "Audit, validation, evidence, and quality gate owner" },
|
|
442
|
+
Phoenix: { description: "Remediation and regression recovery owner" }
|
|
443
|
+
};
|
|
444
|
+
var SUBAGENTS = {
|
|
445
|
+
"Architecture-Specialist": { description: "Select the simplest architecture that safely satisfies current requirements", skill: "architecture" },
|
|
446
|
+
"Backend-Engineer": { description: "Support safe PHP/Node/Python backend changes for APIs and persistence", skill: "backend-development" },
|
|
447
|
+
"Deployment-Specialist": { description: "Plan, review, or validate deployment, release, and production-readiness", skill: "deployment" },
|
|
448
|
+
"Design-Specialist": { description: "Apply practical design principles to keep solutions simple and maintainable", skill: "design-principles" },
|
|
449
|
+
"Docs-Engineer": { description: "Write or improve project documentation, READMEs, guides, and prompts", skill: "documentation" },
|
|
450
|
+
"Frontend-Engineer": { description: "Implement or review frontend changes involving UI, state, routing, and accessibility", skill: "frontend-development" },
|
|
451
|
+
"Frontend-Design-System-Specialist": { description: "Enforce CSS/HTML tokens, semantic elements, typography, and premium atmosphere constraints for frontend quality", skill: "frontend-design-system" },
|
|
452
|
+
"Full-Stack-Engineer": { description: "Coordinate frontend and backend changes as one coherent, testable increment", skill: "full-stack-development" },
|
|
453
|
+
"Token-Economist": { description: "Reduce context size while preserving information required for safe execution", skill: "optimize-tokens" },
|
|
454
|
+
"PR-Manager": { description: "Plan, implement, review, or validate small pull requests while preserving scope", skill: "pr-workflow" },
|
|
455
|
+
"Discovery-Analyst": { description: "Clarify ambiguous requests into actionable problem statements and success outcomes", skill: "product-discovery" },
|
|
456
|
+
"Product-Planner": { description: "Turn clarified needs into scoped requirements with acceptance-ready outcomes", skill: "product-planning" },
|
|
457
|
+
"Memory-Guardian": { description: "Manage discoverable project memory for durable decisions and recurring corrections", skill: "project-memory" },
|
|
458
|
+
"Prompt-Engineer": { description: "Expert in creating and refining prompts for AI agents and workflows", skill: "prompt-engineer" },
|
|
459
|
+
"QA-Engineer": { description: "Design, review, or improve tests, acceptance criteria, and validation evidence", skill: "qa-workflow" },
|
|
460
|
+
"Refactoring-Specialist": { description: "Improve code/document structure safely while preserving behavior", skill: "refactoring" },
|
|
461
|
+
"Release-Specialist": { description: "Prepare release readiness decisions with explicit gates and evidence", skill: "release-workflow" },
|
|
462
|
+
"SDD-Specialist": { description: "Convert approved requirements into explicit, testable specification artifacts", skill: "spec-driven-development" },
|
|
463
|
+
"Technical-Leader": { description: "Make technical decisions, review architecture, and identify trade-offs", skill: "technical-leadership" },
|
|
464
|
+
"UI-UX-Engineer": { description: "Improve usability and interface clarity without introducing unnecessary complexity", skill: "ui-ux-design" }
|
|
465
|
+
};
|
|
466
|
+
var COMMANDS = {
|
|
467
|
+
atlas: { description: "Decide safest next step and delegate to the appropriate workflow or agent", agent: "Atlas" },
|
|
468
|
+
run: { description: "Execute the master orchestration pipeline (Gates, SDD, Implementation, Validation, Healing)", agent: "Atlas" },
|
|
469
|
+
discover: { description: "Understand the project, gather context, and create initial documentation", agent: "Nexus" },
|
|
470
|
+
"spec-create": { description: "Create a clear specification before implementation", agent: "Nexus" },
|
|
471
|
+
"spec-review": { description: "Review a specification and validate scope, risks, criteria, and gaps", agent: "Nexus" },
|
|
472
|
+
"spec-implement": { description: "Implement an approved specification incrementally", agent: "Astra" },
|
|
473
|
+
plan: { description: "Plan a task or change, defining steps, risks, and validation", agent: "Orion" },
|
|
474
|
+
implement: { description: "Implement a small task with defined scope", agent: "Astra" },
|
|
475
|
+
validate: { description: "Run project validations and collect evidence", agent: "Sage" },
|
|
476
|
+
audit: { description: "Audit the project, identify issues, and suggest prioritized improvements", agent: "Sage" },
|
|
477
|
+
"optimize-tokens": { description: "Analyze workflow and context usage; suggest reductions without losing quality or evidence", agent: "Sage" },
|
|
478
|
+
"update-memory": { description: "Update durable project memory with relevant decisions, constraints, and reusable context", agent: "Nexus" },
|
|
479
|
+
release: { description: "Prepare delivery with commit, push, PR/merge readiness, changelog, and evidence", agent: "Orion" },
|
|
480
|
+
deploy: { description: "Publish the project to the defined environment after validation and release readiness", agent: "Orion" }
|
|
481
|
+
};
|
|
482
|
+
function buildManagedConfig() {
|
|
483
|
+
const agent = {};
|
|
484
|
+
for (const [name, def] of Object.entries(AGENTS)) {
|
|
485
|
+
agent[name] = {
|
|
486
|
+
mode: "primary",
|
|
487
|
+
description: def.description,
|
|
488
|
+
prompt: `{file:./.ai-workflow/opencode/agents/${name.toLowerCase()}.md}`
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
for (const [name, def] of Object.entries(SUBAGENTS)) {
|
|
492
|
+
agent[name] = {
|
|
493
|
+
mode: "subagent",
|
|
494
|
+
description: def.description,
|
|
495
|
+
prompt: `{file:./.ai-workflow/opencode/skills/${def.skill}/SKILL.md}`
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
const command = {};
|
|
499
|
+
for (const [name, def] of Object.entries(COMMANDS)) {
|
|
500
|
+
command[name] = {
|
|
501
|
+
description: def.description,
|
|
502
|
+
agent: def.agent,
|
|
503
|
+
template: `{file:./.ai-workflow/opencode/commands/${name}.md}`
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
return {
|
|
507
|
+
$schema: "https://opencode.ai/config.json",
|
|
508
|
+
default_agent: "Atlas",
|
|
509
|
+
agent,
|
|
510
|
+
skills: {
|
|
511
|
+
paths: ["./.ai-workflow/opencode/skills"]
|
|
512
|
+
},
|
|
513
|
+
command
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
async function mergeOpencodeConfig(cwd, { force = false, backupRoot = ".ai-workflow-backups" } = {}) {
|
|
517
|
+
const targetPath = path4.join(cwd, "opencode.jsonc");
|
|
518
|
+
const managed = buildManagedConfig();
|
|
519
|
+
if (!await exists(targetPath)) {
|
|
520
|
+
await writeFileSafe(targetPath, `${JSON.stringify(managed, null, 2)}
|
|
521
|
+
`);
|
|
522
|
+
return { changed: true, reason: "created" };
|
|
523
|
+
}
|
|
524
|
+
let current;
|
|
525
|
+
try {
|
|
526
|
+
current = await readJsonc(targetPath);
|
|
527
|
+
} catch {
|
|
528
|
+
if (!force) {
|
|
529
|
+
return { changed: false, reason: "invalid-json (skipped)" };
|
|
530
|
+
}
|
|
531
|
+
await createManagedBackup(targetPath, {
|
|
532
|
+
cwd,
|
|
533
|
+
backupRoot,
|
|
534
|
+
maxPerFile: 20
|
|
535
|
+
});
|
|
536
|
+
await writeFileSafe(targetPath, `${JSON.stringify(managed, null, 2)}
|
|
537
|
+
`);
|
|
538
|
+
return { changed: true, reason: "invalid-json (replaced with --force)" };
|
|
539
|
+
}
|
|
540
|
+
const { [LEGACY_MARKER]: _legacyManaged, ...currentWithoutLegacy } = current;
|
|
541
|
+
const next = {
|
|
542
|
+
...currentWithoutLegacy,
|
|
543
|
+
$schema: current.$schema ?? managed.$schema,
|
|
544
|
+
...managed.mcp ? {
|
|
545
|
+
mcp: {
|
|
546
|
+
...current.mcp ?? {},
|
|
547
|
+
...managed.mcp
|
|
548
|
+
}
|
|
549
|
+
} : {},
|
|
550
|
+
agent: {
|
|
551
|
+
...current.agent ?? {},
|
|
552
|
+
...managed.agent
|
|
553
|
+
},
|
|
554
|
+
command: {
|
|
555
|
+
...current.command ?? {},
|
|
556
|
+
...managed.command
|
|
557
|
+
}
|
|
558
|
+
};
|
|
559
|
+
const changed = JSON.stringify(current) !== JSON.stringify(next);
|
|
560
|
+
if (!changed) {
|
|
561
|
+
return { changed: false, reason: "already-up-to-date" };
|
|
562
|
+
}
|
|
563
|
+
await writeFileSafe(targetPath, `${JSON.stringify(next, null, 2)}
|
|
564
|
+
`);
|
|
565
|
+
return { changed: true, reason: "merged-managed-sections" };
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// src/core/symlink-layout.ts
|
|
569
|
+
import fs3 from "fs/promises";
|
|
570
|
+
import path5 from "path";
|
|
571
|
+
function normalize(p) {
|
|
572
|
+
return p.split(path5.sep).join("/");
|
|
573
|
+
}
|
|
574
|
+
var LINKABLE_ROOTS = /* @__PURE__ */ new Set([
|
|
575
|
+
"opencode",
|
|
576
|
+
".agents"
|
|
577
|
+
]);
|
|
578
|
+
var LINKABLE_FILES = /* @__PURE__ */ new Set([]);
|
|
579
|
+
function buildSymlinkEntries({ templateFiles, installRoot = ".ai-workflow" }) {
|
|
580
|
+
const entries = /* @__PURE__ */ new Map();
|
|
581
|
+
for (const relativePath of Object.keys(templateFiles)) {
|
|
582
|
+
if (relativePath === "opencode.jsonc") continue;
|
|
583
|
+
const normalized = normalize(relativePath);
|
|
584
|
+
if (LINKABLE_FILES.has(normalized)) {
|
|
585
|
+
entries.set(normalized, `${installRoot}/${normalized}`);
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
588
|
+
for (const rootPath of LINKABLE_ROOTS) {
|
|
589
|
+
if (normalized === rootPath || normalized.startsWith(`${rootPath}/`)) {
|
|
590
|
+
entries.set(rootPath, `${installRoot}/${rootPath}`);
|
|
591
|
+
break;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
return Array.from(entries.entries()).map(([linkPath, targetPath]) => ({ linkPath, targetPath }));
|
|
596
|
+
}
|
|
597
|
+
async function isSymlinkTo(absoluteLinkPath, absoluteTargetPath) {
|
|
598
|
+
try {
|
|
599
|
+
const stat = await fs3.lstat(absoluteLinkPath);
|
|
600
|
+
if (!stat.isSymbolicLink()) return false;
|
|
601
|
+
const currentTarget = await fs3.readlink(absoluteLinkPath);
|
|
602
|
+
const resolved = path5.resolve(path5.dirname(absoluteLinkPath), currentTarget);
|
|
603
|
+
return resolved === absoluteTargetPath;
|
|
604
|
+
} catch {
|
|
605
|
+
return false;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
async function setupInternalSymlinks(cwd, installRoot = ".ai-workflow") {
|
|
609
|
+
const skillsLink = path5.join(cwd, installRoot, "opencode/skills");
|
|
610
|
+
const skillsTarget = path5.join(cwd, installRoot, ".agents/skills");
|
|
611
|
+
if (!await exists2(skillsTarget)) return;
|
|
612
|
+
try {
|
|
613
|
+
const stat = await fs3.lstat(skillsLink).catch(() => null);
|
|
614
|
+
if (stat) {
|
|
615
|
+
if (stat.isSymbolicLink()) {
|
|
616
|
+
const current = await fs3.readlink(skillsLink);
|
|
617
|
+
if (current === "../.agents/skills") return;
|
|
618
|
+
await fs3.unlink(skillsLink);
|
|
619
|
+
} else {
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
const type = process.platform === "win32" ? "junction" : "dir";
|
|
624
|
+
await fs3.symlink("../.agents/skills", skillsLink, type);
|
|
625
|
+
} catch {
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
async function exists2(p) {
|
|
629
|
+
try {
|
|
630
|
+
await fs3.access(p);
|
|
631
|
+
return true;
|
|
632
|
+
} catch {
|
|
633
|
+
return false;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// src/adapters/platforms/claude.ts
|
|
638
|
+
import fs4 from "fs/promises";
|
|
639
|
+
import path6 from "path";
|
|
640
|
+
var ClaudeAdapter = class {
|
|
641
|
+
cwd;
|
|
642
|
+
constructor({ cwd }) {
|
|
643
|
+
this.cwd = cwd;
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Transforms an OpenCode agent or skill into a Claude rule with completion contract.
|
|
647
|
+
*/
|
|
648
|
+
async transformToRule(filePath, type = "agent") {
|
|
649
|
+
const content = await fs4.readFile(filePath, "utf8");
|
|
650
|
+
const fileName = path6.basename(filePath, ".md") === "SKILL" ? path6.basename(path6.dirname(filePath)) : path6.basename(filePath, ".md");
|
|
651
|
+
const ruleName = `${type}-${fileName.toLowerCase()}`;
|
|
652
|
+
const hardenedContent = `---
|
|
653
|
+
name: ${ruleName}
|
|
654
|
+
description: ${type === "agent" ? "Persona" : "Skill"} rule for ${fileName}
|
|
655
|
+
---
|
|
656
|
+
# ${fileName} ${type === "agent" ? "Persona" : "Skill"}
|
|
657
|
+
|
|
658
|
+
${content}
|
|
659
|
+
|
|
660
|
+
## Completion Contract (Mandatory)
|
|
661
|
+
Every task completion MUST provide the following payload:
|
|
662
|
+
1. **Status**: ${COMPLETION_STATUS_TEXT}
|
|
663
|
+
2. **Scope reviewed**: Brief summary of what was analyzed.
|
|
664
|
+
3. **Findings**: Evidence-based discoveries.
|
|
665
|
+
4. **Files affected**: List of all concrete paths modified or created.
|
|
666
|
+
5. **Validation/evidence**: Commands run and their results.
|
|
667
|
+
6. **Risks/notes**: Any caveats or technical debt introduced.
|
|
668
|
+
7. **Required manager action**: Clear next step for the orchestrator.
|
|
669
|
+
8. **Can manager continue**: [yes/no]
|
|
670
|
+
`;
|
|
671
|
+
return {
|
|
672
|
+
content: hardenedContent,
|
|
673
|
+
name: ruleName
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* Deploys agents, skills, and root CLAUDE.md.
|
|
678
|
+
*/
|
|
679
|
+
async deploy(installRoot = ".ai-workflow") {
|
|
680
|
+
const rulesDir = path6.join(this.cwd, ".claude/rules");
|
|
681
|
+
await fs4.mkdir(rulesDir, { recursive: true });
|
|
682
|
+
const sourceAgentsDir = path6.join(this.cwd, installRoot, "opencode/agents");
|
|
683
|
+
const sourceSkillsDir = path6.join(this.cwd, installRoot, "opencode/skills");
|
|
684
|
+
const sourceCommandsDir = path6.join(this.cwd, installRoot, "opencode/commands");
|
|
685
|
+
const agentFiles = await fs4.readdir(sourceAgentsDir).catch(() => []);
|
|
686
|
+
for (const file of agentFiles) {
|
|
687
|
+
if (!file.endsWith(".md")) continue;
|
|
688
|
+
const transformed = await this.transformToRule(path6.join(sourceAgentsDir, file), "agent");
|
|
689
|
+
await fs4.writeFile(path6.join(rulesDir, `${transformed.name}.md`), transformed.content);
|
|
690
|
+
}
|
|
691
|
+
const skillFolders = await fs4.readdir(sourceSkillsDir).catch(() => []);
|
|
692
|
+
for (const folder of skillFolders) {
|
|
693
|
+
const skillSource = path6.join(sourceSkillsDir, folder, "SKILL.md");
|
|
694
|
+
if (await this.exists(skillSource)) {
|
|
695
|
+
const transformed = await this.transformToRule(skillSource, "skill");
|
|
696
|
+
await fs4.writeFile(path6.join(rulesDir, `${transformed.name}.md`), transformed.content);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
await this.deployRootClaudeMd(sourceCommandsDir);
|
|
700
|
+
}
|
|
701
|
+
async deployRootClaudeMd(sourceCommandsDir) {
|
|
702
|
+
const targetPath = path6.join(this.cwd, "CLAUDE.md");
|
|
703
|
+
const commandFiles = await fs4.readdir(sourceCommandsDir).catch(() => []);
|
|
704
|
+
let commandsDoc = "";
|
|
705
|
+
for (const file of commandFiles) {
|
|
706
|
+
if (!file.endsWith(".md")) continue;
|
|
707
|
+
const name = path6.basename(file, ".md");
|
|
708
|
+
commandsDoc += `- \`ai-workflow run --spec-path=...\` (Target: ${name})
|
|
709
|
+
`;
|
|
710
|
+
}
|
|
711
|
+
const instructions = `# CLAUDE.md - AI Workflow Kit Governance
|
|
712
|
+
|
|
713
|
+
This project uses the **AI Workflow Kit** (OpenCode-first) workflow.
|
|
714
|
+
|
|
715
|
+
## Mandate: Atlas Authority Protocol
|
|
716
|
+
All architectural changes must align with the specifications issued by the **Specification Authority** (./specs).
|
|
717
|
+
|
|
718
|
+
## Common Commands
|
|
719
|
+
${commandsDoc}
|
|
720
|
+
- \`ai-workflow doctor\`: Verify installation.
|
|
721
|
+
- \`ai-workflow collect-evidence\`: Generate EVIDENCE.json.
|
|
722
|
+
|
|
723
|
+
## Rules & Personas
|
|
724
|
+
Adhere to the specific rules and personas defined in \`.claude/rules/\`.
|
|
725
|
+
- Always follow the **Branch Gate** policy (never work on \`main\`).
|
|
726
|
+
- Always follow the **SDD Workflow** (Spec -> Plan -> PR -> Evidence).
|
|
727
|
+
|
|
728
|
+
## Completion Contract
|
|
729
|
+
Every task completion MUST follow the payload format defined in the individual rule files.
|
|
730
|
+
`;
|
|
731
|
+
await fs4.writeFile(targetPath, instructions);
|
|
732
|
+
}
|
|
733
|
+
async exists(p) {
|
|
734
|
+
try {
|
|
735
|
+
await fs4.access(p);
|
|
736
|
+
return true;
|
|
737
|
+
} catch {
|
|
738
|
+
return false;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
};
|
|
742
|
+
|
|
743
|
+
// src/adapters/platforms/codex.ts
|
|
744
|
+
import fs5 from "fs/promises";
|
|
745
|
+
import path7 from "path";
|
|
746
|
+
var CodexAdapter = class {
|
|
747
|
+
cwd;
|
|
748
|
+
constructor({ cwd }) {
|
|
749
|
+
this.cwd = cwd;
|
|
750
|
+
}
|
|
751
|
+
/**
|
|
752
|
+
* Transforms an OpenCode agent into a Codex prompt with completion contract.
|
|
753
|
+
*/
|
|
754
|
+
async transformAgent(filePath) {
|
|
755
|
+
const content = await fs5.readFile(filePath, "utf8");
|
|
756
|
+
const fileName = path7.basename(filePath, ".md");
|
|
757
|
+
const hardenedContent = `${content}
|
|
758
|
+
|
|
759
|
+
## Completion Contract (Mandatory)
|
|
760
|
+
Every task completion MUST provide the following payload:
|
|
761
|
+
1. **Status**: ${COMPLETION_STATUS_TEXT}
|
|
762
|
+
2. **Scope reviewed**: Brief summary of what was analyzed.
|
|
763
|
+
3. **Findings**: Evidence-based discoveries.
|
|
764
|
+
4. **Files affected**: List of all concrete paths modified or created.
|
|
765
|
+
5. **Validation/evidence**: Commands run and their results.
|
|
766
|
+
6. **Risks/notes**: Any caveats or technical debt introduced.
|
|
767
|
+
7. **Required manager action**: Clear next step for the orchestrator.
|
|
768
|
+
8. **Can manager continue**: [yes/no]
|
|
769
|
+
`;
|
|
770
|
+
return {
|
|
771
|
+
content: hardenedContent,
|
|
772
|
+
name: fileName
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
/**
|
|
776
|
+
* Deploys agents, skills, and commands to Codex-native directories.
|
|
777
|
+
* Mapping:
|
|
778
|
+
* - Agents -> .github/agents/
|
|
779
|
+
* - Skills -> .agents/skills/
|
|
780
|
+
* - Commands -> .codex/prompts/
|
|
781
|
+
*/
|
|
782
|
+
async deploy(installRoot = ".ai-workflow") {
|
|
783
|
+
const sourceAgentsDir = path7.join(this.cwd, installRoot, "opencode/agents");
|
|
784
|
+
const sourceSkillsDir = path7.join(this.cwd, installRoot, "opencode/skills");
|
|
785
|
+
const sourceCommandsDir = path7.join(this.cwd, installRoot, "opencode/commands");
|
|
786
|
+
const codexAgentsDir = path7.join(this.cwd, ".github/agents");
|
|
787
|
+
await fs5.mkdir(codexAgentsDir, { recursive: true });
|
|
788
|
+
const agentFiles = await fs5.readdir(sourceAgentsDir).catch(() => []);
|
|
789
|
+
for (const file of agentFiles) {
|
|
790
|
+
if (!file.endsWith(".md")) continue;
|
|
791
|
+
const transformed = await this.transformAgent(path7.join(sourceAgentsDir, file));
|
|
792
|
+
await fs5.writeFile(path7.join(codexAgentsDir, `${transformed.name}.md`), transformed.content);
|
|
793
|
+
}
|
|
794
|
+
const codexSkillsDir = path7.join(this.cwd, ".agents/skills");
|
|
795
|
+
await fs5.mkdir(codexSkillsDir, { recursive: true });
|
|
796
|
+
const skillFolders = await fs5.readdir(sourceSkillsDir).catch(() => []);
|
|
797
|
+
for (const folder of skillFolders) {
|
|
798
|
+
const skillSource = path7.join(sourceSkillsDir, folder, "SKILL.md");
|
|
799
|
+
if (await this.exists(skillSource)) {
|
|
800
|
+
const targetDir = path7.join(codexSkillsDir, folder);
|
|
801
|
+
await fs5.mkdir(targetDir, { recursive: true });
|
|
802
|
+
const content = await fs5.readFile(skillSource, "utf8");
|
|
803
|
+
await fs5.writeFile(path7.join(targetDir, "SKILL.md"), content);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
const codexPromptsDir = path7.join(this.cwd, ".codex/prompts");
|
|
807
|
+
await fs5.mkdir(codexPromptsDir, { recursive: true });
|
|
808
|
+
const commandFiles = await fs5.readdir(sourceCommandsDir).catch(() => []);
|
|
809
|
+
for (const file of commandFiles) {
|
|
810
|
+
if (!file.endsWith(".md") && !file.endsWith(".toml")) continue;
|
|
811
|
+
const targetFileName = file.endsWith(".toml") ? `${path7.basename(file, ".toml")}.md` : file;
|
|
812
|
+
const content = await fs5.readFile(path7.join(sourceCommandsDir, file), "utf8");
|
|
813
|
+
await fs5.writeFile(path7.join(codexPromptsDir, targetFileName), content);
|
|
814
|
+
}
|
|
815
|
+
const codexPoliciesDir = path7.join(this.cwd, ".agents", "docs", "policies");
|
|
816
|
+
await fs5.mkdir(codexPoliciesDir, { recursive: true });
|
|
817
|
+
const sourcePoliciesDir = path7.join(this.cwd, installRoot, "opencode", "docs", "policies");
|
|
818
|
+
const policyFiles = await fs5.readdir(sourcePoliciesDir).catch(() => []);
|
|
819
|
+
for (const file of policyFiles) {
|
|
820
|
+
if (!file.endsWith(".md")) continue;
|
|
821
|
+
const policyContent = await fs5.readFile(path7.join(sourcePoliciesDir, file), "utf8");
|
|
822
|
+
await fs5.writeFile(path7.join(codexPoliciesDir, file), policyContent);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
async exists(p) {
|
|
826
|
+
try {
|
|
827
|
+
await fs5.access(p);
|
|
828
|
+
return true;
|
|
829
|
+
} catch {
|
|
830
|
+
return false;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
};
|
|
834
|
+
|
|
835
|
+
// src/adapters/platforms/antigravity.ts
|
|
836
|
+
import fs6 from "fs/promises";
|
|
837
|
+
import path8 from "path";
|
|
838
|
+
import { pathToFileURL } from "url";
|
|
839
|
+
var PERSONA_MAPPING = {
|
|
840
|
+
"atlas": "Atlas",
|
|
841
|
+
"orion": "Orion",
|
|
842
|
+
"sage": "Sage",
|
|
843
|
+
"nexus": "Nexus",
|
|
844
|
+
"astra": "Astra",
|
|
845
|
+
"sage-validation": "Sage",
|
|
846
|
+
"orion-release": "Orion",
|
|
847
|
+
"orion-deploy": "Orion",
|
|
848
|
+
"nexus-discovery": "Nexus",
|
|
849
|
+
"phoenix": "Phoenix"
|
|
850
|
+
};
|
|
851
|
+
var AntigravityAdapter = class {
|
|
852
|
+
cwd;
|
|
853
|
+
constructor({ cwd }) {
|
|
854
|
+
this.cwd = cwd;
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
857
|
+
* Transforms an OpenCode agent or specialized skill file to an Antigravity Skill.
|
|
858
|
+
*/
|
|
859
|
+
async transform(filePath) {
|
|
860
|
+
const content = await fs6.readFile(filePath, "utf8");
|
|
861
|
+
const fileName = path8.basename(filePath, ".md") === "SKILL" ? path8.basename(path8.dirname(filePath)) : path8.basename(filePath, ".md");
|
|
862
|
+
const persona = PERSONA_MAPPING[fileName.toLowerCase()] || "Astra";
|
|
863
|
+
if (content.trim().startsWith("---")) {
|
|
864
|
+
return { content, name: fileName };
|
|
865
|
+
}
|
|
866
|
+
const descriptionMatch = content.match(/^# .*\n\n(.*)/m);
|
|
867
|
+
const description = descriptionMatch ? descriptionMatch[1].trim() : `Specialized agent for ${fileName}.`;
|
|
868
|
+
const skillContent = `---
|
|
869
|
+
name: ${fileName}
|
|
870
|
+
description: ${description}
|
|
871
|
+
---
|
|
872
|
+
# ${persona} Persona
|
|
873
|
+
|
|
874
|
+
${content}
|
|
875
|
+
|
|
876
|
+
## Completion Contract (Mandatory)
|
|
877
|
+
Every task completion MUST provide the following payload:
|
|
878
|
+
1. **Status**: ${COMPLETION_STATUS_TEXT}
|
|
879
|
+
2. **Scope reviewed**: Brief summary of what was analyzed.
|
|
880
|
+
3. **Findings**: Evidence-based discoveries.
|
|
881
|
+
4. **Files affected**: List of all concrete paths modified or created.
|
|
882
|
+
5. **Validation/evidence**: Commands run and their results.
|
|
883
|
+
6. **Risks/notes**: Any caveats or technical debt introduced.
|
|
884
|
+
7. **Required manager action**: Clear next step for the orchestrator.
|
|
885
|
+
8. **Can manager continue**: [yes/no]
|
|
886
|
+
`;
|
|
887
|
+
return {
|
|
888
|
+
content: skillContent,
|
|
889
|
+
name: fileName
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
/**
|
|
893
|
+
* Deploys transformed skills and agents to their respective directories.
|
|
894
|
+
* Also orchestrates the .antigravity/ native integration.
|
|
895
|
+
*/
|
|
896
|
+
async deploy(transformedItems, installRoot = ".ai-workflow") {
|
|
897
|
+
const agentsDir = path8.join(this.cwd, installRoot, "opencode/agents");
|
|
898
|
+
const skillsDir = path8.join(this.cwd, installRoot, "opencode/skills");
|
|
899
|
+
await fs6.mkdir(agentsDir, { recursive: true });
|
|
900
|
+
await fs6.mkdir(skillsDir, { recursive: true });
|
|
901
|
+
for (const item of transformedItems) {
|
|
902
|
+
const isAgent = PERSONA_MAPPING[item.name.toLowerCase()];
|
|
903
|
+
const targetDir = isAgent ? agentsDir : path8.join(skillsDir, item.name);
|
|
904
|
+
const fileName = isAgent ? `${item.name}.md` : "SKILL.md";
|
|
905
|
+
const absoluteTargetDir = isAgent ? agentsDir : targetDir;
|
|
906
|
+
await fs6.mkdir(absoluteTargetDir, { recursive: true });
|
|
907
|
+
await fs6.writeFile(path8.join(absoluteTargetDir, fileName), item.content);
|
|
908
|
+
}
|
|
909
|
+
await this.deployNativeExtensions(installRoot);
|
|
910
|
+
await this.deployInstructions();
|
|
911
|
+
}
|
|
912
|
+
/**
|
|
913
|
+
* Orchestrates the .agents/ directory with copies of .ai-workflow/ assets.
|
|
914
|
+
* Using hard copies instead of symlinks ensures native discovery across all platforms.
|
|
915
|
+
*/
|
|
916
|
+
async deployNativeExtensions(installRoot = ".ai-workflow") {
|
|
917
|
+
const antigravityDir = path8.join(this.cwd, ".agents");
|
|
918
|
+
const antigravityAgents = path8.join(antigravityDir, "agents");
|
|
919
|
+
const antigravitySkills = path8.join(antigravityDir, "skills");
|
|
920
|
+
const antigravityCommands = path8.join(antigravityDir, "commands");
|
|
921
|
+
await fs6.mkdir(antigravityAgents, { recursive: true });
|
|
922
|
+
await fs6.mkdir(antigravitySkills, { recursive: true });
|
|
923
|
+
await fs6.mkdir(antigravityCommands, { recursive: true });
|
|
924
|
+
const getUrls = (targetPath) => {
|
|
925
|
+
const linuxUrl = pathToFileURL(targetPath).href;
|
|
926
|
+
const urls = [linuxUrl];
|
|
927
|
+
if (process.platform === "linux" && process.env.WSL_DISTRO_NAME) {
|
|
928
|
+
const distro = process.env.WSL_DISTRO_NAME;
|
|
929
|
+
const normalizedPath = targetPath.replaceAll("\\", "/");
|
|
930
|
+
urls.push(`file://wsl.localhost/${distro}${normalizedPath}`);
|
|
931
|
+
urls.push(`file:///wsl.localhost/${distro}${normalizedPath}`);
|
|
932
|
+
}
|
|
933
|
+
return urls.join("\n");
|
|
934
|
+
};
|
|
935
|
+
const toolNames = [
|
|
936
|
+
"send_message",
|
|
937
|
+
"find_by_name",
|
|
938
|
+
"grep_search",
|
|
939
|
+
"view_file",
|
|
940
|
+
"list_dir",
|
|
941
|
+
"read_url_content",
|
|
942
|
+
"search_web",
|
|
943
|
+
"schedule",
|
|
944
|
+
"multi_replace_file_content",
|
|
945
|
+
"replace_file_content",
|
|
946
|
+
"write_to_file",
|
|
947
|
+
"run_command",
|
|
948
|
+
"manage_task",
|
|
949
|
+
"define_subagent",
|
|
950
|
+
"invoke_subagent",
|
|
951
|
+
"manage_subagents",
|
|
952
|
+
"call_mcp_tool"
|
|
953
|
+
];
|
|
954
|
+
const systemPromptConfig = {
|
|
955
|
+
"includeSections": [
|
|
956
|
+
"user_information",
|
|
957
|
+
"mcp_servers",
|
|
958
|
+
"skills",
|
|
959
|
+
"subagent_reminder",
|
|
960
|
+
"messaging",
|
|
961
|
+
"artifacts",
|
|
962
|
+
"user_rules"
|
|
963
|
+
]
|
|
964
|
+
};
|
|
965
|
+
const sourceAgentsDir = path8.join(this.cwd, installRoot, "opencode/agents");
|
|
966
|
+
const agentFiles = await fs6.readdir(sourceAgentsDir).catch(() => []);
|
|
967
|
+
const AGENTS_INFO = {
|
|
968
|
+
"atlas": { name: "Atlas", description: "Router and workflow coordinator for AI Workflow Kit" },
|
|
969
|
+
"nexus": { name: "Nexus", description: "Discovery, requirement, scope, and specification owner" },
|
|
970
|
+
"orion": { name: "Orion", description: "Planning, PR sequencing, release, and deployment strategy owner" },
|
|
971
|
+
"astra": { name: "Astra", description: "Implementation owner for incremental, scoped changes" },
|
|
972
|
+
"sage": { name: "Sage", description: "Audit, validation, evidence, and quality gate owner" },
|
|
973
|
+
"phoenix": { name: "Phoenix", description: "Remediation and regression recovery owner" }
|
|
974
|
+
};
|
|
975
|
+
for (const file of agentFiles) {
|
|
976
|
+
if (!file.endsWith(".md")) continue;
|
|
977
|
+
const lowercaseName = path8.basename(file, ".md").toLowerCase();
|
|
978
|
+
const info = AGENTS_INFO[lowercaseName];
|
|
979
|
+
if (!info) continue;
|
|
980
|
+
const agentFolder = path8.join(antigravityAgents, lowercaseName);
|
|
981
|
+
await fs6.mkdir(agentFolder, { recursive: true });
|
|
982
|
+
const content = await fs6.readFile(path8.join(sourceAgentsDir, file), "utf8");
|
|
983
|
+
const mdTarget = path8.join(agentFolder, `${lowercaseName}.md`);
|
|
984
|
+
await fs6.writeFile(mdTarget, content);
|
|
985
|
+
const agentJson = {
|
|
986
|
+
name: lowercaseName,
|
|
987
|
+
description: info.description,
|
|
988
|
+
hidden: false,
|
|
989
|
+
config: {
|
|
990
|
+
customAgent: {
|
|
991
|
+
systemPromptSections: [
|
|
992
|
+
{
|
|
993
|
+
title: "Agent System Instructions",
|
|
994
|
+
content: `You are ${info.name}, the ${lowercaseName === "astra" ? "implementation" : lowercaseName} owner.
|
|
995
|
+
|
|
996
|
+
Follow the contract instructions in:
|
|
997
|
+
${getUrls(mdTarget)}`
|
|
998
|
+
}
|
|
999
|
+
],
|
|
1000
|
+
toolNames,
|
|
1001
|
+
systemPromptConfig
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
};
|
|
1005
|
+
await fs6.writeFile(path8.join(agentFolder, "agent.json"), JSON.stringify(agentJson, null, 2));
|
|
1006
|
+
}
|
|
1007
|
+
const SUBAGENTS_INFO = {
|
|
1008
|
+
"Architecture-Specialist": { description: "Select the simplest architecture that safely satisfies current requirements", skill: "architecture" },
|
|
1009
|
+
"Backend-Engineer": { description: "Support safe PHP/Node/Python backend changes for APIs and persistence", skill: "backend-development" },
|
|
1010
|
+
"Deployment-Specialist": { description: "Plan, review, or validate deployment, release, and production-readiness", skill: "deployment" },
|
|
1011
|
+
"Design-Specialist": { description: "Apply practical design principles to keep solutions simple and maintainable", skill: "design-principles" },
|
|
1012
|
+
"Docs-Engineer": { description: "Write or improve project documentation, READMEs, guides, and prompts", skill: "documentation" },
|
|
1013
|
+
"Frontend-Engineer": { description: "Implement or review frontend changes involving UI, state, routing, and accessibility", skill: "frontend-development" },
|
|
1014
|
+
"Full-Stack-Engineer": { description: "Coordinate frontend and backend changes as one coherent, testable increment", skill: "full-stack-development" },
|
|
1015
|
+
"Token-Economist": { description: "Reduce context size while preserving information required for safe execution", skill: "optimize-tokens" },
|
|
1016
|
+
"PR-Manager": { description: "Plan, implement, review, or validate small pull requests while preserving scope", skill: "pr-workflow" },
|
|
1017
|
+
"Discovery-Analyst": { description: "Clarify ambiguous requests into actionable problem statements and success outcomes", skill: "product-discovery" },
|
|
1018
|
+
"Product-Planner": { description: "Turn clarified needs into scoped requirements with acceptance-ready outcomes", skill: "product-planning" },
|
|
1019
|
+
"Memory-Guardian": { description: "Manage discoverable project memory for durable decisions and recurring corrections", skill: "project-memory" },
|
|
1020
|
+
"Prompt-Engineer": { description: "Expert in creating and refining prompts for AI agents and workflows", skill: "prompt-engineer" },
|
|
1021
|
+
"QA-Engineer": { description: "Design, review, or improve tests, acceptance criteria, and validation evidence", skill: "qa-workflow" },
|
|
1022
|
+
"Refactoring-Specialist": { description: "Improve code/document structure safely while preserving behavior", skill: "refactoring" },
|
|
1023
|
+
"Release-Specialist": { description: "Prepare release readiness decisions with explicit gates and evidence", skill: "release-workflow" },
|
|
1024
|
+
"SDD-Specialist": { description: "Convert approved requirements into explicit, testable specification artifacts", skill: "spec-driven-development" },
|
|
1025
|
+
"Technical-Leader": { description: "Make technical decisions, review architecture, and identify trade-offs", skill: "technical-leadership" },
|
|
1026
|
+
"UI-UX-Engineer": { description: "Improve usability and interface clarity without introducing unnecessary complexity", skill: "ui-ux-design" }
|
|
1027
|
+
};
|
|
1028
|
+
for (const [name, subInfo] of Object.entries(SUBAGENTS_INFO)) {
|
|
1029
|
+
const lowercaseSubagentName = name.toLowerCase();
|
|
1030
|
+
const subagentFolder = path8.join(antigravityAgents, lowercaseSubagentName);
|
|
1031
|
+
await fs6.mkdir(subagentFolder, { recursive: true });
|
|
1032
|
+
const skillPath = path8.join(antigravitySkills, subInfo.skill, "SKILL.md");
|
|
1033
|
+
const subagentJson = {
|
|
1034
|
+
name: lowercaseSubagentName,
|
|
1035
|
+
description: subInfo.description,
|
|
1036
|
+
hidden: false,
|
|
1037
|
+
config: {
|
|
1038
|
+
customAgent: {
|
|
1039
|
+
systemPromptSections: [
|
|
1040
|
+
{
|
|
1041
|
+
title: "Agent System Instructions",
|
|
1042
|
+
content: `You are ${name}.
|
|
1043
|
+
|
|
1044
|
+
Follow the contract instructions in:
|
|
1045
|
+
${getUrls(skillPath)}`
|
|
1046
|
+
}
|
|
1047
|
+
],
|
|
1048
|
+
toolNames,
|
|
1049
|
+
systemPromptConfig
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
};
|
|
1053
|
+
await fs6.writeFile(path8.join(subagentFolder, "agent.json"), JSON.stringify(subagentJson, null, 2));
|
|
1054
|
+
}
|
|
1055
|
+
const sourceSkillsDir = path8.join(this.cwd, installRoot, "opencode/skills");
|
|
1056
|
+
const skillFolders = await fs6.readdir(sourceSkillsDir).catch(() => []);
|
|
1057
|
+
for (const folder of skillFolders) {
|
|
1058
|
+
const skillSource = path8.join(sourceSkillsDir, folder, "SKILL.md");
|
|
1059
|
+
if (await this.exists(skillSource)) {
|
|
1060
|
+
await fs6.mkdir(path8.join(antigravitySkills, folder), { recursive: true });
|
|
1061
|
+
const content = await fs6.readFile(skillSource, "utf8");
|
|
1062
|
+
await fs6.writeFile(path8.join(antigravitySkills, folder, "SKILL.md"), content);
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
const sourceCommandsDir = path8.join(this.cwd, installRoot, "opencode/commands");
|
|
1066
|
+
if (await this.exists(sourceCommandsDir)) {
|
|
1067
|
+
const commandFiles = await fs6.readdir(sourceCommandsDir).catch(() => []);
|
|
1068
|
+
for (const file of commandFiles) {
|
|
1069
|
+
if (!file.endsWith(".md") && !file.endsWith(".toml")) continue;
|
|
1070
|
+
const content = await fs6.readFile(path8.join(sourceCommandsDir, file), "utf8");
|
|
1071
|
+
await fs6.writeFile(path8.join(antigravityCommands, file), content);
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
const sourcePoliciesDir = path8.join(this.cwd, installRoot, "opencode", "docs", "policies");
|
|
1075
|
+
const antigravityPolicies = path8.join(antigravityDir, "docs", "policies");
|
|
1076
|
+
if (await this.exists(sourcePoliciesDir)) {
|
|
1077
|
+
await fs6.mkdir(antigravityPolicies, { recursive: true });
|
|
1078
|
+
const policyFiles = await fs6.readdir(sourcePoliciesDir).catch(() => []);
|
|
1079
|
+
for (const file of policyFiles) {
|
|
1080
|
+
if (!file.endsWith(".md")) continue;
|
|
1081
|
+
const content = await fs6.readFile(path8.join(sourcePoliciesDir, file), "utf8");
|
|
1082
|
+
await fs6.writeFile(path8.join(antigravityPolicies, file), content);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
const settingsPath = path8.join(antigravityDir, "settings.json");
|
|
1086
|
+
if (!await this.exists(settingsPath)) {
|
|
1087
|
+
const settings = {
|
|
1088
|
+
model: { name: "auto" },
|
|
1089
|
+
ui: { theme: "Ayu" }
|
|
1090
|
+
};
|
|
1091
|
+
await fs6.writeFile(settingsPath, JSON.stringify(settings, null, 2));
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
async exists(p) {
|
|
1095
|
+
try {
|
|
1096
|
+
await fs6.access(p);
|
|
1097
|
+
return true;
|
|
1098
|
+
} catch {
|
|
1099
|
+
return false;
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
/**
|
|
1103
|
+
* Deploys the main ANTIGRAVITY.md and .antigravityignore instructions to the project root.
|
|
1104
|
+
*/
|
|
1105
|
+
async deployInstructions() {
|
|
1106
|
+
const targetPath = path8.join(this.cwd, "ANTIGRAVITY.md");
|
|
1107
|
+
const ignorePath = path8.join(this.cwd, ".antigravityignore");
|
|
1108
|
+
try {
|
|
1109
|
+
await fs6.access(targetPath);
|
|
1110
|
+
} catch {
|
|
1111
|
+
const instructions = `# AI Workflow Kit - Engineering Governance
|
|
1112
|
+
|
|
1113
|
+
## Mandate: Atlas Authority Protocol
|
|
1114
|
+
All architectural changes must align with the specifications issued by the **Specification Authority** (./specs).
|
|
1115
|
+
|
|
1116
|
+
## Ternary Architecture
|
|
1117
|
+
- **Root:** Orchestration and config.
|
|
1118
|
+
- **.ai-workflow/:** Implementation (The Muscle).
|
|
1119
|
+
- **./specs:** Specifications (The Brain).
|
|
1120
|
+
|
|
1121
|
+
## Branch Gates
|
|
1122
|
+
- **main Branch:** READ-ONLY.
|
|
1123
|
+
- **Validation:** Merges require successful observed validation; full/release workflows may persist EVIDENCE.json.
|
|
1124
|
+
|
|
1125
|
+
## Primary Agents
|
|
1126
|
+
- **Atlas (Orchestrator)**
|
|
1127
|
+
- **Orion (Strategist)**
|
|
1128
|
+
- **Sage (Auditor)**
|
|
1129
|
+
- **Nexus (Spec Architect)**
|
|
1130
|
+
- **Astra (Developer)**
|
|
1131
|
+
- **Phoenix (Healer)**
|
|
1132
|
+
|
|
1133
|
+
## Operational Preferences
|
|
1134
|
+
- Use \`token-economy\` and \`minimal-context\` skills.
|
|
1135
|
+
- **Native Discovery**: This project uses workspace-specific extensions in \`.agents/\`.
|
|
1136
|
+
- **Workflow Entry**: Use \`/atlas\` to begin any task.
|
|
1137
|
+
`;
|
|
1138
|
+
await fs6.writeFile(targetPath, instructions);
|
|
1139
|
+
}
|
|
1140
|
+
try {
|
|
1141
|
+
await fs6.access(ignorePath);
|
|
1142
|
+
} catch {
|
|
1143
|
+
const ignoreContent = `.ai-workflow/
|
|
1144
|
+
.ai-workflow-backups/
|
|
1145
|
+
node_modules/
|
|
1146
|
+
.git/
|
|
1147
|
+
EVIDENCE.json
|
|
1148
|
+
*.tgz
|
|
1149
|
+
*.zip
|
|
1150
|
+
*.tar
|
|
1151
|
+
.agents/tmp/
|
|
1152
|
+
.agents/history/
|
|
1153
|
+
`;
|
|
1154
|
+
await fs6.writeFile(ignorePath, ignoreContent);
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
};
|
|
1158
|
+
|
|
1159
|
+
// src/cli/commands/init.ts
|
|
1160
|
+
function summarize(actions) {
|
|
1161
|
+
const createCount = actions.filter((a) => a.type === "create").length;
|
|
1162
|
+
const conflictCount = actions.filter((a) => a.type === "conflict").length;
|
|
1163
|
+
return { createCount, conflictCount };
|
|
1164
|
+
}
|
|
1165
|
+
function printPlan(actions) {
|
|
1166
|
+
for (const action of actions) {
|
|
1167
|
+
console.log(`- ${action.type.padEnd(8)} ${action.relativePath}`);
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
function toIgnoreEntries(linkPath) {
|
|
1171
|
+
if (linkPath === "README.workflow.md") {
|
|
1172
|
+
return [linkPath];
|
|
1173
|
+
}
|
|
1174
|
+
const normalized = linkPath.endsWith("/") ? linkPath.slice(0, -1) : linkPath;
|
|
1175
|
+
return [normalized, `${normalized}/`];
|
|
1176
|
+
}
|
|
1177
|
+
function buildGitignoreBlock(entries) {
|
|
1178
|
+
const unique = Array.from(new Set(entries));
|
|
1179
|
+
const sorted = unique.sort((a, b) => a.localeCompare(b));
|
|
1180
|
+
return [
|
|
1181
|
+
"# BEGIN AI WORKFLOW KIT",
|
|
1182
|
+
"# AI Workflow Kit generated files",
|
|
1183
|
+
...sorted,
|
|
1184
|
+
"# END AI WORKFLOW KIT"
|
|
1185
|
+
].join("\n");
|
|
1186
|
+
}
|
|
1187
|
+
async function upsertGitignoreBlock(cwd, entries) {
|
|
1188
|
+
const gitignorePath = path9.join(cwd, ".gitignore");
|
|
1189
|
+
const block = buildGitignoreBlock(entries);
|
|
1190
|
+
const beginMarker = "# BEGIN AI WORKFLOW KIT";
|
|
1191
|
+
const endMarker = "# END AI WORKFLOW KIT";
|
|
1192
|
+
const hasGitignore = await exists(gitignorePath);
|
|
1193
|
+
if (!hasGitignore) {
|
|
1194
|
+
await writeFileSafe(gitignorePath, `${block}
|
|
1195
|
+
`);
|
|
1196
|
+
return "created";
|
|
1197
|
+
}
|
|
1198
|
+
const current = await fs7.readFile(gitignorePath, "utf8");
|
|
1199
|
+
const beginIndex = current.indexOf(beginMarker);
|
|
1200
|
+
const endIndex = current.indexOf(endMarker);
|
|
1201
|
+
if (beginIndex >= 0 && endIndex > beginIndex) {
|
|
1202
|
+
const before = current.slice(0, beginIndex).replace(/\n?# AI Workflow Kit generated files\n?$/, "\n").replace(/[ \t]+$/gm, "").replace(/\n*$/, "\n");
|
|
1203
|
+
const afterStart = endIndex + endMarker.length;
|
|
1204
|
+
const after = current.slice(afterStart).replace(/^\n*/, "\n");
|
|
1205
|
+
const next2 = `${before}${block}${after}`;
|
|
1206
|
+
if (next2 !== current) {
|
|
1207
|
+
await writeFileSafe(gitignorePath, next2);
|
|
1208
|
+
return "updated";
|
|
1209
|
+
}
|
|
1210
|
+
return "unchanged";
|
|
1211
|
+
}
|
|
1212
|
+
const separator = current.endsWith("\n") ? "\n" : "\n\n";
|
|
1213
|
+
const next = `${current}${separator}${block}
|
|
1214
|
+
`;
|
|
1215
|
+
await writeFileSafe(gitignorePath, next);
|
|
1216
|
+
return "updated";
|
|
1217
|
+
}
|
|
1218
|
+
async function injectScripts(cwd) {
|
|
1219
|
+
const pkgPath = path9.join(cwd, "package.json");
|
|
1220
|
+
if (!await exists(pkgPath)) return false;
|
|
1221
|
+
try {
|
|
1222
|
+
const pkg = await readJson(pkgPath);
|
|
1223
|
+
if (!pkg.scripts) pkg.scripts = {};
|
|
1224
|
+
if (!pkg.scripts.validate) {
|
|
1225
|
+
pkg.scripts.validate = "ai-workflow collect-evidence";
|
|
1226
|
+
await writeFileSafe(pkgPath, `${JSON.stringify(pkg, null, 2)}
|
|
1227
|
+
`);
|
|
1228
|
+
return true;
|
|
1229
|
+
}
|
|
1230
|
+
} catch {
|
|
1231
|
+
}
|
|
1232
|
+
return false;
|
|
1233
|
+
}
|
|
1234
|
+
async function verifySelfExecutionGuard(cwd, devMode) {
|
|
1235
|
+
if (!devMode) {
|
|
1236
|
+
try {
|
|
1237
|
+
const pkgPath = path9.join(cwd, "package.json");
|
|
1238
|
+
if (await exists(pkgPath)) {
|
|
1239
|
+
const pkg = await readJson(pkgPath);
|
|
1240
|
+
if (pkg.name === "@williambeto/ai-workflow") {
|
|
1241
|
+
console.error("\nFATAL: Cannot run 'ai-workflow init' inside its own development repository.");
|
|
1242
|
+
console.error("This would corrupt the development environment. Aborting.");
|
|
1243
|
+
console.error("Use the --dev-mode flag if you are developing the kit itself and need to test the init command.\n");
|
|
1244
|
+
process.exit(1);
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
} catch {
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
function resolveProfile(profile) {
|
|
1252
|
+
const selectedProfile = profile || "standard";
|
|
1253
|
+
if (!isValidProfile(selectedProfile)) {
|
|
1254
|
+
console.error(`Invalid profile: "${selectedProfile}". Valid profiles: standard, full`);
|
|
1255
|
+
process.exit(1);
|
|
1256
|
+
}
|
|
1257
|
+
return selectedProfile;
|
|
1258
|
+
}
|
|
1259
|
+
async function writeInstallFiles(cwd, actions, selectedProfile, force, noOverwrite, backupRoot = ".ai-workflow-backups") {
|
|
1260
|
+
const skipped = [];
|
|
1261
|
+
const backups = [];
|
|
1262
|
+
for (const action of actions) {
|
|
1263
|
+
if (action.type === "conflict" && (noOverwrite || !force)) {
|
|
1264
|
+
skipped.push(action.relativePath);
|
|
1265
|
+
continue;
|
|
1266
|
+
}
|
|
1267
|
+
if (action.type === "conflict" && force) {
|
|
1268
|
+
const backupPath = await createManagedBackup(action.absolutePath, {
|
|
1269
|
+
cwd,
|
|
1270
|
+
backupRoot,
|
|
1271
|
+
maxPerFile: 20
|
|
1272
|
+
});
|
|
1273
|
+
backups.push(backupPath);
|
|
1274
|
+
}
|
|
1275
|
+
const content = action.relativePath === ".ai-workflow/config.json" ? `${JSON.stringify(buildAiWorkflowConfig({ profile: selectedProfile }), null, 2)}
|
|
1276
|
+
` : action.content;
|
|
1277
|
+
await writeFileSafe(action.absolutePath, content || "");
|
|
1278
|
+
}
|
|
1279
|
+
return { skipped, backups };
|
|
1280
|
+
}
|
|
1281
|
+
async function writeSymlinks(cwd, linkEntries, force, backupRoot = ".ai-workflow-backups") {
|
|
1282
|
+
const linkBackups = [];
|
|
1283
|
+
const linkCreated = [];
|
|
1284
|
+
for (const { linkPath, targetPath } of linkEntries) {
|
|
1285
|
+
const absoluteLinkPath = path9.join(cwd, linkPath);
|
|
1286
|
+
const absoluteTargetPath = path9.join(cwd, targetPath);
|
|
1287
|
+
if (!await exists(absoluteTargetPath)) {
|
|
1288
|
+
throw new Error(`missing link target: ${targetPath}`);
|
|
1289
|
+
}
|
|
1290
|
+
if (await isSymlinkTo(absoluteLinkPath, absoluteTargetPath)) {
|
|
1291
|
+
continue;
|
|
1292
|
+
}
|
|
1293
|
+
if (await exists(absoluteLinkPath)) {
|
|
1294
|
+
if (!force) {
|
|
1295
|
+
throw new Error(
|
|
1296
|
+
`cannot create symlink '${linkPath}' because path already exists. Re-run with --force to backup and replace. If symlink creation fails, see troubleshooting in docs/npm-consumer-quickstart.md.`
|
|
1297
|
+
);
|
|
1298
|
+
}
|
|
1299
|
+
const backupPath = await createManagedPathBackup(absoluteLinkPath, {
|
|
1300
|
+
cwd,
|
|
1301
|
+
backupRoot,
|
|
1302
|
+
maxPerFile: 20
|
|
1303
|
+
});
|
|
1304
|
+
linkBackups.push(backupPath);
|
|
1305
|
+
await fs7.rm(absoluteLinkPath, { recursive: true, force: true });
|
|
1306
|
+
}
|
|
1307
|
+
const stat = await fs7.lstat(absoluteTargetPath);
|
|
1308
|
+
const type = process.platform === "win32" ? stat.isDirectory() ? "junction" : "file" : stat.isDirectory() ? "dir" : "file";
|
|
1309
|
+
const linkTarget = process.platform === "win32" ? absoluteTargetPath : path9.relative(path9.dirname(absoluteLinkPath), absoluteTargetPath);
|
|
1310
|
+
try {
|
|
1311
|
+
await fs7.mkdir(path9.dirname(absoluteLinkPath), { recursive: true });
|
|
1312
|
+
await fs7.symlink(linkTarget, absoluteLinkPath, type);
|
|
1313
|
+
} catch (error) {
|
|
1314
|
+
throw new Error(
|
|
1315
|
+
`failed to create symlink '${linkPath}' -> '${targetPath}' (${error.message}). For Windows, enable Developer Mode or run terminal as Administrator. For Linux/macOS, verify write permissions in project root.`
|
|
1316
|
+
);
|
|
1317
|
+
}
|
|
1318
|
+
linkCreated.push(linkPath);
|
|
1319
|
+
}
|
|
1320
|
+
return { linkBackups, linkCreated };
|
|
1321
|
+
}
|
|
1322
|
+
async function executePlatformMigrations(cwd, installRoot, platforms, claude, codex, antigravity) {
|
|
1323
|
+
if (platforms.length === 0) return;
|
|
1324
|
+
console.log("ai-workflow: migrating to native platforms...");
|
|
1325
|
+
const agentsDir = path9.join(cwd, installRoot, "opencode/agents");
|
|
1326
|
+
const skillsDir = path9.join(cwd, installRoot, "opencode/skills");
|
|
1327
|
+
const agentFiles = (await fs7.readdir(agentsDir).catch(() => [])).filter((f) => f.endsWith(".md")).map((f) => path9.join(agentsDir, f));
|
|
1328
|
+
if (claude) {
|
|
1329
|
+
const adapter = new ClaudeAdapter({ cwd });
|
|
1330
|
+
await adapter.deploy(installRoot);
|
|
1331
|
+
console.log("- claude: deployed AI Workflow Kit agents to CLAUDE.md and .claude/rules/ (native discovery enabled)");
|
|
1332
|
+
}
|
|
1333
|
+
if (codex) {
|
|
1334
|
+
const adapter = new CodexAdapter({ cwd });
|
|
1335
|
+
await adapter.deploy(installRoot);
|
|
1336
|
+
console.log("- codex: deployed AI Workflow Kit agents to .agents/, .codex/, and .github/ (native discovery enabled)");
|
|
1337
|
+
}
|
|
1338
|
+
if (antigravity) {
|
|
1339
|
+
const adapter = new AntigravityAdapter({ cwd });
|
|
1340
|
+
const skillFolders = await fs7.readdir(skillsDir).catch(() => []);
|
|
1341
|
+
const skillFiles = [];
|
|
1342
|
+
for (const folder of skillFolders) {
|
|
1343
|
+
const skillPath = path9.join(skillsDir, folder, "SKILL.md");
|
|
1344
|
+
if (await exists(skillPath)) {
|
|
1345
|
+
skillFiles.push(skillPath);
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
const allFiles = [...agentFiles, ...skillFiles];
|
|
1349
|
+
const transformed = await Promise.all(allFiles.map((f) => adapter.transform(f)));
|
|
1350
|
+
await adapter.deploy(transformed, installRoot);
|
|
1351
|
+
console.log(`- antigravity: deployed AI Workflow Kit agents to .agents/ (native discovery enabled)`);
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
function printInstallationSummary({
|
|
1355
|
+
selectedProfile,
|
|
1356
|
+
opencodeResult,
|
|
1357
|
+
gitignoreResult,
|
|
1358
|
+
scriptInjected,
|
|
1359
|
+
linkCreated,
|
|
1360
|
+
backups,
|
|
1361
|
+
linkBackups,
|
|
1362
|
+
skipped,
|
|
1363
|
+
noInstall
|
|
1364
|
+
}) {
|
|
1365
|
+
console.log("Installation complete.");
|
|
1366
|
+
console.log(`- profile: ${selectedProfile}`);
|
|
1367
|
+
console.log(`- opencode.jsonc: ${opencodeResult.reason}`);
|
|
1368
|
+
console.log(`- .gitignore: ${gitignoreResult}`);
|
|
1369
|
+
console.log(`- scripts: ${scriptInjected ? "injected 'validate'" : "already-up-to-date"}`);
|
|
1370
|
+
console.log(`- symlink layout: ${linkCreated.length > 0 ? `created ${linkCreated.length}` : "already-up-to-date"}`);
|
|
1371
|
+
if (backups.length > 0) {
|
|
1372
|
+
console.log(`- backups created: ${backups.length}`);
|
|
1373
|
+
for (const filePath of backups) {
|
|
1374
|
+
console.log(` ${filePath}`);
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
if (linkBackups.length > 0) {
|
|
1378
|
+
console.log(`- symlink backups created: ${linkBackups.length}`);
|
|
1379
|
+
for (const filePath of linkBackups) {
|
|
1380
|
+
console.log(` ${filePath}`);
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
if (skipped.length > 0) {
|
|
1384
|
+
console.log(`- conflicts skipped: ${skipped.length}`);
|
|
1385
|
+
for (const relativePath of skipped) {
|
|
1386
|
+
console.log(` ${relativePath}`);
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
console.log(`- dependency install: ${noInstall ? "skipped (--no-install)" : "not managed in this step"}`);
|
|
1390
|
+
console.log("Next steps:");
|
|
1391
|
+
console.log(" npx @williambeto/ai-workflow doctor");
|
|
1392
|
+
}
|
|
1393
|
+
async function runInit({
|
|
1394
|
+
cwd,
|
|
1395
|
+
yes,
|
|
1396
|
+
force,
|
|
1397
|
+
dryRun,
|
|
1398
|
+
noInstall,
|
|
1399
|
+
noOverwrite,
|
|
1400
|
+
claude,
|
|
1401
|
+
codex,
|
|
1402
|
+
antigravity,
|
|
1403
|
+
"dev-mode": devMode,
|
|
1404
|
+
profile
|
|
1405
|
+
}) {
|
|
1406
|
+
await verifySelfExecutionGuard(cwd, devMode);
|
|
1407
|
+
const selectedProfile = resolveProfile(profile);
|
|
1408
|
+
const installRoot = ".ai-workflow";
|
|
1409
|
+
const backupRoot = ".ai-workflow-backups";
|
|
1410
|
+
const platforms = [];
|
|
1411
|
+
if (claude) platforms.push("claude");
|
|
1412
|
+
if (codex) platforms.push("codex");
|
|
1413
|
+
if (antigravity) platforms.push("antigravity");
|
|
1414
|
+
console.log(`ai-workflow: initializing in ${cwd}`);
|
|
1415
|
+
console.log(`Profile: ${selectedProfile}`);
|
|
1416
|
+
if (platforms.length > 0) {
|
|
1417
|
+
console.log(`Platforms: ${platforms.join(", ")}`);
|
|
1418
|
+
}
|
|
1419
|
+
const actions = await createInstallPlan({ cwd, exists, profile: selectedProfile });
|
|
1420
|
+
const linkEntries = buildSymlinkEntries({ templateFiles: getTemplateFiles(selectedProfile), installRoot });
|
|
1421
|
+
const { createCount, conflictCount } = summarize(actions);
|
|
1422
|
+
console.log(`Plan: ${createCount} create, ${conflictCount} conflict`);
|
|
1423
|
+
printPlan(actions);
|
|
1424
|
+
if (!yes && conflictCount > 0 && !force && !noOverwrite) {
|
|
1425
|
+
throw new Error(
|
|
1426
|
+
"conflicts detected. Re-run with --force to replace managed files or resolve conflicts manually"
|
|
1427
|
+
);
|
|
1428
|
+
}
|
|
1429
|
+
if (dryRun) {
|
|
1430
|
+
console.log("Dry-run enabled. No files were written.");
|
|
1431
|
+
if (platforms.length > 0) {
|
|
1432
|
+
console.log("ai-workflow: (dry-run) platform migration would be executed after file writing.");
|
|
1433
|
+
}
|
|
1434
|
+
return;
|
|
1435
|
+
}
|
|
1436
|
+
const { skipped, backups } = await writeInstallFiles(cwd, actions, selectedProfile, force, noOverwrite, backupRoot);
|
|
1437
|
+
const { linkBackups, linkCreated } = await writeSymlinks(cwd, linkEntries, force, backupRoot);
|
|
1438
|
+
await setupInternalSymlinks(cwd, installRoot);
|
|
1439
|
+
const opencodeResult = await mergeOpencodeConfig(cwd, { force, backupRoot });
|
|
1440
|
+
const generatedIgnoreEntries = [
|
|
1441
|
+
"node_modules/",
|
|
1442
|
+
".ai-workflow/",
|
|
1443
|
+
".ai-workflow-backups/",
|
|
1444
|
+
"opencode.jsonc.backup.*",
|
|
1445
|
+
"EVIDENCE.json",
|
|
1446
|
+
"*.tgz",
|
|
1447
|
+
...linkEntries.flatMap((entry) => toIgnoreEntries(entry.linkPath))
|
|
1448
|
+
];
|
|
1449
|
+
if (codex) {
|
|
1450
|
+
generatedIgnoreEntries.push(".agents/", ".codex/");
|
|
1451
|
+
}
|
|
1452
|
+
if (claude) {
|
|
1453
|
+
generatedIgnoreEntries.push(".claude/");
|
|
1454
|
+
}
|
|
1455
|
+
if (antigravity) {
|
|
1456
|
+
generatedIgnoreEntries.push(".agents/");
|
|
1457
|
+
}
|
|
1458
|
+
const gitignoreResult = await upsertGitignoreBlock(cwd, generatedIgnoreEntries);
|
|
1459
|
+
const scriptInjected = await injectScripts(cwd);
|
|
1460
|
+
await executePlatformMigrations(cwd, installRoot, platforms, claude, codex, antigravity);
|
|
1461
|
+
printInstallationSummary({
|
|
1462
|
+
selectedProfile,
|
|
1463
|
+
opencodeResult,
|
|
1464
|
+
gitignoreResult,
|
|
1465
|
+
scriptInjected,
|
|
1466
|
+
linkCreated,
|
|
1467
|
+
backups,
|
|
1468
|
+
linkBackups,
|
|
1469
|
+
skipped,
|
|
1470
|
+
noInstall
|
|
1471
|
+
});
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
// src/cli/commands/doctor.ts
|
|
1475
|
+
import fs8 from "fs/promises";
|
|
1476
|
+
import path10 from "path";
|
|
1477
|
+
var REQUIRED_FILES = [
|
|
1478
|
+
".ai-workflow",
|
|
1479
|
+
"opencode/README.md"
|
|
1480
|
+
];
|
|
1481
|
+
async function isSymlink(filePath) {
|
|
1482
|
+
try {
|
|
1483
|
+
const stat = await fs8.lstat(filePath);
|
|
1484
|
+
return stat.isSymbolicLink();
|
|
1485
|
+
} catch {
|
|
1486
|
+
return false;
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
async function runDoctor({ cwd }) {
|
|
1490
|
+
let hasFailure = false;
|
|
1491
|
+
let hasWarning = false;
|
|
1492
|
+
let managedBlocks = [];
|
|
1493
|
+
console.log("Doctor report:");
|
|
1494
|
+
const adapter = new OpenCodeAdapter({ cwd });
|
|
1495
|
+
const inspection = await adapter.inspect();
|
|
1496
|
+
if (inspection.available) {
|
|
1497
|
+
console.log("PASS opencode CLI is available");
|
|
1498
|
+
const supportedStr = Object.entries(inspection.supports).filter(([_, v]) => v).map(([k, _]) => k).join(", ");
|
|
1499
|
+
console.log(`PASS opencode capabilities detected: ${supportedStr || "none"}`);
|
|
1500
|
+
if (inspection.supports.agent) {
|
|
1501
|
+
console.log("PASS opencode --agent selection is supported");
|
|
1502
|
+
} else {
|
|
1503
|
+
hasWarning = true;
|
|
1504
|
+
console.log("WARN opencode --agent selection is not supported; prompt fallback is available");
|
|
1505
|
+
}
|
|
1506
|
+
} else {
|
|
1507
|
+
hasFailure = true;
|
|
1508
|
+
console.log("FAIL opencode CLI is not installed or not available in PATH");
|
|
1509
|
+
}
|
|
1510
|
+
for (const relativePath of REQUIRED_FILES) {
|
|
1511
|
+
const ok = await exists(path10.join(cwd, relativePath));
|
|
1512
|
+
if (ok) {
|
|
1513
|
+
console.log(`PASS ${relativePath}`);
|
|
1514
|
+
} else {
|
|
1515
|
+
hasFailure = true;
|
|
1516
|
+
console.log(`FAIL ${relativePath} missing`);
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
const configPathV2 = path10.join(cwd, ".ai-workflow/config.json");
|
|
1520
|
+
const configPathLegacy = path10.join(cwd, ".ai-workflow.json");
|
|
1521
|
+
let configPath = null;
|
|
1522
|
+
if (await exists(configPathV2)) {
|
|
1523
|
+
configPath = configPathV2;
|
|
1524
|
+
} else if (await exists(configPathLegacy)) {
|
|
1525
|
+
configPath = configPathLegacy;
|
|
1526
|
+
}
|
|
1527
|
+
if (configPath) {
|
|
1528
|
+
try {
|
|
1529
|
+
const config = await readJson(configPath);
|
|
1530
|
+
if (config.installMode === "project-local" || config.mode === "standalone") {
|
|
1531
|
+
console.log("PASS install mode is project-local/standalone");
|
|
1532
|
+
} else {
|
|
1533
|
+
hasWarning = true;
|
|
1534
|
+
console.log(`WARN install mode is not project-local/standalone in ${path10.basename(configPath)}`);
|
|
1535
|
+
}
|
|
1536
|
+
if (config.profile) {
|
|
1537
|
+
console.log(`PASS profile detected: ${config.profile}`);
|
|
1538
|
+
} else {
|
|
1539
|
+
hasWarning = true;
|
|
1540
|
+
console.log(`WARN profile is missing in ${path10.basename(configPath)}`);
|
|
1541
|
+
}
|
|
1542
|
+
if (config.runtime) {
|
|
1543
|
+
console.log(`PASS runtime: ${config.runtime}`);
|
|
1544
|
+
} else {
|
|
1545
|
+
console.log(`NOTE runtime field not present in ${path10.basename(configPath)}`);
|
|
1546
|
+
}
|
|
1547
|
+
if (Array.isArray(config.managedBlocks)) {
|
|
1548
|
+
managedBlocks = config.managedBlocks;
|
|
1549
|
+
} else {
|
|
1550
|
+
console.log(`NOTE managedBlocks not present in ${path10.basename(configPath)} (legacy field)`);
|
|
1551
|
+
}
|
|
1552
|
+
if (Array.isArray(config.managedFiles)) {
|
|
1553
|
+
for (const relativePath of config.managedFiles) {
|
|
1554
|
+
const fileExists = await exists(path10.join(cwd, relativePath));
|
|
1555
|
+
if (!fileExists) {
|
|
1556
|
+
hasWarning = true;
|
|
1557
|
+
console.log(`WARN managed file missing: ${relativePath}`);
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
if (Array.isArray(config.managedLinks)) {
|
|
1562
|
+
for (const relativePath of config.managedLinks) {
|
|
1563
|
+
const absolutePath = path10.join(cwd, relativePath);
|
|
1564
|
+
const linkExists = await exists(absolutePath);
|
|
1565
|
+
if (!linkExists) {
|
|
1566
|
+
hasWarning = true;
|
|
1567
|
+
console.log(`WARN managed link missing: ${relativePath}`);
|
|
1568
|
+
continue;
|
|
1569
|
+
}
|
|
1570
|
+
const isLink = await isSymlink(absolutePath);
|
|
1571
|
+
if (!isLink) {
|
|
1572
|
+
hasWarning = true;
|
|
1573
|
+
console.log(`WARN managed link is not a symlink: ${relativePath}`);
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
} catch {
|
|
1578
|
+
hasFailure = true;
|
|
1579
|
+
console.log(`FAIL ${path10.basename(configPath)} is not valid JSON`);
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
const opencodePath = path10.join(cwd, "opencode.jsonc");
|
|
1583
|
+
if (!await exists(opencodePath)) {
|
|
1584
|
+
hasFailure = true;
|
|
1585
|
+
console.log("FAIL opencode.jsonc missing");
|
|
1586
|
+
} else {
|
|
1587
|
+
try {
|
|
1588
|
+
const opencodeConfig = await readJsonc(opencodePath);
|
|
1589
|
+
console.log("PASS opencode.jsonc is valid JSONC");
|
|
1590
|
+
const needsAgentDefault = managedBlocks.includes("opencode.jsonc:agent.default");
|
|
1591
|
+
if (!needsAgentDefault || opencodeConfig.agent?.default) {
|
|
1592
|
+
console.log("PASS opencode agent.default available");
|
|
1593
|
+
} else {
|
|
1594
|
+
hasFailure = true;
|
|
1595
|
+
console.log("FAIL opencode agent.default missing");
|
|
1596
|
+
}
|
|
1597
|
+
const requiredCommands = managedBlocks.filter((block) => block.startsWith("opencode.jsonc:command.")).map((block) => block.replace("opencode.jsonc:command.", ""));
|
|
1598
|
+
if (requiredCommands.length === 0) {
|
|
1599
|
+
console.log("PASS no managed opencode commands required");
|
|
1600
|
+
} else {
|
|
1601
|
+
const missingCommands = requiredCommands.filter((command) => !opencodeConfig.command?.[command]);
|
|
1602
|
+
if (missingCommands.length === 0) {
|
|
1603
|
+
console.log(`PASS opencode managed commands available (${requiredCommands.length})`);
|
|
1604
|
+
} else {
|
|
1605
|
+
hasFailure = true;
|
|
1606
|
+
console.log(`FAIL opencode command entries missing: ${missingCommands.join(", ")}`);
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
const requiredAgents = managedBlocks.filter((block) => block.startsWith("opencode.jsonc:agent.") && !block.endsWith(".default")).map((block) => block.replace("opencode.jsonc:agent.", ""));
|
|
1610
|
+
if (requiredAgents.length === 0) {
|
|
1611
|
+
console.log("PASS no managed opencode agents required");
|
|
1612
|
+
} else {
|
|
1613
|
+
const missingAgents = requiredAgents.filter((agent) => !opencodeConfig.agent?.[agent]);
|
|
1614
|
+
if (missingAgents.length === 0) {
|
|
1615
|
+
console.log(`PASS opencode managed agents available (${requiredAgents.length})`);
|
|
1616
|
+
} else {
|
|
1617
|
+
hasFailure = true;
|
|
1618
|
+
console.log(`FAIL opencode agent entries missing: ${missingAgents.join(", ")}`);
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
} catch {
|
|
1622
|
+
hasFailure = true;
|
|
1623
|
+
console.log("FAIL opencode.jsonc is not valid JSONC");
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
const packageJsonPath = path10.join(cwd, "package.json");
|
|
1627
|
+
if (await exists(packageJsonPath)) {
|
|
1628
|
+
try {
|
|
1629
|
+
const packageJson = await readJson(packageJsonPath);
|
|
1630
|
+
const scripts = packageJson.scripts ?? {};
|
|
1631
|
+
if (!scripts.validate) {
|
|
1632
|
+
hasWarning = true;
|
|
1633
|
+
console.log("WARN package.json has no validate script");
|
|
1634
|
+
}
|
|
1635
|
+
} catch {
|
|
1636
|
+
hasFailure = true;
|
|
1637
|
+
console.log("FAIL package.json is not valid JSON");
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
const finalStatus = hasFailure ? "FAIL" : hasWarning ? "PASS_WITH_NOTES" : "PASS";
|
|
1641
|
+
console.log(`Final status: ${finalStatus}`);
|
|
1642
|
+
if (hasFailure) {
|
|
1643
|
+
process.exitCode = 1;
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
// src/core/validation/canonical-finalization.ts
|
|
1648
|
+
var DELIVERY_SUMMARY_FIELDS = Object.freeze([
|
|
1649
|
+
"Status",
|
|
1650
|
+
"Branch",
|
|
1651
|
+
"Changes",
|
|
1652
|
+
"Validation",
|
|
1653
|
+
"Known limitations"
|
|
1654
|
+
]);
|
|
1655
|
+
function buildDeliverySummary({ evidence }) {
|
|
1656
|
+
const commands = (evidence?.commands || []).map((item) => `${item.command || item.name}: ${item.status}`).join("; ") || "No validation commands recorded";
|
|
1657
|
+
return {
|
|
1658
|
+
Status: evidence?.status || "BLOCKED",
|
|
1659
|
+
Branch: evidence?.branch || "unknown",
|
|
1660
|
+
Changes: (evidence?.changedFiles || []).join(", ") || "No changed files recorded",
|
|
1661
|
+
Validation: commands,
|
|
1662
|
+
"Known limitations": (evidence?.limitations || []).join("; ") || "None recorded"
|
|
1663
|
+
};
|
|
1664
|
+
}
|
|
1665
|
+
function formatDeliverySummary(summary) {
|
|
1666
|
+
return DELIVERY_SUMMARY_FIELDS.map((field) => `${field}: ${summary?.[field] || "NOT_RECORDED"}`).join("\n");
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
// src/cli/commands/collect-evidence.ts
|
|
1670
|
+
function resolvePoliciesFromLegacyMode(mode, evidencePolicy) {
|
|
1671
|
+
const activePolicy = evidencePolicy || (mode === "full" ? "required" : "optional");
|
|
1672
|
+
let risk = "medium";
|
|
1673
|
+
if (mode === "full") {
|
|
1674
|
+
risk = "high";
|
|
1675
|
+
} else if (mode === "quick") {
|
|
1676
|
+
risk = activePolicy === "required" ? "medium" : "low";
|
|
1677
|
+
} else if (activePolicy === "required") {
|
|
1678
|
+
risk = "high";
|
|
1679
|
+
}
|
|
1680
|
+
return { evidencePolicy: activePolicy, riskLevel: risk };
|
|
1681
|
+
}
|
|
1682
|
+
async function runCollectEvidence({
|
|
1683
|
+
cwd,
|
|
1684
|
+
exitOnError = true,
|
|
1685
|
+
taskSlug = null,
|
|
1686
|
+
mode = null,
|
|
1687
|
+
evidencePolicy = null,
|
|
1688
|
+
dryRun = false,
|
|
1689
|
+
profile = "generic",
|
|
1690
|
+
branchRecovery = "NOT_RECORDED",
|
|
1691
|
+
userRequest = null,
|
|
1692
|
+
explicitApprovals = [],
|
|
1693
|
+
visualDist = null,
|
|
1694
|
+
port = null
|
|
1695
|
+
}) {
|
|
1696
|
+
const policies = resolvePoliciesFromLegacyMode(mode, evidencePolicy);
|
|
1697
|
+
const qualityGuard = new QualityGuard({ cwd, taskSlug, evidencePolicy: policies.evidencePolicy, riskLevel: policies.riskLevel });
|
|
1698
|
+
const planner = new ValidationPlanner({ cwd, qualityGuard });
|
|
1699
|
+
const tasks = await planner.plan(profile);
|
|
1700
|
+
const persist = !dryRun && policies.evidencePolicy === "required";
|
|
1701
|
+
const collector = new EvidenceCollector({
|
|
1702
|
+
cwd,
|
|
1703
|
+
timeout: 6e4,
|
|
1704
|
+
taskSlug,
|
|
1705
|
+
evidencePolicy: policies.evidencePolicy,
|
|
1706
|
+
profile,
|
|
1707
|
+
branchRecovery,
|
|
1708
|
+
userRequest,
|
|
1709
|
+
explicitApprovals,
|
|
1710
|
+
visualDist,
|
|
1711
|
+
port: port || 8080
|
|
1712
|
+
});
|
|
1713
|
+
const evidence = await collector.collect(tasks, { writeArtifact: persist });
|
|
1714
|
+
const summary = buildDeliverySummary({ evidence });
|
|
1715
|
+
console.log("\n--- Delivery Summary ---");
|
|
1716
|
+
console.log(formatDeliverySummary(summary));
|
|
1717
|
+
console.log(persist ? "\nArtifact: EVIDENCE.json" : "\nArtifact: NOT_REQUIRED for optional evidence policy");
|
|
1718
|
+
console.log(`Commands processed: ${evidence.commands.length}
|
|
1719
|
+
`);
|
|
1720
|
+
if (evidence.status === "BLOCKED" && exitOnError) process.exit(1);
|
|
1721
|
+
return evidence;
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
// src/core/healing/cli-remediation-executor.ts
|
|
1725
|
+
import { spawnSync, execSync as execSync2 } from "child_process";
|
|
1726
|
+
import fs9 from "fs/promises";
|
|
1727
|
+
import path11 from "path";
|
|
1728
|
+
async function readScripts(cwd) {
|
|
1729
|
+
try {
|
|
1730
|
+
const pkg = JSON.parse(await fs9.readFile(path11.join(cwd, "package.json"), "utf8"));
|
|
1731
|
+
return pkg.scripts || {};
|
|
1732
|
+
} catch {
|
|
1733
|
+
return {};
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
function getChangedFiles(cwd) {
|
|
1737
|
+
try {
|
|
1738
|
+
const output = execSync2("git status --short", { cwd, encoding: "utf8" });
|
|
1739
|
+
return output.split("\n").filter(Boolean).map((line) => line.slice(3).trim()).filter(Boolean);
|
|
1740
|
+
} catch {
|
|
1741
|
+
return [];
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
function resolveRetryScripts(affectedChecks, scripts) {
|
|
1745
|
+
const retry = /* @__PURE__ */ new Set();
|
|
1746
|
+
for (const check of affectedChecks) {
|
|
1747
|
+
if (check === "evidence" || check === "result") {
|
|
1748
|
+
if (scripts.test) retry.add("test");
|
|
1749
|
+
if (scripts.lint) retry.add("lint");
|
|
1750
|
+
if (scripts.build) retry.add("build");
|
|
1751
|
+
if (scripts.typecheck) retry.add("typecheck");
|
|
1752
|
+
}
|
|
1753
|
+
if (check.includes("test")) retry.add("test");
|
|
1754
|
+
if (check.includes("lint")) retry.add("lint");
|
|
1755
|
+
if (check.includes("build")) retry.add("build");
|
|
1756
|
+
if (check.includes("typecheck")) retry.add("typecheck");
|
|
1757
|
+
}
|
|
1758
|
+
return [...retry].filter((name) => !!scripts[name]);
|
|
1759
|
+
}
|
|
1760
|
+
function createCliRemediationExecutor(cwd) {
|
|
1761
|
+
return async function remediationExecutor({
|
|
1762
|
+
attempt,
|
|
1763
|
+
maxAttempts,
|
|
1764
|
+
mode,
|
|
1765
|
+
affectedChecks,
|
|
1766
|
+
requestPath
|
|
1767
|
+
}) {
|
|
1768
|
+
console.log(`
|
|
1769
|
+
[HEALER] Attempt ${attempt}/${maxAttempts} \u2014 mode: ${mode}`);
|
|
1770
|
+
console.log(`[HEALER] Affected: ${affectedChecks.join(", ")}`);
|
|
1771
|
+
const scripts = await readScripts(cwd);
|
|
1772
|
+
const filesBefore = getChangedFiles(cwd);
|
|
1773
|
+
const toRetry = resolveRetryScripts(affectedChecks, scripts);
|
|
1774
|
+
if (toRetry.length === 0) {
|
|
1775
|
+
console.log("[HEALER] No retryable scripts found for the affected checks.");
|
|
1776
|
+
console.log(`[HEALER] Review ${requestPath} for details.`);
|
|
1777
|
+
return {
|
|
1778
|
+
applied: false,
|
|
1779
|
+
reason: `No npm scripts available to address: ${affectedChecks.join(", ")}. Manual remediation required.`
|
|
1780
|
+
};
|
|
1781
|
+
}
|
|
1782
|
+
console.log(`[HEALER] Re-running: ${toRetry.map((s) => `npm run ${s}`).join(", ")}`);
|
|
1783
|
+
const results = [];
|
|
1784
|
+
let anyPassed = false;
|
|
1785
|
+
for (const scriptName of toRetry) {
|
|
1786
|
+
process.stdout.write(` \u2192 npm run ${scriptName} ... `);
|
|
1787
|
+
const isWin32 = process.platform === "win32";
|
|
1788
|
+
const cmd = isWin32 ? "npm.cmd" : "npm";
|
|
1789
|
+
const result = spawnSync(cmd, ["run", scriptName], {
|
|
1790
|
+
cwd,
|
|
1791
|
+
shell: false,
|
|
1792
|
+
encoding: "utf8",
|
|
1793
|
+
timeout: 12e4
|
|
1794
|
+
});
|
|
1795
|
+
const passed = result.status === 0;
|
|
1796
|
+
if (passed) anyPassed = true;
|
|
1797
|
+
console.log(passed ? "PASS" : "FAIL");
|
|
1798
|
+
results.push({ script: scriptName, passed, exitCode: result.status });
|
|
1799
|
+
}
|
|
1800
|
+
const filesAfter = getChangedFiles(cwd);
|
|
1801
|
+
const changedFiles = filesAfter.filter((f) => !filesBefore.includes(f));
|
|
1802
|
+
if (!anyPassed && changedFiles.length === 0) {
|
|
1803
|
+
console.log("[HEALER] No scripts passed and no files changed \u2014 remediation had no effect.");
|
|
1804
|
+
return {
|
|
1805
|
+
applied: false,
|
|
1806
|
+
reason: `Re-ran ${toRetry.join(", ")} \u2014 all failed. Structural fix required by the implementing agent.`
|
|
1807
|
+
};
|
|
1808
|
+
}
|
|
1809
|
+
console.log(`[HEALER] Applied: ${results.filter((r) => r.passed).map((r) => r.script).join(", ") || "none passed"}`);
|
|
1810
|
+
if (changedFiles.length) console.log(`[HEALER] Changed files: ${changedFiles.join(", ")}`);
|
|
1811
|
+
return {
|
|
1812
|
+
applied: true,
|
|
1813
|
+
changedFiles,
|
|
1814
|
+
evidenceAdded: anyPassed ? ["validation-output"] : []
|
|
1815
|
+
};
|
|
1816
|
+
};
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
// src/cli/commands/run.ts
|
|
1820
|
+
import path12 from "path";
|
|
1821
|
+
import fs10 from "fs/promises";
|
|
1822
|
+
function normalizeTaskSlugFromSpec(specPath) {
|
|
1823
|
+
const normalized = String(specPath).replaceAll("\\", "/");
|
|
1824
|
+
const marker = "/docs/workflows/";
|
|
1825
|
+
const index = normalized.includes(marker) ? normalized.indexOf(marker) + marker.length : normalized.startsWith("docs/workflows/") ? "docs/workflows/".length : -1;
|
|
1826
|
+
if (index >= 0) {
|
|
1827
|
+
const slug = normalized.slice(index).split("/")[0];
|
|
1828
|
+
if (slug) return slug;
|
|
1829
|
+
}
|
|
1830
|
+
return path12.basename(specPath, path12.extname(specPath));
|
|
1831
|
+
}
|
|
1832
|
+
function combineValidation(evidence, quality) {
|
|
1833
|
+
const statuses = [evidence.internalStatus, quality.overallStatus];
|
|
1834
|
+
let overallStatus = "PASS";
|
|
1835
|
+
if (statuses.includes("BLOCKED")) overallStatus = "BLOCKED";
|
|
1836
|
+
else if (statuses.includes("FAIL")) overallStatus = "FAIL";
|
|
1837
|
+
else if (statuses.includes("FAIL_DELEGATION_GATE")) overallStatus = "FAIL_DELEGATION_GATE";
|
|
1838
|
+
else if (statuses.includes("FAIL_QUALITY_GATE")) overallStatus = "FAIL_QUALITY_GATE";
|
|
1839
|
+
else if (statuses.includes("PASS_WITH_NOTES")) overallStatus = "PASS_WITH_NOTES";
|
|
1840
|
+
return { overallStatus, evidence, quality };
|
|
1841
|
+
}
|
|
1842
|
+
async function runMasterOrchestrator({ cwd, specPath, override, remediationExecutor = null }) {
|
|
1843
|
+
console.log("\n[AI WORKFLOW] Starting evidence-backed orchestration...\n");
|
|
1844
|
+
const branchGate = new BranchGate({ memoryDir: path12.join(cwd, ".ai-workflow"), cwd });
|
|
1845
|
+
const taskSlug = specPath ? normalizeTaskSlugFromSpec(specPath) : "implementation";
|
|
1846
|
+
const gateResult = branchGate.check(override, { taskSlug });
|
|
1847
|
+
if (gateResult.blocked) throw new Error(`[GATE BLOCKED] ${gateResult.reason}`);
|
|
1848
|
+
console.log(`[PASS] Branch Gate: ${gateResult.recovered ? `${gateResult.branchBefore} -> ${gateResult.branch}` : `${gateResult.branch} is authorized`}.`);
|
|
1849
|
+
if (!specPath) throw new Error("[SDD BLOCKED] Missing --spec-path. An approved specification is required.");
|
|
1850
|
+
const validator = new SpecValidator();
|
|
1851
|
+
const absoluteSpecPath = path12.isAbsolute(specPath) ? specPath : path12.join(cwd, specPath);
|
|
1852
|
+
const validation = await validator.validate(absoluteSpecPath);
|
|
1853
|
+
if (!validation.valid) throw new Error(`[SDD BLOCKED] Specification is not ready: ${validation.reason}`);
|
|
1854
|
+
const specTier = validation.tier || "standard";
|
|
1855
|
+
const riskLevel = specTier === "deep" ? "high" : specTier === "tiny" ? "low" : "medium";
|
|
1856
|
+
const evidencePolicy = specTier === "deep" ? "required" : "optional";
|
|
1857
|
+
const remediationLimit = specTier === "deep" ? 3 : specTier === "tiny" ? 1 : 2;
|
|
1858
|
+
const specContent = await fs10.readFile(absoluteSpecPath, "utf8");
|
|
1859
|
+
const workflowProfile = resolveWorkflowProfile({ request: specContent });
|
|
1860
|
+
const profileDefinition = getWorkflowProfile(workflowProfile);
|
|
1861
|
+
console.log(`[PASS] Specification: ${specTier.toUpperCase()} spec is APPROVED.`);
|
|
1862
|
+
console.log(`[PROFILE] ${workflowProfile} -> ${profileDefinition.owner}; skills: ${profileDefinition.skills.join(", ") || "none"}.`);
|
|
1863
|
+
const validateWorkflow = async () => {
|
|
1864
|
+
const evidence = await runCollectEvidence({
|
|
1865
|
+
cwd,
|
|
1866
|
+
exitOnError: false,
|
|
1867
|
+
taskSlug,
|
|
1868
|
+
evidencePolicy,
|
|
1869
|
+
profile: workflowProfile,
|
|
1870
|
+
branchRecovery: gateResult.recovered ? `${gateResult.branchBefore} -> ${gateResult.branch}` : "NOT_REQUIRED"
|
|
1871
|
+
});
|
|
1872
|
+
const quality = await new QualityGuard({ cwd, taskSlug, evidencePolicy, riskLevel }).verify();
|
|
1873
|
+
return combineValidation(evidence, quality);
|
|
1874
|
+
};
|
|
1875
|
+
let result = await validateWorkflow();
|
|
1876
|
+
if (isRecoverableGateFailure(result.overallStatus)) {
|
|
1877
|
+
console.log(`
|
|
1878
|
+
[REMEDIATION REQUIRED] ${result.overallStatus}. Starting bounded remediation (limit: ${remediationLimit}).`);
|
|
1879
|
+
const executor = typeof remediationExecutor === "function" ? remediationExecutor : createCliRemediationExecutor(cwd);
|
|
1880
|
+
const healer = new HealerEngine({ cwd, remediationLimit, taskSlug });
|
|
1881
|
+
result = await healer.run({ initialResult: result, validate: validateWorkflow, remediate: executor });
|
|
1882
|
+
}
|
|
1883
|
+
if (isTerminalFailure(result.overallStatus)) {
|
|
1884
|
+
throw new Error(`[WORKFLOW BLOCKED] ${result.overallStatus}: ${result.reason || "Validation could not be resolved safely."}`);
|
|
1885
|
+
}
|
|
1886
|
+
if (isRecoverableGateFailure(result.overallStatus)) {
|
|
1887
|
+
throw new Error(`[WORKFLOW BLOCKED] Unresolved gate status after remediation: ${result.overallStatus}`);
|
|
1888
|
+
}
|
|
1889
|
+
console.log("\n--- Final Handoff ---");
|
|
1890
|
+
const handoffEngine = new HandoffEngine({ cwd });
|
|
1891
|
+
const handoffPath = await handoffEngine.generate({
|
|
1892
|
+
taskId: path12.basename(specPath, ".md"),
|
|
1893
|
+
status: result.overallStatus,
|
|
1894
|
+
specPaths: [specPath],
|
|
1895
|
+
evidence: result.evidence,
|
|
1896
|
+
nextActions: `Profile ${workflowProfile}. Observed validation completed. Ready for review or explicitly approved release actions.`
|
|
1897
|
+
});
|
|
1898
|
+
console.log(`
|
|
1899
|
+
[AI WORKFLOW COMPLETE] ${result.overallStatus}`);
|
|
1900
|
+
if (result.remediation?.attempts) console.log(`Remediation attempts: ${result.remediation.attempts}`);
|
|
1901
|
+
console.log(`Handoff Packet: ${path12.relative(cwd, handoffPath)}
|
|
1902
|
+
`);
|
|
1903
|
+
return { ...result, workflowProfile };
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
// src/core/healing/runtime-remediation-executor.ts
|
|
1907
|
+
import fs11 from "fs/promises";
|
|
1908
|
+
import path13 from "path";
|
|
1909
|
+
import { execSync as execSync3 } from "child_process";
|
|
1910
|
+
function createRuntimeRemediationExecutor(cwd, ledger = null) {
|
|
1911
|
+
const adapter = new OpenCodeAdapter({ cwd });
|
|
1912
|
+
function getChangedFiles2() {
|
|
1913
|
+
try {
|
|
1914
|
+
const output = execSync3("git status --short", { cwd, encoding: "utf8" });
|
|
1915
|
+
return output.split("\n").filter(Boolean).map((line) => line.slice(3).trim()).filter(Boolean);
|
|
1916
|
+
} catch {
|
|
1917
|
+
return [];
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
return async function runtimeRemediationExecutor({
|
|
1921
|
+
attempt,
|
|
1922
|
+
maxAttempts,
|
|
1923
|
+
mode,
|
|
1924
|
+
affectedChecks,
|
|
1925
|
+
requestPath,
|
|
1926
|
+
result
|
|
1927
|
+
}) {
|
|
1928
|
+
console.log(`
|
|
1929
|
+
[HEALER] Running Runtime/Phoenix Structural Remediation [Attempt ${attempt}/${maxAttempts}]`);
|
|
1930
|
+
console.log(`[HEALER] Affected checks: ${affectedChecks.join(", ")}`);
|
|
1931
|
+
if (ledger) {
|
|
1932
|
+
ledger.logEvent({
|
|
1933
|
+
actor: "Phoenix",
|
|
1934
|
+
actorType: "runtime-agent",
|
|
1935
|
+
observed: true,
|
|
1936
|
+
runtime: "opencode",
|
|
1937
|
+
eventType: "remediation_start",
|
|
1938
|
+
provenance: "opencode-adapter",
|
|
1939
|
+
data: {
|
|
1940
|
+
attempt,
|
|
1941
|
+
affectedChecks,
|
|
1942
|
+
event: "remediation.started"
|
|
1943
|
+
}
|
|
1944
|
+
});
|
|
1945
|
+
}
|
|
1946
|
+
let requestContent = {};
|
|
1947
|
+
try {
|
|
1948
|
+
requestContent = JSON.parse(await fs11.readFile(requestPath, "utf8"));
|
|
1949
|
+
} catch {
|
|
1950
|
+
}
|
|
1951
|
+
const filesBefore = getChangedFiles2();
|
|
1952
|
+
const fileContentsBefore = {};
|
|
1953
|
+
for (const file of filesBefore) {
|
|
1954
|
+
try {
|
|
1955
|
+
fileContentsBefore[file] = await fs11.readFile(path13.join(cwd, file), "utf8");
|
|
1956
|
+
} catch {
|
|
1957
|
+
fileContentsBefore[file] = null;
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
const failedCommands = (result?.evidence?.commands || []).filter((cmd) => ["FAIL", "BLOCKED"].includes(cmd.status));
|
|
1961
|
+
let concreteOutput = "";
|
|
1962
|
+
if (failedCommands.length > 0) {
|
|
1963
|
+
concreteOutput = failedCommands.map((cmd) => {
|
|
1964
|
+
return `Command: ${cmd.name} (${Array.isArray(cmd.command) ? cmd.command.join(" ") : cmd.command})
|
|
1965
|
+
Exit Code: ${cmd.exitCode}
|
|
1966
|
+
Output:
|
|
1967
|
+
${cmd.output}`;
|
|
1968
|
+
}).join("\n\n");
|
|
1969
|
+
} else {
|
|
1970
|
+
concreteOutput = JSON.stringify(result?.quality || result || {}, null, 2);
|
|
1971
|
+
}
|
|
1972
|
+
const promptMsg = `We are in remediation mode (attempt ${attempt}/${maxAttempts}).
|
|
1973
|
+
The following validation checks have failed:
|
|
1974
|
+
${affectedChecks.join(", ")}
|
|
1975
|
+
|
|
1976
|
+
Details of the failure:
|
|
1977
|
+
\`\`\`
|
|
1978
|
+
${concreteOutput}
|
|
1979
|
+
\`\`\`
|
|
1980
|
+
|
|
1981
|
+
Please inspect the code, identify the root cause of these failures (e.g. failing tests, syntax issues, build errors), and write a correction to the relevant files in the workspace to make the tests/validations pass.`;
|
|
1982
|
+
console.log(`[HEALER] Sending structural healing request to Phoenix agent...`);
|
|
1983
|
+
const runResult = await adapter.execute(promptMsg, {
|
|
1984
|
+
agent: "Phoenix"
|
|
1985
|
+
});
|
|
1986
|
+
const filesAfter = getChangedFiles2();
|
|
1987
|
+
const changedFiles = [];
|
|
1988
|
+
for (const file of filesAfter) {
|
|
1989
|
+
if (!filesBefore.includes(file)) {
|
|
1990
|
+
changedFiles.push(file);
|
|
1991
|
+
} else {
|
|
1992
|
+
try {
|
|
1993
|
+
const contentAfter = await fs11.readFile(path13.join(cwd, file), "utf8");
|
|
1994
|
+
if (contentAfter !== fileContentsBefore[file]) {
|
|
1995
|
+
changedFiles.push(file);
|
|
1996
|
+
}
|
|
1997
|
+
} catch {
|
|
1998
|
+
if (fileContentsBefore[file] !== null) {
|
|
1999
|
+
changedFiles.push(file);
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
const success = runResult.success && changedFiles.length > 0;
|
|
2005
|
+
if (!runResult.success) {
|
|
2006
|
+
console.log(`[HEALER] Phoenix remediation failed: ${runResult.error || "Unknown error"}`);
|
|
2007
|
+
return {
|
|
2008
|
+
applied: false,
|
|
2009
|
+
reason: `Phoenix runtime execution failed: ${runResult.error || "Unknown error"}`
|
|
2010
|
+
};
|
|
2011
|
+
}
|
|
2012
|
+
if (changedFiles.length === 0) {
|
|
2013
|
+
console.log(`[HEALER] Phoenix execution succeeded but no files were modified.`);
|
|
2014
|
+
return {
|
|
2015
|
+
applied: false,
|
|
2016
|
+
reason: "Phoenix runtime executed but did not modify any files in the workspace."
|
|
2017
|
+
};
|
|
2018
|
+
}
|
|
2019
|
+
console.log(`[HEALER] Phoenix successfully executed remediation commands.`);
|
|
2020
|
+
console.log(`[HEALER] Files modified: ${changedFiles.join(", ")}`);
|
|
2021
|
+
return {
|
|
2022
|
+
applied: true,
|
|
2023
|
+
changedFiles,
|
|
2024
|
+
evidenceAdded: ["phoenix-healing-run"]
|
|
2025
|
+
};
|
|
2026
|
+
};
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
// src/cli/commands/execute.ts
|
|
2030
|
+
import fs12 from "fs/promises";
|
|
2031
|
+
import path14 from "path";
|
|
2032
|
+
import { execSync as execSync4 } from "child_process";
|
|
2033
|
+
async function captureReadOnlyState(cwd, fastTrack = false) {
|
|
2034
|
+
const finalizer = new Finalizer({ cwd, fastTrack });
|
|
2035
|
+
const snapshot = await finalizer.snapshotManager.capture();
|
|
2036
|
+
const execGit = (args) => {
|
|
2037
|
+
try {
|
|
2038
|
+
return execSync4(`git ${args}`, { cwd, encoding: "utf8", stdio: "pipe" }).trim();
|
|
2039
|
+
} catch {
|
|
2040
|
+
return "";
|
|
2041
|
+
}
|
|
2042
|
+
};
|
|
2043
|
+
const head = execGit("rev-parse HEAD");
|
|
2044
|
+
const branch = execGit("rev-parse --abbrev-ref HEAD");
|
|
2045
|
+
const indexTree = execGit("write-tree");
|
|
2046
|
+
const porcelainStatus = execGit("status --porcelain");
|
|
2047
|
+
return {
|
|
2048
|
+
snapshot,
|
|
2049
|
+
head,
|
|
2050
|
+
branch,
|
|
2051
|
+
indexTree,
|
|
2052
|
+
porcelainStatus
|
|
2053
|
+
};
|
|
2054
|
+
}
|
|
2055
|
+
function compareReadOnlyState(before, after) {
|
|
2056
|
+
const violations = [];
|
|
2057
|
+
if (before.branch !== after.branch) {
|
|
2058
|
+
violations.push(`Branch changed from '${before.branch}' to '${after.branch}'`);
|
|
2059
|
+
}
|
|
2060
|
+
if (before.head !== after.head) {
|
|
2061
|
+
violations.push(`Git HEAD changed from '${before.head}' to '${after.head}' (commit or reset made inside read-only task)`);
|
|
2062
|
+
}
|
|
2063
|
+
if (before.indexTree !== after.indexTree) {
|
|
2064
|
+
violations.push(`Git index tree changed from '${before.indexTree}' to '${after.indexTree}'`);
|
|
2065
|
+
}
|
|
2066
|
+
if (before.porcelainStatus !== after.porcelainStatus) {
|
|
2067
|
+
violations.push(`Git status changed. Before:
|
|
2068
|
+
${before.porcelainStatus}
|
|
2069
|
+
After:
|
|
2070
|
+
${after.porcelainStatus}`);
|
|
2071
|
+
}
|
|
2072
|
+
const beforeFiles = before.snapshot.files || {};
|
|
2073
|
+
const afterFiles = after.snapshot.files || {};
|
|
2074
|
+
for (const [file, info] of Object.entries(afterFiles)) {
|
|
2075
|
+
const prevInfo = beforeFiles[file];
|
|
2076
|
+
if (!prevInfo) {
|
|
2077
|
+
violations.push(`File added: ${file}`);
|
|
2078
|
+
} else if (prevInfo.sha256 !== info.sha256) {
|
|
2079
|
+
violations.push(`File content modified: ${file}`);
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
for (const file of Object.keys(beforeFiles)) {
|
|
2083
|
+
if (!afterFiles[file]) {
|
|
2084
|
+
violations.push(`File deleted: ${file}`);
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
return violations;
|
|
2088
|
+
}
|
|
2089
|
+
function slugify(text) {
|
|
2090
|
+
return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 32) || "task";
|
|
2091
|
+
}
|
|
2092
|
+
function combineValidation2(evidence, quality) {
|
|
2093
|
+
const statuses = [evidence.internalStatus, quality.overallStatus];
|
|
2094
|
+
let overallStatus = "PASS";
|
|
2095
|
+
if (statuses.includes("BLOCKED")) overallStatus = "BLOCKED";
|
|
2096
|
+
else if (statuses.includes("FAIL")) overallStatus = "FAIL";
|
|
2097
|
+
else if (statuses.includes("FAIL_DELEGATION_GATE")) overallStatus = "FAIL_DELEGATION_GATE";
|
|
2098
|
+
else if (statuses.includes("FAIL_QUALITY_GATE")) overallStatus = "FAIL_QUALITY_GATE";
|
|
2099
|
+
else if (statuses.includes("PASS_WITH_NOTES")) overallStatus = "PASS_WITH_NOTES";
|
|
2100
|
+
return { overallStatus, evidence, quality };
|
|
2101
|
+
}
|
|
2102
|
+
async function classifyAndPlanExecution(naturalRequest, cwd, taskSlug, delegationController, stateMachine) {
|
|
2103
|
+
const classifier = new RequestClassifier();
|
|
2104
|
+
const classification = classifier.classify(naturalRequest, { cwd });
|
|
2105
|
+
console.log(`[CLASSIFIED] Intent: ${classification.intent}, Mode: ${classification.mode}, Profile: ${classification.profile}`);
|
|
2106
|
+
stateMachine.transitionTo("CLASSIFIED");
|
|
2107
|
+
await delegationController.route(naturalRequest, classification);
|
|
2108
|
+
const routingDecision = classification.routingDecision || {};
|
|
2109
|
+
if (routingDecision.permissionDecision === "blocked") {
|
|
2110
|
+
stateMachine.transitionTo("BLOCKED");
|
|
2111
|
+
throw new Error(`[ROUTE BLOCKED] ${routingDecision.reason}`);
|
|
2112
|
+
}
|
|
2113
|
+
if (routingDecision.permissionDecision === "rerouted") {
|
|
2114
|
+
console.log(`[REROUTED] ${routingDecision.reason}`);
|
|
2115
|
+
}
|
|
2116
|
+
const planner = new ExecutionPlanner({ cwd });
|
|
2117
|
+
const plan = planner.plan(classification, taskSlug);
|
|
2118
|
+
console.log(`[PLANNED] Owner: ${plan.owner}, Remediation limit: ${plan.remediationLimit}`);
|
|
2119
|
+
stateMachine.transitionTo("PLANNED");
|
|
2120
|
+
return { plan, classification };
|
|
2121
|
+
}
|
|
2122
|
+
function runBranchGate(plan, taskSlug, cwd, stateMachine, branchGate) {
|
|
2123
|
+
const gateResult = branchGate.check("", {
|
|
2124
|
+
taskSlug,
|
|
2125
|
+
readOnly: !plan.branchNeeded
|
|
2126
|
+
});
|
|
2127
|
+
if (gateResult.blocked) {
|
|
2128
|
+
stateMachine.transitionTo("BLOCKED");
|
|
2129
|
+
throw new Error(`[GATE BLOCKED] ${gateResult.reason}`);
|
|
2130
|
+
}
|
|
2131
|
+
console.log(`[PASS] Branch Gate: ${gateResult.recovered ? `${gateResult.branchBefore} -> ${gateResult.branch}` : `${gateResult.branch} is authorized`}.`);
|
|
2132
|
+
stateMachine.transitionTo("BRANCH_READY");
|
|
2133
|
+
return gateResult;
|
|
2134
|
+
}
|
|
2135
|
+
async function createSpecIfRequired(plan, classification, taskSlug, cwd) {
|
|
2136
|
+
if (plan.specPath) {
|
|
2137
|
+
const fullSpecPath = path14.join(cwd, plan.specPath);
|
|
2138
|
+
const specExists = await fs12.access(fullSpecPath).then(() => true).catch(() => false);
|
|
2139
|
+
if (!specExists) {
|
|
2140
|
+
await fs12.mkdir(path14.dirname(fullSpecPath), { recursive: true });
|
|
2141
|
+
const specTemplatePath = path14.join(cwd, ".ai-workflow/templates/specs/standard.md");
|
|
2142
|
+
const templateContent = await fs12.readFile(specTemplatePath, "utf8").catch(() => {
|
|
2143
|
+
return `# [STANDARD] Specification: ${classification.request}
|
|
2144
|
+
|
|
2145
|
+
## Metadata
|
|
2146
|
+
|
|
2147
|
+
- ID: SPEC-${taskSlug}
|
|
2148
|
+
- Author: Spec-Engineer
|
|
2149
|
+
- Status: DRAFT
|
|
2150
|
+
- Date: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}
|
|
2151
|
+
|
|
2152
|
+
## Functional Requirements
|
|
2153
|
+
|
|
2154
|
+
- ${classification.request}
|
|
2155
|
+
|
|
2156
|
+
## Technical Implementation Plan
|
|
2157
|
+
|
|
2158
|
+
### Files to Create/Modify
|
|
2159
|
+
|
|
2160
|
+
- \`src/index.js\`
|
|
2161
|
+
|
|
2162
|
+
## Acceptance Criteria
|
|
2163
|
+
|
|
2164
|
+
- [ ] Implemented successfully
|
|
2165
|
+
|
|
2166
|
+
## Testing Strategy
|
|
2167
|
+
|
|
2168
|
+
- [ ] Behavior tests pass`;
|
|
2169
|
+
});
|
|
2170
|
+
await fs12.writeFile(fullSpecPath, templateContent);
|
|
2171
|
+
console.log(`[EXECUTE] Created DRAFT specification template at: ${plan.specPath}`);
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
async function runImplementation(plan, classification, delegationController, stateMachine, fastTrack = false) {
|
|
2176
|
+
stateMachine.transitionTo("DELEGATED");
|
|
2177
|
+
stateMachine.transitionTo("IMPLEMENTING");
|
|
2178
|
+
let promptMsg = classification.request;
|
|
2179
|
+
if (plan.specPath) {
|
|
2180
|
+
promptMsg = `Please review and fill in the specification file at: ${plan.specPath}. Make sure to change the Status field to 'APPROVED' in the Metadata section, and then implement the behavior described.`;
|
|
2181
|
+
}
|
|
2182
|
+
const runResult = await delegationController.implement(promptMsg, plan.owner, { readOnly: !plan.branchNeeded, fastTrack });
|
|
2183
|
+
if (!runResult.success) {
|
|
2184
|
+
console.error(`[EXECUTE] OpenCode adapter execution reported failure: ${runResult.error || "Unknown error"}`);
|
|
2185
|
+
stateMachine.transitionTo("BLOCKED");
|
|
2186
|
+
throw new Error(`[WORKFLOW BLOCKED] OpenCode runtime execution failed: ${runResult.error || "Unknown error"}`);
|
|
2187
|
+
}
|
|
2188
|
+
return runResult;
|
|
2189
|
+
}
|
|
2190
|
+
async function runReadOnlyConfinementCheck(readOnlyStateBefore, cwd, stateMachine, fastTrack = false) {
|
|
2191
|
+
const readOnlyStateAfter = await captureReadOnlyState(cwd, fastTrack);
|
|
2192
|
+
const violations = compareReadOnlyState(readOnlyStateBefore, readOnlyStateAfter);
|
|
2193
|
+
if (violations.length > 0) {
|
|
2194
|
+
console.error(`[EXECUTE] Read-only confinement violation detected:`);
|
|
2195
|
+
for (const v of violations) {
|
|
2196
|
+
console.error(` - ${v}`);
|
|
2197
|
+
}
|
|
2198
|
+
stateMachine.transitionTo("BLOCKED");
|
|
2199
|
+
throw new Error(`[WORKFLOW BLOCKED] Read-only confinement violation: files were written/modified, index or commits modified:
|
|
2200
|
+
${violations.join("\n")}`);
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
async function runValidation(plan, taskSlug, cwd, gateResult, naturalRequest, delegationController, stateMachine) {
|
|
2204
|
+
stateMachine.transitionTo("VALIDATING");
|
|
2205
|
+
const validateWorkflow = async () => {
|
|
2206
|
+
const evidence = await runCollectEvidence({
|
|
2207
|
+
cwd,
|
|
2208
|
+
exitOnError: false,
|
|
2209
|
+
taskSlug,
|
|
2210
|
+
evidencePolicy: plan.evidencePolicy,
|
|
2211
|
+
profile: plan.profile,
|
|
2212
|
+
branchRecovery: gateResult.recovered ? `${gateResult.branchBefore} -> ${gateResult.branch}` : "NOT_REQUIRED",
|
|
2213
|
+
userRequest: naturalRequest,
|
|
2214
|
+
explicitApprovals: process.env.AI_WORKFLOW_APPROVALS ? process.env.AI_WORKFLOW_APPROVALS.split(",").map((a) => a.trim()) : []
|
|
2215
|
+
});
|
|
2216
|
+
const quality = await new QualityGuard({
|
|
2217
|
+
cwd,
|
|
2218
|
+
taskSlug,
|
|
2219
|
+
evidencePolicy: plan.evidencePolicy,
|
|
2220
|
+
riskLevel: plan.riskLevel
|
|
2221
|
+
}).verify();
|
|
2222
|
+
return combineValidation2(evidence, quality);
|
|
2223
|
+
};
|
|
2224
|
+
const result = await delegationController.validate(taskSlug, plan.profile, validateWorkflow, plan.evidencePolicy);
|
|
2225
|
+
return { result, validateWorkflow };
|
|
2226
|
+
}
|
|
2227
|
+
async function runBoundedRemediation(plan, taskSlug, cwd, initialResult, validateWorkflow, ledger, delegationController, stateMachine, checkBranchSafety, updateSnapshot) {
|
|
2228
|
+
if (!isRecoverableGateFailure(initialResult.overallStatus)) {
|
|
2229
|
+
return initialResult;
|
|
2230
|
+
}
|
|
2231
|
+
console.log(`
|
|
2232
|
+
[REMEDIATION REQUIRED] ${initialResult.overallStatus}. Starting bounded remediation (limit: ${plan.remediationLimit}).`);
|
|
2233
|
+
const executor = createRuntimeRemediationExecutor(cwd, ledger);
|
|
2234
|
+
const healer = new HealerEngine({
|
|
2235
|
+
cwd,
|
|
2236
|
+
remediationLimit: plan.remediationLimit,
|
|
2237
|
+
taskSlug,
|
|
2238
|
+
ledger
|
|
2239
|
+
});
|
|
2240
|
+
return await healer.run({
|
|
2241
|
+
initialResult,
|
|
2242
|
+
validate: async () => {
|
|
2243
|
+
stateMachine.transitionTo("REVALIDATING");
|
|
2244
|
+
return await delegationController.validate(taskSlug, plan.profile, validateWorkflow, plan.evidencePolicy);
|
|
2245
|
+
},
|
|
2246
|
+
remediate: async (remedCtx) => {
|
|
2247
|
+
stateMachine.transitionTo("REMEDIATING");
|
|
2248
|
+
const remRes = await executor(remedCtx);
|
|
2249
|
+
if (remRes.applied) {
|
|
2250
|
+
checkBranchSafety();
|
|
2251
|
+
await updateSnapshot();
|
|
2252
|
+
}
|
|
2253
|
+
return remRes;
|
|
2254
|
+
}
|
|
2255
|
+
});
|
|
2256
|
+
}
|
|
2257
|
+
async function runFinalizerStabilization(plan, taskSlug, cwd, initialResult, validateWorkflow, delegationController, finalizer, lastWriteSnapshotRef, checkBranchSafety) {
|
|
2258
|
+
let integrity = await finalizer.verifyIntegrity(lastWriteSnapshotRef.current);
|
|
2259
|
+
let revalidationCount = 0;
|
|
2260
|
+
const maxRevalidations = 2;
|
|
2261
|
+
let result = initialResult;
|
|
2262
|
+
while (!integrity.valid && revalidationCount < maxRevalidations) {
|
|
2263
|
+
console.log(`
|
|
2264
|
+
[FINALIZER WARNING] Workspace mutated during validation! Re-running validation to stabilize.`);
|
|
2265
|
+
console.log(`Changes detected:`, integrity.changes);
|
|
2266
|
+
checkBranchSafety();
|
|
2267
|
+
lastWriteSnapshotRef.current = await finalizer.snapshotManager.capture();
|
|
2268
|
+
revalidationCount++;
|
|
2269
|
+
result = await delegationController.validate(taskSlug, plan.profile, validateWorkflow, plan.evidencePolicy);
|
|
2270
|
+
checkBranchSafety();
|
|
2271
|
+
integrity = await finalizer.verifyIntegrity(lastWriteSnapshotRef.current);
|
|
2272
|
+
}
|
|
2273
|
+
return { result, integrity };
|
|
2274
|
+
}
|
|
2275
|
+
async function generateFinalHandoff(plan, taskSlug, cwd, finalState, evidence) {
|
|
2276
|
+
console.log("\n--- Final Handoff ---");
|
|
2277
|
+
const handoffEngine = new HandoffEngine({ cwd });
|
|
2278
|
+
const handoffPath = await handoffEngine.generate({
|
|
2279
|
+
taskId: taskSlug,
|
|
2280
|
+
status: finalState,
|
|
2281
|
+
specPaths: plan.specPath ? [plan.specPath] : [],
|
|
2282
|
+
evidence,
|
|
2283
|
+
nextActions: `Profile ${plan.profile}. Observed execution completed with status ${finalState}.`
|
|
2284
|
+
});
|
|
2285
|
+
console.log(`
|
|
2286
|
+
[AI WORKFLOW COMPLETE] ${finalState}`);
|
|
2287
|
+
console.log(`Handoff Packet: ${path14.relative(cwd, handoffPath)}
|
|
2288
|
+
`);
|
|
2289
|
+
return handoffPath;
|
|
2290
|
+
}
|
|
2291
|
+
async function runExecute({ cwd, naturalRequest, taskSlug: taskSlugOverride }) {
|
|
2292
|
+
if (!naturalRequest || !naturalRequest.trim()) {
|
|
2293
|
+
throw new Error("Missing request. Please provide a natural request string via positional arguments or --request flag.");
|
|
2294
|
+
}
|
|
2295
|
+
const taskSlug = taskSlugOverride || slugify(naturalRequest);
|
|
2296
|
+
console.log(`
|
|
2297
|
+
[AI WORKFLOW] Executing natural request intake [slug: ${taskSlug}]...
|
|
2298
|
+
`);
|
|
2299
|
+
const ledger = new EvidenceLedger({ cwd, workflowId: taskSlug });
|
|
2300
|
+
const delegationController = new DelegationController({ cwd, ledger, adapter: new OpenCodeAdapter({ cwd }) });
|
|
2301
|
+
let plan = null;
|
|
2302
|
+
try {
|
|
2303
|
+
const stateMachine = new WorkflowStateMachine();
|
|
2304
|
+
const { plan: planObj, classification } = await classifyAndPlanExecution(naturalRequest, cwd, taskSlug, delegationController, stateMachine);
|
|
2305
|
+
plan = planObj;
|
|
2306
|
+
const fastTrack = plan.riskLevel === "low" && (plan.branchNeeded ? plan.evidencePolicy === "optional" : true);
|
|
2307
|
+
const branchGate = new BranchGate({ memoryDir: path14.join(cwd, ".ai-workflow"), cwd });
|
|
2308
|
+
const gateResult = runBranchGate(plan, taskSlug, cwd, stateMachine, branchGate);
|
|
2309
|
+
const checkBranchSafety = () => {
|
|
2310
|
+
if (plan.branchNeeded) {
|
|
2311
|
+
const currentBranch = branchGate.getCurrentBranch();
|
|
2312
|
+
if (branchGate.protectedBranches.includes(currentBranch)) {
|
|
2313
|
+
stateMachine.transitionTo("BLOCKED");
|
|
2314
|
+
throw new Error(`[WORKFLOW BLOCKED] Security violation: current branch is protected '${currentBranch}'!`);
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
};
|
|
2318
|
+
await createSpecIfRequired(plan, classification, taskSlug, cwd);
|
|
2319
|
+
let readOnlyStateBefore = null;
|
|
2320
|
+
if (!plan.branchNeeded) {
|
|
2321
|
+
readOnlyStateBefore = await captureReadOnlyState(cwd, fastTrack);
|
|
2322
|
+
}
|
|
2323
|
+
await runImplementation(plan, classification, delegationController, stateMachine, fastTrack);
|
|
2324
|
+
if (!plan.branchNeeded) {
|
|
2325
|
+
await runReadOnlyConfinementCheck(readOnlyStateBefore, cwd, stateMachine, fastTrack);
|
|
2326
|
+
stateMachine.transitionTo("IMPLEMENTED");
|
|
2327
|
+
stateMachine.transitionTo("VALIDATING");
|
|
2328
|
+
stateMachine.transitionTo("COMPLETED");
|
|
2329
|
+
console.log("\n--- Final Handoff ---");
|
|
2330
|
+
console.log(`
|
|
2331
|
+
[AI WORKFLOW COMPLETE] COMPLETED`);
|
|
2332
|
+
console.log(`Handoff Packet: [IN-MEMORY] (No handoff file written in read-only mode)
|
|
2333
|
+
`);
|
|
2334
|
+
return {
|
|
2335
|
+
overallStatus: "PASS",
|
|
2336
|
+
evidence: {
|
|
2337
|
+
internalStatus: "PASS",
|
|
2338
|
+
commandsRun: []
|
|
2339
|
+
},
|
|
2340
|
+
quality: {
|
|
2341
|
+
overallStatus: "PASS"
|
|
2342
|
+
},
|
|
2343
|
+
stateHistory: stateMachine.getHistory()
|
|
2344
|
+
};
|
|
2345
|
+
}
|
|
2346
|
+
stateMachine.transitionTo("IMPLEMENTED");
|
|
2347
|
+
checkBranchSafety();
|
|
2348
|
+
const finalizer = new Finalizer({ cwd, fastTrack });
|
|
2349
|
+
const snapshotRef = { current: await finalizer.snapshotManager.capture() };
|
|
2350
|
+
const { result: valResult, validateWorkflow } = await runValidation(plan, taskSlug, cwd, gateResult, naturalRequest, delegationController, stateMachine);
|
|
2351
|
+
let result = valResult;
|
|
2352
|
+
result = await runBoundedRemediation(
|
|
2353
|
+
plan,
|
|
2354
|
+
taskSlug,
|
|
2355
|
+
cwd,
|
|
2356
|
+
result,
|
|
2357
|
+
validateWorkflow,
|
|
2358
|
+
ledger,
|
|
2359
|
+
delegationController,
|
|
2360
|
+
stateMachine,
|
|
2361
|
+
checkBranchSafety,
|
|
2362
|
+
async () => {
|
|
2363
|
+
snapshotRef.current = await finalizer.snapshotManager.capture();
|
|
2364
|
+
}
|
|
2365
|
+
);
|
|
2366
|
+
if (isTerminalFailure(result.overallStatus)) {
|
|
2367
|
+
stateMachine.transitionTo("BLOCKED");
|
|
2368
|
+
throw new Error(`[WORKFLOW BLOCKED] ${result.overallStatus}: Validation could not be resolved safely.`);
|
|
2369
|
+
}
|
|
2370
|
+
checkBranchSafety();
|
|
2371
|
+
let integrity = { valid: true, changes: { added: [], modified: [], deleted: [] } };
|
|
2372
|
+
let stabResult = result;
|
|
2373
|
+
if (!fastTrack) {
|
|
2374
|
+
const stab = await runFinalizerStabilization(
|
|
2375
|
+
plan,
|
|
2376
|
+
taskSlug,
|
|
2377
|
+
cwd,
|
|
2378
|
+
result,
|
|
2379
|
+
validateWorkflow,
|
|
2380
|
+
delegationController,
|
|
2381
|
+
finalizer,
|
|
2382
|
+
snapshotRef,
|
|
2383
|
+
checkBranchSafety
|
|
2384
|
+
);
|
|
2385
|
+
stabResult = stab.result;
|
|
2386
|
+
integrity = stab.integrity;
|
|
2387
|
+
}
|
|
2388
|
+
result = stabResult;
|
|
2389
|
+
let finalStatus = result.overallStatus;
|
|
2390
|
+
if (fastTrack) {
|
|
2391
|
+
console.log(`
|
|
2392
|
+
--- [FAST-TRACK] Running Lightweight Validation Gates ---`);
|
|
2393
|
+
checkBranchSafety();
|
|
2394
|
+
const changedFiles = result.evidence?.changedFiles || [];
|
|
2395
|
+
console.log(`[FAST-TRACK] Changed files detected:`, changedFiles);
|
|
2396
|
+
const evidencePath = path14.join(cwd, "EVIDENCE.json");
|
|
2397
|
+
const hasEvidence = await fs12.access(evidencePath).then(() => true).catch(() => false);
|
|
2398
|
+
if (hasEvidence) {
|
|
2399
|
+
const { EvidenceValidator } = await import("../evidence-validator-76ZQQYDU.js");
|
|
2400
|
+
const isTest = process.env.NODE_ENV === "test" || process.env.VITEST === "true";
|
|
2401
|
+
const evidenceValidator = new EvidenceValidator({ cwd, skipFileCheck: isTest, skipGitCheck: isTest });
|
|
2402
|
+
const valResult2 = await evidenceValidator.validate();
|
|
2403
|
+
if (!valResult2.valid) {
|
|
2404
|
+
console.error(`
|
|
2405
|
+
[FAST-TRACK BLOCKED] EVIDENCE.json validation failed: ${valResult2.reason}`);
|
|
2406
|
+
finalStatus = "FAIL_QUALITY_GATE";
|
|
2407
|
+
}
|
|
2408
|
+
}
|
|
2409
|
+
} else if (!integrity.valid) {
|
|
2410
|
+
console.log(`
|
|
2411
|
+
[FINALIZER BLOCKED] BLOCKED: workspace did not stabilize`);
|
|
2412
|
+
console.log(`Final changes detected:`, integrity.changes);
|
|
2413
|
+
finalStatus = "FAIL_QUALITY_GATE";
|
|
2414
|
+
} else {
|
|
2415
|
+
const fidelityResult = await finalizer.verifyFidelity({
|
|
2416
|
+
userRequest: naturalRequest,
|
|
2417
|
+
projectContext: result.evidence?.deliveryDecision?.projectContext || null,
|
|
2418
|
+
deliveryDecision: result.evidence?.deliveryDecision || null,
|
|
2419
|
+
changedFiles: result.evidence?.changedFiles || [],
|
|
2420
|
+
evidence: result.evidence || null,
|
|
2421
|
+
explicitApprovals: process.env.AI_WORKFLOW_APPROVALS ? process.env.AI_WORKFLOW_APPROVALS.split(",").map((a) => a.trim()) : []
|
|
2422
|
+
});
|
|
2423
|
+
if (!fidelityResult.passed) {
|
|
2424
|
+
console.log(`
|
|
2425
|
+
[FINALIZER BLOCKED] BLOCKED: Artifact fidelity check failed!`);
|
|
2426
|
+
console.log(`Reason: ${fidelityResult.reason}`);
|
|
2427
|
+
finalStatus = "FAIL_QUALITY_GATE";
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
const finalState = finalStatus === "PASS" ? "COMPLETED" : finalStatus === "PASS_WITH_NOTES" ? "COMPLETED_WITH_NOTES" : "BLOCKED";
|
|
2431
|
+
stateMachine.transitionTo(finalState);
|
|
2432
|
+
const handoffPath = await generateFinalHandoff(plan, taskSlug, cwd, finalState, result.evidence);
|
|
2433
|
+
return { ...result, overallStatus: finalStatus, stateHistory: stateMachine.getHistory() };
|
|
2434
|
+
} finally {
|
|
2435
|
+
if (plan && !plan.branchNeeded) {
|
|
2436
|
+
console.log(`
|
|
2437
|
+
[READ-ONLY LEDGER] (In-Memory/Stdout)
|
|
2438
|
+
${JSON.stringify(ledger.getEvents(), null, 2)}
|
|
2439
|
+
`);
|
|
2440
|
+
} else {
|
|
2441
|
+
try {
|
|
2442
|
+
await ledger.save(`.ai-workflow/history/${taskSlug}-ledger.json`);
|
|
2443
|
+
} catch (err) {
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
|
|
2449
|
+
// src/cli/commands/clean.ts
|
|
2450
|
+
import { existsSync, rmSync, lstatSync, unlinkSync } from "fs";
|
|
2451
|
+
import { join } from "path";
|
|
2452
|
+
import readline from "readline";
|
|
2453
|
+
function askConfirmation(question) {
|
|
2454
|
+
const rl = readline.createInterface({
|
|
2455
|
+
input: process.stdin,
|
|
2456
|
+
output: process.stdout
|
|
2457
|
+
});
|
|
2458
|
+
return new Promise((resolve) => {
|
|
2459
|
+
rl.question(question, (answer) => {
|
|
2460
|
+
rl.close();
|
|
2461
|
+
resolve(answer.trim().toLowerCase());
|
|
2462
|
+
});
|
|
2463
|
+
});
|
|
2464
|
+
}
|
|
2465
|
+
function isSymlink2(filePath) {
|
|
2466
|
+
try {
|
|
2467
|
+
return lstatSync(filePath).isSymbolicLink();
|
|
2468
|
+
} catch (_) {
|
|
2469
|
+
return false;
|
|
2470
|
+
}
|
|
2471
|
+
}
|
|
2472
|
+
async function runClean({ cwd, yes = false, dryRun = false, purgeAgents = false }) {
|
|
2473
|
+
const targets = [
|
|
2474
|
+
".ai-workflow",
|
|
2475
|
+
"opencode.jsonc",
|
|
2476
|
+
".workflow-state.json",
|
|
2477
|
+
"opencode"
|
|
2478
|
+
];
|
|
2479
|
+
const hasAgents = existsSync(join(cwd, ".agents"));
|
|
2480
|
+
const existingTargets = targets.map((name) => join(cwd, name)).filter((path15) => existsSync(path15) || isSymlink2(path15));
|
|
2481
|
+
if (existingTargets.length === 0 && (!hasAgents || !purgeAgents)) {
|
|
2482
|
+
console.log("No AI Workflow configuration or files found in the current project.");
|
|
2483
|
+
return;
|
|
2484
|
+
}
|
|
2485
|
+
console.log("\n[AI WORKFLOW] De-initialization");
|
|
2486
|
+
console.log("===============================");
|
|
2487
|
+
if (dryRun) {
|
|
2488
|
+
console.log("[DRY RUN] The following items would be deleted:");
|
|
2489
|
+
existingTargets.forEach((path15) => console.log(` - ${path15}`));
|
|
2490
|
+
if (hasAgents && purgeAgents) {
|
|
2491
|
+
console.log(` - ${join(cwd, ".agents")} (customizations folder)`);
|
|
2492
|
+
} else if (hasAgents) {
|
|
2493
|
+
console.log(` - NOTE: ${join(cwd, ".agents")} exists but will NOT be deleted (run with --purge-agents to delete)`);
|
|
2494
|
+
}
|
|
2495
|
+
return;
|
|
2496
|
+
}
|
|
2497
|
+
if (!yes) {
|
|
2498
|
+
if (!process.stdin.isTTY) {
|
|
2499
|
+
console.log("Error: Non-interactive terminal detected. Run with --yes to bypass confirmation.");
|
|
2500
|
+
if (process.env.NODE_ENV === "test") {
|
|
2501
|
+
} else {
|
|
2502
|
+
process.exit(1);
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
2505
|
+
const confirmDeinit = await askConfirmation("Are you sure you want to de-initialize AI Workflow from the project? [y/N] ");
|
|
2506
|
+
if (confirmDeinit !== "y" && confirmDeinit !== "yes") {
|
|
2507
|
+
console.log("Aborted.");
|
|
2508
|
+
return;
|
|
2509
|
+
}
|
|
2510
|
+
if (hasAgents && !purgeAgents) {
|
|
2511
|
+
const confirmPurge = await askConfirmation("A custom '.agents' folder was detected. Do you want to purge it (this deletes custom prompts/rules/skills)? [y/N] ");
|
|
2512
|
+
if (confirmPurge === "y" || confirmPurge === "yes") {
|
|
2513
|
+
purgeAgents = true;
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
if (purgeAgents && hasAgents) {
|
|
2518
|
+
existingTargets.push(join(cwd, ".agents"));
|
|
2519
|
+
}
|
|
2520
|
+
console.log("\nRemoving files...");
|
|
2521
|
+
for (const target of existingTargets) {
|
|
2522
|
+
try {
|
|
2523
|
+
if (isSymlink2(target)) {
|
|
2524
|
+
unlinkSync(target);
|
|
2525
|
+
console.log(`Deleted symlink: ${target}`);
|
|
2526
|
+
} else {
|
|
2527
|
+
rmSync(target, { recursive: true, force: true });
|
|
2528
|
+
console.log(`Deleted directory/file: ${target}`);
|
|
2529
|
+
}
|
|
2530
|
+
} catch (error) {
|
|
2531
|
+
console.error(`Failed to delete ${target}: ${error.message}`);
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2534
|
+
console.log("\nAI Workflow has been successfully removed from this project.");
|
|
2535
|
+
}
|
|
2536
|
+
|
|
2537
|
+
// src/cli/index.ts
|
|
2538
|
+
function printHelp() {
|
|
2539
|
+
console.log(`ai-workflow
|
|
2540
|
+
|
|
2541
|
+
Usage:
|
|
2542
|
+
ai-workflow execute "<request>" [--task=<slug>] [--request="<request>"]
|
|
2543
|
+
ai-workflow run --spec-path=<path>
|
|
2544
|
+
ai-workflow init [--yes] [--force] [--dry-run] [--no-install] [--no-overwrite] [--claude] [--codex] [--antigravity] [--profile=<profile>]
|
|
2545
|
+
ai-workflow validate [--a11y] [--visual-dist=<path>] [--port=<port>]
|
|
2546
|
+
ai-workflow collect-evidence [--task=<slug>] [--evidence-policy=<optional|required>] [--mode=<quick|standard|full>] [--dry-run] [--visual-dist=<path>] [--port=<port>]
|
|
2547
|
+
ai-workflow doctor
|
|
2548
|
+
ai-workflow clean [--yes] [--dry-run] [--purge-agents]
|
|
2549
|
+
|
|
2550
|
+
Commands:
|
|
2551
|
+
execute Orchestrate execution of a natural request through the state machine
|
|
2552
|
+
run Proportionate orchestrator with branch safety, validation, and bounded remediation
|
|
2553
|
+
init Install AI workflow defaults (OpenCode). --profile=standard (default) or --profile=full (adds examples)
|
|
2554
|
+
collect-evidence Run observed project validation; persists EVIDENCE.json under required evidence policy
|
|
2555
|
+
doctor Verify local ai-workflow installation
|
|
2556
|
+
clean De-initialize and remove AI Workflow files and symlinks from the project
|
|
2557
|
+
`);
|
|
2558
|
+
}
|
|
2559
|
+
function parseFlags(args) {
|
|
2560
|
+
const specPathArg = args.find((arg) => arg.startsWith("--spec-path="));
|
|
2561
|
+
const profileEqArg = args.find((arg) => arg.startsWith("--profile="));
|
|
2562
|
+
const taskArg = args.find((arg) => arg.startsWith("--task="));
|
|
2563
|
+
const modeArg = args.find((arg) => arg.startsWith("--mode="));
|
|
2564
|
+
const evidencePolicyArg = args.find((arg) => arg.startsWith("--evidence-policy="));
|
|
2565
|
+
const requestArg = args.find((arg) => arg.startsWith("--request="));
|
|
2566
|
+
const visualDistArg = args.find((arg) => arg.startsWith("--visual-dist="));
|
|
2567
|
+
const portArg = args.find((arg) => arg.startsWith("--port="));
|
|
2568
|
+
const profileIdx = args.indexOf("--profile");
|
|
2569
|
+
const profileVal = profileEqArg ? profileEqArg.replace("--profile=", "") : profileIdx >= 0 && profileIdx + 1 < args.length && !args[profileIdx + 1].startsWith("--") ? args[profileIdx + 1] : void 0;
|
|
2570
|
+
return {
|
|
2571
|
+
yes: args.includes("--yes"),
|
|
2572
|
+
force: args.includes("--force"),
|
|
2573
|
+
dryRun: args.includes("--dry-run"),
|
|
2574
|
+
noInstall: args.includes("--no-install"),
|
|
2575
|
+
noOverwrite: args.includes("--no-overwrite"),
|
|
2576
|
+
claude: args.includes("--claude"),
|
|
2577
|
+
codex: args.includes("--codex"),
|
|
2578
|
+
antigravity: args.includes("--antigravity"),
|
|
2579
|
+
"dev-mode": args.includes("--dev-mode"),
|
|
2580
|
+
specPath: specPathArg ? specPathArg.replace("--spec-path=", "") : void 0,
|
|
2581
|
+
profile: profileVal || void 0,
|
|
2582
|
+
taskSlug: taskArg ? taskArg.replace("--task=", "") : void 0,
|
|
2583
|
+
mode: modeArg ? modeArg.replace("--mode=", "") : void 0,
|
|
2584
|
+
evidencePolicy: evidencePolicyArg ? evidencePolicyArg.replace("--evidence-policy=", "") : void 0,
|
|
2585
|
+
request: requestArg ? requestArg.replace("--request=", "") : void 0,
|
|
2586
|
+
visualDist: visualDistArg ? visualDistArg.replace("--visual-dist=", "") : void 0,
|
|
2587
|
+
port: portArg ? portArg.replace("--port=", "") : void 0,
|
|
2588
|
+
a11y: args.includes("--a11y"),
|
|
2589
|
+
purgeAgents: args.includes("--purge-agents") || args.includes("--purge")
|
|
2590
|
+
};
|
|
2591
|
+
}
|
|
2592
|
+
async function runCli(args) {
|
|
2593
|
+
const [command] = args;
|
|
2594
|
+
if (command === "--version" || command === "-v") {
|
|
2595
|
+
console.log(getPackageVersion() || "unknown");
|
|
2596
|
+
return;
|
|
2597
|
+
}
|
|
2598
|
+
if (!command || command === "--help" || command === "-h") {
|
|
2599
|
+
printHelp();
|
|
2600
|
+
return;
|
|
2601
|
+
}
|
|
2602
|
+
if (command === "execute") {
|
|
2603
|
+
const flags = parseFlags(args.slice(1));
|
|
2604
|
+
const positionals = args.slice(1).filter((arg) => !arg.startsWith("-"));
|
|
2605
|
+
const request = flags.request || positionals.join(" ");
|
|
2606
|
+
const result = await runExecute({
|
|
2607
|
+
cwd: process.cwd(),
|
|
2608
|
+
naturalRequest: request,
|
|
2609
|
+
taskSlug: flags.taskSlug
|
|
2610
|
+
});
|
|
2611
|
+
if (result && (result.overallStatus === "FAIL_QUALITY_GATE" || result.overallStatus === "BLOCKED" || result.overallStatus === "FAIL")) {
|
|
2612
|
+
process.exit(1);
|
|
2613
|
+
}
|
|
2614
|
+
return;
|
|
2615
|
+
}
|
|
2616
|
+
if (command === "run") {
|
|
2617
|
+
await runMasterOrchestrator({
|
|
2618
|
+
cwd: process.cwd(),
|
|
2619
|
+
...parseFlags(args.slice(1))
|
|
2620
|
+
});
|
|
2621
|
+
return;
|
|
2622
|
+
}
|
|
2623
|
+
if (command === "init") {
|
|
2624
|
+
await runInit({
|
|
2625
|
+
cwd: process.cwd(),
|
|
2626
|
+
...parseFlags(args.slice(1))
|
|
2627
|
+
});
|
|
2628
|
+
return;
|
|
2629
|
+
}
|
|
2630
|
+
if (command === "collect-evidence") {
|
|
2631
|
+
const flags = parseFlags(args.slice(1));
|
|
2632
|
+
if (flags.mode && !["quick", "standard", "full"].includes(flags.mode)) throw new Error("--mode must be quick, standard, or full");
|
|
2633
|
+
await runCollectEvidence({
|
|
2634
|
+
cwd: process.cwd(),
|
|
2635
|
+
taskSlug: flags.taskSlug,
|
|
2636
|
+
mode: flags.mode,
|
|
2637
|
+
evidencePolicy: flags.evidencePolicy,
|
|
2638
|
+
dryRun: flags.dryRun,
|
|
2639
|
+
visualDist: flags.visualDist,
|
|
2640
|
+
port: flags.port
|
|
2641
|
+
});
|
|
2642
|
+
return;
|
|
2643
|
+
}
|
|
2644
|
+
if (command === "validate") {
|
|
2645
|
+
const flags = parseFlags(args.slice(1));
|
|
2646
|
+
const { runValidate } = await import("../validate-Q2NLLQ5G.js");
|
|
2647
|
+
await runValidate({
|
|
2648
|
+
cwd: process.cwd(),
|
|
2649
|
+
taskSlug: flags.taskSlug,
|
|
2650
|
+
a11y: flags.a11y,
|
|
2651
|
+
visualDist: flags.visualDist,
|
|
2652
|
+
port: flags.port
|
|
2653
|
+
});
|
|
2654
|
+
return;
|
|
2655
|
+
}
|
|
2656
|
+
if (command === "doctor") {
|
|
2657
|
+
await runDoctor({ cwd: process.cwd() });
|
|
2658
|
+
return;
|
|
2659
|
+
}
|
|
2660
|
+
if (command === "clean") {
|
|
2661
|
+
await runClean({
|
|
2662
|
+
cwd: process.cwd(),
|
|
2663
|
+
...parseFlags(args.slice(1))
|
|
2664
|
+
});
|
|
2665
|
+
return;
|
|
2666
|
+
}
|
|
2667
|
+
throw new Error(`unknown command: ${command}`);
|
|
2668
|
+
}
|
|
5
2669
|
runCli(process.argv.slice(2)).catch((error) => {
|
|
6
2670
|
console.error(`ai-workflow error: ${error.message}`);
|
|
7
2671
|
process.exit(1);
|
|
8
2672
|
});
|
|
2673
|
+
export {
|
|
2674
|
+
runCli
|
|
2675
|
+
};
|
|
2676
|
+
//# sourceMappingURL=ai-workflow.js.map
|