@williambeto/ai-workflow 2.4.9 → 2.6.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1138 -168
- package/README.md +203 -77
- 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/PUBLISH_MANIFEST.json +0 -34
- 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
|
@@ -0,0 +1,1827 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ArtifactFidelityGate,
|
|
3
|
+
DeliveryDecisionEngine
|
|
4
|
+
} from "./chunk-2AOV2ATY.js";
|
|
5
|
+
|
|
6
|
+
// src/core/package-assets.ts
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
8
|
+
import { dirname, resolve } from "path";
|
|
9
|
+
import { readFileSync, readdirSync, statSync } from "fs";
|
|
10
|
+
var cachedRoot = null;
|
|
11
|
+
function getPackageRoot() {
|
|
12
|
+
if (cachedRoot) return cachedRoot;
|
|
13
|
+
let currentDir = dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
while (true) {
|
|
15
|
+
const pkgPath = resolve(currentDir, "package.json");
|
|
16
|
+
try {
|
|
17
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
18
|
+
if (pkg && typeof pkg === "object" && pkg.name === "@williambeto/ai-workflow") {
|
|
19
|
+
cachedRoot = currentDir;
|
|
20
|
+
return cachedRoot;
|
|
21
|
+
}
|
|
22
|
+
} catch {
|
|
23
|
+
}
|
|
24
|
+
const parentDir = dirname(currentDir);
|
|
25
|
+
if (parentDir === currentDir) {
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
currentDir = parentDir;
|
|
29
|
+
}
|
|
30
|
+
throw new Error("Failed to resolve @williambeto/ai-workflow package root physically.");
|
|
31
|
+
}
|
|
32
|
+
function readPackageFile(relativePath) {
|
|
33
|
+
try {
|
|
34
|
+
const fullPath = resolve(getPackageRoot(), relativePath);
|
|
35
|
+
return readFileSync(fullPath, "utf8");
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function getPackageVersion() {
|
|
41
|
+
try {
|
|
42
|
+
const raw = readPackageFile("package.json");
|
|
43
|
+
if (!raw) return null;
|
|
44
|
+
const parsed = JSON.parse(raw);
|
|
45
|
+
return parsed && typeof parsed === "object" && typeof parsed.version === "string" ? parsed.version : null;
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function discoverPackageFiles(baseRelativePath) {
|
|
51
|
+
const files = {};
|
|
52
|
+
const basePath = resolve(getPackageRoot(), baseRelativePath);
|
|
53
|
+
try {
|
|
54
|
+
if (!statSync(basePath).isDirectory()) {
|
|
55
|
+
return files;
|
|
56
|
+
}
|
|
57
|
+
} catch {
|
|
58
|
+
return files;
|
|
59
|
+
}
|
|
60
|
+
function walk(dir, relativePrefix) {
|
|
61
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
62
|
+
for (const entry of entries) {
|
|
63
|
+
const fullPath = resolve(dir, entry.name);
|
|
64
|
+
const relativePath = relativePrefix ? `${relativePrefix}/${entry.name}` : entry.name;
|
|
65
|
+
if (entry.isDirectory()) {
|
|
66
|
+
walk(fullPath, relativePath);
|
|
67
|
+
} else if (entry.isFile()) {
|
|
68
|
+
try {
|
|
69
|
+
files[relativePath] = readFileSync(fullPath, "utf8");
|
|
70
|
+
} catch {
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
walk(basePath, baseRelativePath);
|
|
76
|
+
return files;
|
|
77
|
+
}
|
|
78
|
+
function getFullSkillFiles(skillName) {
|
|
79
|
+
const skillRelPath = `dist-assets/skills/${skillName}`;
|
|
80
|
+
const files = discoverPackageFiles(skillRelPath);
|
|
81
|
+
return files;
|
|
82
|
+
}
|
|
83
|
+
function getFullAgentContent(agentName) {
|
|
84
|
+
return readPackageFile(`dist-assets/agents/${agentName}.md`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// src/core/runtime/opencode-adapter.ts
|
|
88
|
+
import { spawn, spawnSync, execSync } from "child_process";
|
|
89
|
+
import readline from "readline";
|
|
90
|
+
var AGENT_PROMPT_CONTRACTS = {
|
|
91
|
+
Nexus: "Act as Nexus, the discovery and requirements agent. Stay read-oriented unless the routing contract explicitly permits writes. Produce concrete findings, requirements, and specification inputs.",
|
|
92
|
+
Orion: "Act as Orion, the planning and release strategy agent. Sequence work through branch safety, proportional validation, and release gates. Do not bypass human authorization.",
|
|
93
|
+
Astra: "Act as Astra, the implementation agent. Make the minimum scoped code changes needed, preserve unrelated work, and respect branch and validation gates.",
|
|
94
|
+
Sage: "Act as Sage, the independent validation agent. Audit the implementation against requirements, inspect evidence, and report concrete pass/fail findings.",
|
|
95
|
+
Phoenix: "Act as Phoenix, the bounded remediation agent. Remediate only concrete findings, keep attempts bounded, and stop when no progress is made."
|
|
96
|
+
};
|
|
97
|
+
var OpenCodeAdapter = class {
|
|
98
|
+
cwd;
|
|
99
|
+
constructor({ cwd = process.cwd() } = {}) {
|
|
100
|
+
this.cwd = cwd;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Inspects the availability and capability of the OpenCode CLI.
|
|
104
|
+
*/
|
|
105
|
+
async inspect() {
|
|
106
|
+
try {
|
|
107
|
+
const cliHelp = spawnSync("opencode", ["--help"], { encoding: "utf8" });
|
|
108
|
+
const runHelp = spawnSync("opencode", ["run", "--help"], { encoding: "utf8" });
|
|
109
|
+
const available = cliHelp.status === 0 && runHelp.status === 0;
|
|
110
|
+
if (!available) {
|
|
111
|
+
return {
|
|
112
|
+
available: false,
|
|
113
|
+
supports: { run: false, formatJson: false, agent: false, model: false }
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
available: true,
|
|
118
|
+
supports: {
|
|
119
|
+
run: /\brun\b/.test(cliHelp.stdout),
|
|
120
|
+
formatJson: /--format/.test(runHelp.stdout),
|
|
121
|
+
agent: /--agent/.test(runHelp.stdout),
|
|
122
|
+
model: /--model/.test(runHelp.stdout)
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
} catch {
|
|
126
|
+
return {
|
|
127
|
+
available: false,
|
|
128
|
+
supports: { run: false, formatJson: false, agent: false, model: false }
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Runs opencode with a prompt and options.
|
|
134
|
+
*/
|
|
135
|
+
async execute(message, { agent = null, model = null, readOnly = false, fastTrack = false } = {}) {
|
|
136
|
+
const inspection = await this.inspect();
|
|
137
|
+
const isSpecializedAgent = agent && ["Nexus", "Orion", "Astra", "Sage", "Phoenix"].includes(agent);
|
|
138
|
+
if (!readOnly) {
|
|
139
|
+
let currentBranch = "";
|
|
140
|
+
try {
|
|
141
|
+
currentBranch = execSync("git branch --show-current", { cwd: this.cwd, encoding: "utf8", stdio: "pipe" }).trim();
|
|
142
|
+
} catch {
|
|
143
|
+
}
|
|
144
|
+
const protectedBranches = ["main", "master"];
|
|
145
|
+
if (protectedBranches.includes(currentBranch)) {
|
|
146
|
+
if (process.env.AI_WORKFLOW_BYPASS_PROTECTED_BRANCH !== "true") {
|
|
147
|
+
return {
|
|
148
|
+
success: false,
|
|
149
|
+
status: "BLOCKED",
|
|
150
|
+
error: "FAIL_PROTECTED_BRANCH: Direct writes to protected branch are prohibited.",
|
|
151
|
+
runtimeAgentRequested: agent,
|
|
152
|
+
runtimeAgentApplied: null,
|
|
153
|
+
runtimeAgentSupported: false,
|
|
154
|
+
runtimeAgentSelectionMode: "unsupported",
|
|
155
|
+
commandsRun: [],
|
|
156
|
+
eventCount: 0
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (!inspection.available) {
|
|
162
|
+
return {
|
|
163
|
+
success: false,
|
|
164
|
+
status: isSpecializedAgent ? "BLOCKED" : "FAILED",
|
|
165
|
+
commandsRun: [],
|
|
166
|
+
eventCount: 0,
|
|
167
|
+
error: "OpenCode CLI is not installed or not available in the system PATH.",
|
|
168
|
+
runtimeAgentRequested: agent || null,
|
|
169
|
+
runtimeAgentApplied: null,
|
|
170
|
+
runtimeAgentSupported: false,
|
|
171
|
+
runtimeAgentSelectionMode: isSpecializedAgent ? "explicit" : "default"
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
const usePromptFallback = Boolean(isSpecializedAgent && !inspection.supports.agent && agent);
|
|
175
|
+
const runtimeMessage = usePromptFallback ? `${AGENT_PROMPT_CONTRACTS[agent]}
|
|
176
|
+
|
|
177
|
+
Runtime agent selection note: requested actor '${agent}' could not be applied with --agent because this OpenCode runtime does not support it. Continue as that actor by prompt contract and preserve all AI Workflow Kit safety rules.
|
|
178
|
+
|
|
179
|
+
User request:
|
|
180
|
+
${message}` : message;
|
|
181
|
+
const runtimeAgentSelectionMode = isSpecializedAgent ? usePromptFallback ? "prompt-fallback" : "explicit" : "default";
|
|
182
|
+
const runInWorkspace = async (targetCwd) => {
|
|
183
|
+
return new Promise((resolve2) => {
|
|
184
|
+
const args = ["run", runtimeMessage];
|
|
185
|
+
if (inspection.supports.formatJson) {
|
|
186
|
+
args.push("--format", "json");
|
|
187
|
+
}
|
|
188
|
+
if (isSpecializedAgent && inspection.supports.agent && agent) {
|
|
189
|
+
args.push("--agent", agent);
|
|
190
|
+
}
|
|
191
|
+
if (model && inspection.supports.model) {
|
|
192
|
+
args.push("--model", model);
|
|
193
|
+
}
|
|
194
|
+
console.log(`[RUNTIME] Delegating to OpenCode: opencode ${args.map((a) => a.includes(" ") ? `"${a}"` : a).join(" ")}`);
|
|
195
|
+
const child = spawn("opencode", args, {
|
|
196
|
+
cwd: targetCwd,
|
|
197
|
+
stdio: ["ignore", "pipe", "inherit"]
|
|
198
|
+
// Inherit stderr to show warnings directly
|
|
199
|
+
});
|
|
200
|
+
const rl = readline.createInterface({
|
|
201
|
+
input: child.stdout,
|
|
202
|
+
terminal: false
|
|
203
|
+
});
|
|
204
|
+
const commandsRun = [];
|
|
205
|
+
let eventCount = 0;
|
|
206
|
+
rl.on("line", (line) => {
|
|
207
|
+
const trimmed = line.trim();
|
|
208
|
+
if (!trimmed) return;
|
|
209
|
+
try {
|
|
210
|
+
const event = JSON.parse(trimmed);
|
|
211
|
+
eventCount++;
|
|
212
|
+
if (event.type === "text" && event.part?.text) {
|
|
213
|
+
process.stdout.write(event.part.text);
|
|
214
|
+
}
|
|
215
|
+
if (event.type === "step_start" && event.part?.toolCalls) {
|
|
216
|
+
for (const call of event.part.toolCalls) {
|
|
217
|
+
if (call.name === "run_command" && call.args?.CommandLine) {
|
|
218
|
+
commandsRun.push(call.args.CommandLine);
|
|
219
|
+
}
|
|
220
|
+
if (readOnly) {
|
|
221
|
+
const name = call.name || "";
|
|
222
|
+
const isWriteTool = name.startsWith("write_") || name.includes("write") || name.includes("replace") || name.includes("edit") || name.includes("delete") || name.includes("remove");
|
|
223
|
+
let isDestructiveGit = false;
|
|
224
|
+
if (name === "run_command" && call.args?.CommandLine) {
|
|
225
|
+
const cmd = call.args.CommandLine;
|
|
226
|
+
const destPattern = /\b(git\s+(commit|add|push|merge|reset|rm|branch|checkout|switch|stash|init)|touch|rm|mkdir|cp|mv|chmod|chown|tee|sed|awk)\b|>>|>/;
|
|
227
|
+
if (destPattern.test(cmd)) {
|
|
228
|
+
isDestructiveGit = true;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (isWriteTool || isDestructiveGit) {
|
|
232
|
+
console.error(`
|
|
233
|
+
[SECURITY WARNING] Read-only execution blocked attempt to write/modify workspace: tool '${name}' or command detected.`);
|
|
234
|
+
child.kill();
|
|
235
|
+
resolve2({
|
|
236
|
+
success: false,
|
|
237
|
+
commandsRun,
|
|
238
|
+
eventCount,
|
|
239
|
+
error: `Read-only confinement violation: tool '${name}' or command attempted write/modify operation during read-only task.`,
|
|
240
|
+
runtimeAgentRequested: agent || null,
|
|
241
|
+
runtimeAgentApplied: usePromptFallback ? null : null,
|
|
242
|
+
runtimeAgentSupported: inspection.supports.agent,
|
|
243
|
+
runtimeAgentSelectionMode
|
|
244
|
+
});
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
} catch {
|
|
251
|
+
console.log(trimmed);
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
child.on("close", (code) => {
|
|
255
|
+
console.log("\n[RUNTIME] OpenCode finished execution.");
|
|
256
|
+
resolve2({
|
|
257
|
+
success: code === 0,
|
|
258
|
+
commandsRun,
|
|
259
|
+
eventCount,
|
|
260
|
+
runtimeAgentRequested: agent || null,
|
|
261
|
+
runtimeAgentApplied: isSpecializedAgent && !usePromptFallback ? agent : null,
|
|
262
|
+
runtimeAgentSupported: inspection.supports.agent,
|
|
263
|
+
runtimeAgentSelectionMode
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
child.on("error", (err) => {
|
|
267
|
+
console.error(`
|
|
268
|
+
[RUNTIME] Failed to launch opencode: ${err.message}`);
|
|
269
|
+
resolve2({
|
|
270
|
+
success: false,
|
|
271
|
+
commandsRun,
|
|
272
|
+
eventCount: 0,
|
|
273
|
+
error: err.message,
|
|
274
|
+
runtimeAgentRequested: agent || null,
|
|
275
|
+
runtimeAgentApplied: null,
|
|
276
|
+
runtimeAgentSupported: inspection.supports.agent,
|
|
277
|
+
runtimeAgentSelectionMode
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
};
|
|
282
|
+
if (readOnly && !fastTrack) {
|
|
283
|
+
const { runInReadOnlyWorkspace } = await import("./read-only-workspace-PZBE7KAX.js");
|
|
284
|
+
return runInReadOnlyWorkspace(this.cwd, runInWorkspace);
|
|
285
|
+
} else {
|
|
286
|
+
return runInWorkspace(this.cwd);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
// src/core/gates/branch-gate.ts
|
|
292
|
+
import { execSync as execSync2 } from "child_process";
|
|
293
|
+
import fs from "fs";
|
|
294
|
+
import path from "path";
|
|
295
|
+
function slugify(value = "task") {
|
|
296
|
+
return String(value).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48) || "task";
|
|
297
|
+
}
|
|
298
|
+
var BranchGate = class {
|
|
299
|
+
protectedBranches;
|
|
300
|
+
logPath;
|
|
301
|
+
cwd;
|
|
302
|
+
constructor({ protectedBranches = ["main", "master"], memoryDir, cwd = process.cwd() } = {}) {
|
|
303
|
+
this.protectedBranches = protectedBranches;
|
|
304
|
+
this.logPath = memoryDir ? path.join(memoryDir, "GATE_ALERTS.log") : null;
|
|
305
|
+
this.cwd = cwd;
|
|
306
|
+
}
|
|
307
|
+
run(command) {
|
|
308
|
+
return execSync2(command, {
|
|
309
|
+
cwd: this.cwd,
|
|
310
|
+
encoding: "utf8",
|
|
311
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
312
|
+
}).trim();
|
|
313
|
+
}
|
|
314
|
+
log(event) {
|
|
315
|
+
if (!this.logPath) return;
|
|
316
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
317
|
+
const logEntry = `[${timestamp}] ${event}
|
|
318
|
+
`;
|
|
319
|
+
try {
|
|
320
|
+
if (!fs.existsSync(path.dirname(this.logPath))) fs.mkdirSync(path.dirname(this.logPath), { recursive: true });
|
|
321
|
+
fs.appendFileSync(this.logPath, logEntry);
|
|
322
|
+
} catch (error) {
|
|
323
|
+
console.error(`Failed to write to gate log: ${error.message}`);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
getDirtyState() {
|
|
327
|
+
const lines = this.run("git status --short").split("\n").filter(Boolean);
|
|
328
|
+
const tracked = lines.filter((line) => !line.startsWith("??"));
|
|
329
|
+
const untracked = lines.filter((line) => line.startsWith("??"));
|
|
330
|
+
return { clean: lines.length === 0, tracked, untracked, lines };
|
|
331
|
+
}
|
|
332
|
+
createScopedBranch(taskSlug) {
|
|
333
|
+
const base = `feat/${slugify(taskSlug)}`;
|
|
334
|
+
let candidate = base;
|
|
335
|
+
let suffix = 2;
|
|
336
|
+
while (true) {
|
|
337
|
+
try {
|
|
338
|
+
this.run(`git show-ref --verify --quiet refs/heads/${candidate}`);
|
|
339
|
+
candidate = `${base}-${suffix++}`;
|
|
340
|
+
} catch {
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
this.run(`git switch -c ${candidate}`);
|
|
345
|
+
return candidate;
|
|
346
|
+
}
|
|
347
|
+
getCurrentBranch() {
|
|
348
|
+
try {
|
|
349
|
+
return this.run("git branch --show-current") || this.run("git symbolic-ref --short HEAD") || "unknown";
|
|
350
|
+
} catch {
|
|
351
|
+
try {
|
|
352
|
+
return this.run("git symbolic-ref --short HEAD") || "unknown";
|
|
353
|
+
} catch {
|
|
354
|
+
return "unknown";
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Strictly verifies branch safety without any override bypasses.
|
|
360
|
+
*/
|
|
361
|
+
check(overrideIgnored = "", { taskSlug = "implementation", readOnly = false } = {}) {
|
|
362
|
+
if (readOnly) {
|
|
363
|
+
const currentBranch = this.getCurrentBranch();
|
|
364
|
+
return { blocked: false, branch: currentBranch, recovered: false, readOnly: true };
|
|
365
|
+
}
|
|
366
|
+
try {
|
|
367
|
+
try {
|
|
368
|
+
this.run("git rev-parse --is-inside-work-tree");
|
|
369
|
+
} catch (e) {
|
|
370
|
+
const reason = "Git is unavailable or not inside a Git repository. Implementation work is blocked.";
|
|
371
|
+
this.log(`BLOCKED: ${reason}`);
|
|
372
|
+
return { blocked: true, branch: "unknown", reason };
|
|
373
|
+
}
|
|
374
|
+
const currentBranch = this.getCurrentBranch();
|
|
375
|
+
if (!currentBranch || currentBranch === "unknown" || currentBranch.trim() === "") {
|
|
376
|
+
const reason = "Could not determine current Git branch name. Implementation work is blocked.";
|
|
377
|
+
this.log(`BLOCKED: ${reason}`);
|
|
378
|
+
return { blocked: true, branch: "unknown", reason };
|
|
379
|
+
}
|
|
380
|
+
const isProtected = this.protectedBranches.includes(currentBranch);
|
|
381
|
+
if (!isProtected) {
|
|
382
|
+
return { blocked: false, branch: currentBranch, recovered: false };
|
|
383
|
+
}
|
|
384
|
+
const dirty = this.getDirtyState();
|
|
385
|
+
if (!dirty.clean) {
|
|
386
|
+
const reason = `Direct writes to protected branch '${currentBranch}' are prohibited and the branch is dirty (has changes).`;
|
|
387
|
+
this.log(`BLOCKED: ${reason}`);
|
|
388
|
+
return { blocked: true, branch: currentBranch, reason, dirtyState: dirty };
|
|
389
|
+
}
|
|
390
|
+
const recoveredBranch = this.createScopedBranch(taskSlug);
|
|
391
|
+
this.log(`AUTO-RECOVERED '${currentBranch}' -> '${recoveredBranch}'`);
|
|
392
|
+
return {
|
|
393
|
+
blocked: false,
|
|
394
|
+
branch: recoveredBranch,
|
|
395
|
+
branchBefore: currentBranch,
|
|
396
|
+
recovered: true,
|
|
397
|
+
dirtyState: dirty
|
|
398
|
+
};
|
|
399
|
+
} catch (error) {
|
|
400
|
+
const reason = `Git command failure on branch gate check: ${error.message}`;
|
|
401
|
+
this.log(`BLOCKED: ${reason}`);
|
|
402
|
+
return { blocked: true, branch: "unknown", error: error.message, reason, recovered: false };
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
// src/core/sdd/validator.ts
|
|
408
|
+
import fs2 from "fs/promises";
|
|
409
|
+
var SpecValidator = class {
|
|
410
|
+
/**
|
|
411
|
+
* Validates a specification file.
|
|
412
|
+
*/
|
|
413
|
+
async validate(filePath) {
|
|
414
|
+
try {
|
|
415
|
+
const content = await fs2.readFile(filePath, "utf8");
|
|
416
|
+
let tier = "unknown";
|
|
417
|
+
if (content.includes("[DEEP]")) tier = "deep";
|
|
418
|
+
else if (content.includes("[STANDARD]")) tier = "standard";
|
|
419
|
+
else if (content.includes("[TINY]")) tier = "tiny";
|
|
420
|
+
if (tier === "unknown") {
|
|
421
|
+
return { valid: false, reason: "Specification tier [DEEP|STANDARD|TINY] not identified in title." };
|
|
422
|
+
}
|
|
423
|
+
if (!content.includes("## Metadata")) {
|
|
424
|
+
return { valid: false, reason: "Missing '## Metadata' section.", tier };
|
|
425
|
+
}
|
|
426
|
+
if (!content.includes("## Acceptance Criteria")) {
|
|
427
|
+
return { valid: false, reason: "Missing '## Acceptance Criteria' section.", tier };
|
|
428
|
+
}
|
|
429
|
+
const lines = content.split("\n");
|
|
430
|
+
const statusLine = lines.find((l) => l.includes("Status:"));
|
|
431
|
+
if (!statusLine) {
|
|
432
|
+
return { valid: false, reason: "Missing Status field in Metadata section.", tier };
|
|
433
|
+
}
|
|
434
|
+
const statusPart = statusLine.split(":")[1].replace(/[\*_]/g, "").trim().toUpperCase();
|
|
435
|
+
if (statusPart.includes("|")) {
|
|
436
|
+
return { valid: false, reason: "Specification status is a template list. Must be explicitly set to 'APPROVED'.", tier };
|
|
437
|
+
}
|
|
438
|
+
if (statusPart !== "APPROVED") {
|
|
439
|
+
return { valid: false, reason: `Specification status is '${statusPart}', but must be 'APPROVED' to proceed.`, tier };
|
|
440
|
+
}
|
|
441
|
+
return { valid: true, tier };
|
|
442
|
+
} catch (error) {
|
|
443
|
+
if (error.code === "ENOENT") {
|
|
444
|
+
return { valid: false, reason: `Specification file not found: ${filePath}` };
|
|
445
|
+
}
|
|
446
|
+
return { valid: false, reason: `Error reading specification: ${error.message}` };
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
// src/core/handoff/handoff-engine.ts
|
|
452
|
+
import { execSync as execSync3 } from "child_process";
|
|
453
|
+
import fs3 from "fs/promises";
|
|
454
|
+
import path2 from "path";
|
|
455
|
+
var HandoffEngine = class {
|
|
456
|
+
cwd;
|
|
457
|
+
constructor({ cwd }) {
|
|
458
|
+
this.cwd = cwd;
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Generates a handoff packet.
|
|
462
|
+
*/
|
|
463
|
+
async generate({ taskId, author, status, specPaths = [], nextActions, evidence = null }) {
|
|
464
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
465
|
+
let specsContent = "";
|
|
466
|
+
for (const specPath of specPaths) {
|
|
467
|
+
const absolutePath = path2.isAbsolute(specPath) ? specPath : path2.join(this.cwd, specPath);
|
|
468
|
+
try {
|
|
469
|
+
const content = await fs3.readFile(absolutePath, "utf8");
|
|
470
|
+
specsContent += `### File: ${specPath}
|
|
471
|
+
|
|
472
|
+
${content}
|
|
473
|
+
|
|
474
|
+
`;
|
|
475
|
+
} catch (err) {
|
|
476
|
+
specsContent += `### File: ${specPath}
|
|
477
|
+
|
|
478
|
+
[Error reading file: ${err.message}]
|
|
479
|
+
|
|
480
|
+
`;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
let gitDiff = "No changes detected.";
|
|
484
|
+
try {
|
|
485
|
+
gitDiff = execSync3("git diff HEAD", { cwd: this.cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() || "No changes detected.";
|
|
486
|
+
} catch (err) {
|
|
487
|
+
gitDiff = `[Error capturing diff: ${err.message}]`;
|
|
488
|
+
}
|
|
489
|
+
let evidenceJson = "{}";
|
|
490
|
+
let evidenceSummary = "No evidence found.";
|
|
491
|
+
if (evidence) {
|
|
492
|
+
const data = typeof evidence === "string" ? JSON.parse(evidence) : evidence;
|
|
493
|
+
evidenceJson = JSON.stringify(data, null, 2);
|
|
494
|
+
evidenceSummary = `Status: ${data.status || data.internalStatus || "unknown"}, Commands: ${data.commands?.length || 0}`;
|
|
495
|
+
} else {
|
|
496
|
+
const evidencePath = path2.join(this.cwd, "EVIDENCE.json");
|
|
497
|
+
try {
|
|
498
|
+
const content = await fs3.readFile(evidencePath, "utf8");
|
|
499
|
+
const data = JSON.parse(content);
|
|
500
|
+
evidenceJson = JSON.stringify(data, null, 2);
|
|
501
|
+
evidenceSummary = `Status: ${data.status}, Commands: ${data.commands?.length || 0}`;
|
|
502
|
+
} catch (err) {
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
const possibleTemplatePaths = [
|
|
506
|
+
path2.join(this.cwd, ".ai-workflow/templates/HANDOFF.template.md"),
|
|
507
|
+
path2.join(this.cwd, ".ai-workflow/harness/handoffs/HANDOFF.template.md"),
|
|
508
|
+
path2.join(getPackageRoot(), "dist-assets/templates/HANDOFF.template.md"),
|
|
509
|
+
path2.join(getPackageRoot(), "dist-assets/harness/handoffs/HANDOFF.template.md")
|
|
510
|
+
];
|
|
511
|
+
let template = null;
|
|
512
|
+
let lastError = null;
|
|
513
|
+
for (const tPath of possibleTemplatePaths) {
|
|
514
|
+
try {
|
|
515
|
+
template = await fs3.readFile(tPath, "utf8");
|
|
516
|
+
break;
|
|
517
|
+
} catch (err) {
|
|
518
|
+
lastError = err;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
if (!template) {
|
|
522
|
+
throw new Error(`CRITICAL FAILURE: Could not locate HANDOFF.template.md. Tried paths:
|
|
523
|
+
${possibleTemplatePaths.join("\n")}
|
|
524
|
+
Last error: ${lastError?.message}`);
|
|
525
|
+
}
|
|
526
|
+
const packet = template.replace("${TASK_ID}", taskId || "unknown").replace("${TIMESTAMP}", timestamp).replace("${AUTHOR}", author || "AI Workflow Kit").replace("${STATUS}", status || "IN_PROGRESS").replace("${SPECS_CONTENT}", specsContent || "No specs provided.").replace("${EVIDENCE_SUMMARY}", evidenceSummary).replace("${EVIDENCE_JSON}", evidenceJson).replace("${GIT_DIFF}", gitDiff).replace("${NEXT_ACTIONS}", nextActions || "Awaiting manager assignment.");
|
|
527
|
+
const handoffDir = path2.join(this.cwd, ".ai-workflow/handoffs");
|
|
528
|
+
await fs3.mkdir(handoffDir, { recursive: true });
|
|
529
|
+
const packetName = `HANDOFF-${taskId || "TMP"}-${Date.now()}.md`;
|
|
530
|
+
const packetPath = path2.join(handoffDir, packetName);
|
|
531
|
+
await fs3.writeFile(packetPath, packet);
|
|
532
|
+
return packetPath;
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
// src/core/healing/healer-engine.ts
|
|
537
|
+
import fs4 from "fs/promises";
|
|
538
|
+
import path3 from "path";
|
|
539
|
+
import crypto from "crypto";
|
|
540
|
+
|
|
541
|
+
// src/core/statuses.ts
|
|
542
|
+
var COMPLETION_STATUSES = Object.freeze([
|
|
543
|
+
"PASS",
|
|
544
|
+
"PASS_WITH_NOTES",
|
|
545
|
+
"FAIL_DELEGATION_GATE",
|
|
546
|
+
"FAIL_QUALITY_GATE",
|
|
547
|
+
"FAIL",
|
|
548
|
+
"BLOCKED",
|
|
549
|
+
"NOT_RUN"
|
|
550
|
+
]);
|
|
551
|
+
var RECOVERABLE_GATE_STATUSES = Object.freeze([
|
|
552
|
+
"FAIL_DELEGATION_GATE",
|
|
553
|
+
"FAIL_QUALITY_GATE"
|
|
554
|
+
]);
|
|
555
|
+
var TERMINAL_FAILURE_STATUSES = Object.freeze([
|
|
556
|
+
"FAIL",
|
|
557
|
+
"BLOCKED"
|
|
558
|
+
]);
|
|
559
|
+
function isRecoverableGateFailure(status) {
|
|
560
|
+
return RECOVERABLE_GATE_STATUSES.includes(status);
|
|
561
|
+
}
|
|
562
|
+
function isTerminalFailure(status) {
|
|
563
|
+
return TERMINAL_FAILURE_STATUSES.includes(status);
|
|
564
|
+
}
|
|
565
|
+
var COMPLETION_STATUS_TEXT = COMPLETION_STATUSES.join(" | ");
|
|
566
|
+
|
|
567
|
+
// src/core/healing/healer-engine.ts
|
|
568
|
+
function stableJson(value) {
|
|
569
|
+
if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
|
|
570
|
+
if (value && typeof value === "object") {
|
|
571
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(",")}}`;
|
|
572
|
+
}
|
|
573
|
+
return JSON.stringify(value);
|
|
574
|
+
}
|
|
575
|
+
function collectBlockingFindings(result = {}) {
|
|
576
|
+
const findings = [];
|
|
577
|
+
const add = (source, value) => {
|
|
578
|
+
if (!value || typeof value !== "object") return;
|
|
579
|
+
const status = value.status || value.overallStatus;
|
|
580
|
+
if (!["FAIL", "FAIL_QUALITY_GATE", "FAIL_DELEGATION_GATE", "BLOCKED"].includes(status)) return;
|
|
581
|
+
findings.push({
|
|
582
|
+
source,
|
|
583
|
+
status,
|
|
584
|
+
missing: Array.isArray(value.missing) ? [...value.missing].sort() : [],
|
|
585
|
+
reason: value.reason || value.summary || ""
|
|
586
|
+
});
|
|
587
|
+
};
|
|
588
|
+
add("result", result);
|
|
589
|
+
add("evidence", result.evidence);
|
|
590
|
+
add("quality", result.quality);
|
|
591
|
+
const checks = result.quality?.checks || result.policyValidation?.checks || result.checks || {};
|
|
592
|
+
for (const [name, check] of Object.entries(checks)) add(`check:${name}`, check);
|
|
593
|
+
return findings.sort((a, b) => a.source.localeCompare(b.source));
|
|
594
|
+
}
|
|
595
|
+
var HealerEngine = class {
|
|
596
|
+
cwd;
|
|
597
|
+
/** @deprecated use remediationLimit instead */
|
|
598
|
+
mode;
|
|
599
|
+
taskSlug;
|
|
600
|
+
maxAttempts;
|
|
601
|
+
ledger;
|
|
602
|
+
constructor({ cwd, mode, taskSlug = "implementation", maxAttempts = null, remediationLimit = null, ledger = null }) {
|
|
603
|
+
this.cwd = cwd;
|
|
604
|
+
this.taskSlug = taskSlug;
|
|
605
|
+
const legacyLimit = mode === "full" ? 3 : mode === "quick" ? 1 : mode === "standard" ? 2 : null;
|
|
606
|
+
this.maxAttempts = remediationLimit ?? maxAttempts ?? legacyLimit ?? 2;
|
|
607
|
+
this.mode = this.maxAttempts === 3 ? "full" : this.maxAttempts === 1 ? "quick" : "standard";
|
|
608
|
+
this.ledger = ledger;
|
|
609
|
+
}
|
|
610
|
+
get statePath() {
|
|
611
|
+
return path3.join(this.cwd, ".ai-workflow", `healing-state-${this.taskSlug}.json`);
|
|
612
|
+
}
|
|
613
|
+
fingerprint(result) {
|
|
614
|
+
return crypto.createHash("sha256").update(stableJson(collectBlockingFindings(result))).digest("hex");
|
|
615
|
+
}
|
|
616
|
+
blockingCount(result) {
|
|
617
|
+
return collectBlockingFindings(result).length;
|
|
618
|
+
}
|
|
619
|
+
affectedChecks(result) {
|
|
620
|
+
return collectBlockingFindings(result).map((finding) => finding.source);
|
|
621
|
+
}
|
|
622
|
+
hasProgress(before, after, remediation = {}) {
|
|
623
|
+
if (!isRecoverableGateFailure(after.overallStatus)) return true;
|
|
624
|
+
const beforeCount = this.blockingCount(before);
|
|
625
|
+
const afterCount = this.blockingCount(after);
|
|
626
|
+
if (afterCount < beforeCount) return true;
|
|
627
|
+
if (this.fingerprint(before) !== this.fingerprint(after) && (remediation.changedFiles?.length || remediation.evidenceAdded?.length)) return true;
|
|
628
|
+
return false;
|
|
629
|
+
}
|
|
630
|
+
async writeState(state) {
|
|
631
|
+
await fs4.mkdir(path3.dirname(this.statePath), { recursive: true });
|
|
632
|
+
await fs4.writeFile(this.statePath, JSON.stringify(state, null, 2));
|
|
633
|
+
}
|
|
634
|
+
async resetState() {
|
|
635
|
+
try {
|
|
636
|
+
await fs4.unlink(this.statePath);
|
|
637
|
+
} catch {
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
async writeRemediationRequest({ attempt, result }) {
|
|
641
|
+
const requestPath = path3.join(this.cwd, ".ai-workflow", "remediation-request.json");
|
|
642
|
+
await fs4.mkdir(path3.dirname(requestPath), { recursive: true });
|
|
643
|
+
const request = {
|
|
644
|
+
taskSlug: this.taskSlug,
|
|
645
|
+
executionMode: this.mode,
|
|
646
|
+
attempt,
|
|
647
|
+
maxAttempts: this.maxAttempts,
|
|
648
|
+
status: result.overallStatus,
|
|
649
|
+
affectedChecks: this.affectedChecks(result),
|
|
650
|
+
findingsFingerprint: this.fingerprint(result),
|
|
651
|
+
instruction: "Apply the smallest safe correction for the listed gates, then rerun validation. Do not broaden scope."
|
|
652
|
+
};
|
|
653
|
+
await fs4.writeFile(requestPath, JSON.stringify(request, null, 2));
|
|
654
|
+
return requestPath;
|
|
655
|
+
}
|
|
656
|
+
async run({
|
|
657
|
+
initialResult,
|
|
658
|
+
validate,
|
|
659
|
+
remediate
|
|
660
|
+
}) {
|
|
661
|
+
let current = initialResult;
|
|
662
|
+
const history = [];
|
|
663
|
+
const seen = /* @__PURE__ */ new Set();
|
|
664
|
+
if (!isRecoverableGateFailure(current.overallStatus)) {
|
|
665
|
+
await this.resetState();
|
|
666
|
+
return { ...current, remediation: { status: "NOT_REQUIRED", attempts: 0, history } };
|
|
667
|
+
}
|
|
668
|
+
for (let attempt = 1; attempt <= this.maxAttempts; attempt += 1) {
|
|
669
|
+
const fingerprint = this.fingerprint(current);
|
|
670
|
+
if (seen.has(fingerprint)) {
|
|
671
|
+
const blocked = { overallStatus: "BLOCKED", reason: "Repeated unresolved finding", previousResult: current };
|
|
672
|
+
await this.writeState({ status: "BLOCKED", reason: blocked.reason, attempts: attempt - 1, history });
|
|
673
|
+
return { ...blocked, remediation: { status: "BLOCKED", attempts: attempt - 1, history } };
|
|
674
|
+
}
|
|
675
|
+
seen.add(fingerprint);
|
|
676
|
+
const requestPath = await this.writeRemediationRequest({ attempt, result: current });
|
|
677
|
+
if (typeof remediate !== "function") {
|
|
678
|
+
const blocked = { overallStatus: "BLOCKED", reason: "No remediation executor available", previousResult: current, requestPath };
|
|
679
|
+
await this.writeState({ status: "BLOCKED", reason: blocked.reason, attempts: attempt - 1, history, requestPath });
|
|
680
|
+
return { ...blocked, remediation: { status: "BLOCKED", attempts: attempt - 1, history, requestPath } };
|
|
681
|
+
}
|
|
682
|
+
const remediation = await remediate({
|
|
683
|
+
attempt,
|
|
684
|
+
maxAttempts: this.maxAttempts,
|
|
685
|
+
mode: this.mode,
|
|
686
|
+
taskSlug: this.taskSlug,
|
|
687
|
+
result: current,
|
|
688
|
+
affectedChecks: this.affectedChecks(current),
|
|
689
|
+
requestPath
|
|
690
|
+
});
|
|
691
|
+
if (!remediation?.applied) {
|
|
692
|
+
const blocked = { overallStatus: "BLOCKED", reason: remediation?.reason || "Remediation was not applied", previousResult: current, requestPath };
|
|
693
|
+
history.push({ attempt, before: current.overallStatus, remediation, after: "NOT_RUN", progress: false });
|
|
694
|
+
await this.writeState({ status: "BLOCKED", reason: blocked.reason, attempts: attempt, history });
|
|
695
|
+
return { ...blocked, remediation: { status: "BLOCKED", attempts: attempt, history } };
|
|
696
|
+
}
|
|
697
|
+
const next = await validate({ affectedChecks: this.affectedChecks(current), attempt });
|
|
698
|
+
const progress = this.hasProgress(current, next, remediation);
|
|
699
|
+
const findingsConcrete = this.affectedChecks(current).length > 0;
|
|
700
|
+
const hashAltered = remediation.changedFiles && remediation.changedFiles.length > 0;
|
|
701
|
+
const inScope = hashAltered && remediation.changedFiles.every((file) => {
|
|
702
|
+
const norm = file.replaceAll("\\", "/");
|
|
703
|
+
if (norm.startsWith(".ai-workflow/")) return false;
|
|
704
|
+
if (norm === "package.json" || norm === "package-lock.json" || norm === "tsconfig.json" || norm === ".gitignore" || norm === "opencode.jsonc" || norm === "EVIDENCE.json") {
|
|
705
|
+
return false;
|
|
706
|
+
}
|
|
707
|
+
return true;
|
|
708
|
+
});
|
|
709
|
+
const validationPassed = next.overallStatus === "PASS" || next.overallStatus === "PASS_WITH_NOTES";
|
|
710
|
+
const phoenixSuccessful = findingsConcrete && hashAltered && inScope && validationPassed;
|
|
711
|
+
if (this.ledger && phoenixSuccessful) {
|
|
712
|
+
this.ledger.logEvent({
|
|
713
|
+
actor: "Phoenix",
|
|
714
|
+
actorType: "runtime-agent",
|
|
715
|
+
observed: true,
|
|
716
|
+
runtime: "opencode",
|
|
717
|
+
eventType: "remediation_complete",
|
|
718
|
+
provenance: "opencode-adapter",
|
|
719
|
+
data: {
|
|
720
|
+
success: true,
|
|
721
|
+
changedFiles: remediation.changedFiles,
|
|
722
|
+
event: "remediation.completed"
|
|
723
|
+
}
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
history.push({
|
|
727
|
+
attempt,
|
|
728
|
+
before: current.overallStatus,
|
|
729
|
+
after: next.overallStatus,
|
|
730
|
+
progress,
|
|
731
|
+
changedFiles: remediation.changedFiles || [],
|
|
732
|
+
evidenceAdded: remediation.evidenceAdded || [],
|
|
733
|
+
affectedChecks: this.affectedChecks(current)
|
|
734
|
+
});
|
|
735
|
+
await this.writeState({ status: next.overallStatus, attempts: attempt, history });
|
|
736
|
+
if (!isRecoverableGateFailure(next.overallStatus)) {
|
|
737
|
+
await this.resetState();
|
|
738
|
+
return { ...next, remediation: { status: "RESOLVED", attempts: attempt, history } };
|
|
739
|
+
}
|
|
740
|
+
if (!progress) {
|
|
741
|
+
return {
|
|
742
|
+
overallStatus: "BLOCKED",
|
|
743
|
+
reason: "Remediation made no measurable progress",
|
|
744
|
+
previousResult: next,
|
|
745
|
+
remediation: { status: "BLOCKED", attempts: attempt, history }
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
current = next;
|
|
749
|
+
}
|
|
750
|
+
return {
|
|
751
|
+
overallStatus: "BLOCKED",
|
|
752
|
+
reason: `Quality remediation attempts exhausted (limit: ${this.maxAttempts})`,
|
|
753
|
+
previousResult: current,
|
|
754
|
+
remediation: { status: "BLOCKED", attempts: this.maxAttempts, history }
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
};
|
|
758
|
+
|
|
759
|
+
// src/core/workflow-profiles.ts
|
|
760
|
+
var PROFILE_DEFINITIONS = Object.freeze({
|
|
761
|
+
"frontend-product": Object.freeze({
|
|
762
|
+
owner: "Astra",
|
|
763
|
+
skills: ["frontend-development", "ui-ux-design", "frontend-design-system"],
|
|
764
|
+
objective: "Deliver a user-facing product or marketing surface with truthful copy and deliberate visual composition.",
|
|
765
|
+
requiredChecks: ["tests", "typecheck", "build", "responsive-render", "primary-interaction", "truthfulness"],
|
|
766
|
+
forbiddenAssumptions: ["fixed SaaS section sequence", "mandatory pricing", "mandatory testimonials", "subjective premium score"]
|
|
767
|
+
}),
|
|
768
|
+
"frontend-utility": Object.freeze({
|
|
769
|
+
owner: "Astra",
|
|
770
|
+
skills: ["frontend-development", "ui-ux-design", "frontend-design-system"],
|
|
771
|
+
objective: "Deliver a focused user tool with a complete primary flow and clear operational states.",
|
|
772
|
+
requiredChecks: ["tests", "typecheck", "build", "responsive-render", "primary-interaction"],
|
|
773
|
+
forbiddenAssumptions: ["marketing sections", "pricing", "testimonials", "commercial narrative"]
|
|
774
|
+
}),
|
|
775
|
+
"backend-api": Object.freeze({
|
|
776
|
+
owner: "Astra",
|
|
777
|
+
skills: ["backend-development"],
|
|
778
|
+
objective: "Deliver a bounded backend/API change with validated contracts, errors, authorization, and tests.",
|
|
779
|
+
requiredChecks: ["tests", "build-or-typecheck", "input-validation", "failure-paths"],
|
|
780
|
+
forbiddenAssumptions: ["frontend deliverables", "visual evidence"]
|
|
781
|
+
}),
|
|
782
|
+
refactor: Object.freeze({
|
|
783
|
+
owner: "Astra",
|
|
784
|
+
skills: ["refactoring"],
|
|
785
|
+
objective: "Improve structure without changing observable behavior outside the approved scope.",
|
|
786
|
+
requiredChecks: ["behavior-preservation", "tests", "focused-diff"],
|
|
787
|
+
forbiddenAssumptions: ["new product behavior", "unrelated cleanup"]
|
|
788
|
+
}),
|
|
789
|
+
documentation: Object.freeze({
|
|
790
|
+
owner: "Astra",
|
|
791
|
+
skills: ["documentation"],
|
|
792
|
+
objective: "Create or update accurate documentation grounded in the current repository behavior.",
|
|
793
|
+
requiredChecks: ["references", "paths", "commands", "consistency"],
|
|
794
|
+
forbiddenAssumptions: ["implementation claims without evidence"]
|
|
795
|
+
}),
|
|
796
|
+
"security-review": Object.freeze({
|
|
797
|
+
owner: "Sage",
|
|
798
|
+
skills: ["qa-workflow", "technical-leadership"],
|
|
799
|
+
objective: "Inspect security-relevant behavior and report evidence-ranked findings without implementing unrelated changes.",
|
|
800
|
+
requiredChecks: ["evidence", "severity", "runtime-vs-dev", "remediation-owner"],
|
|
801
|
+
forbiddenAssumptions: ["automatic force fixes", "unverified exploitability"]
|
|
802
|
+
}),
|
|
803
|
+
generic: Object.freeze({
|
|
804
|
+
owner: "Atlas",
|
|
805
|
+
skills: [],
|
|
806
|
+
objective: "Route work without inventing domain requirements.",
|
|
807
|
+
requiredChecks: ["scope", "owner", "mode"],
|
|
808
|
+
forbiddenAssumptions: ["domain-specific structure"]
|
|
809
|
+
})
|
|
810
|
+
});
|
|
811
|
+
var PROFILE_PATTERNS = Object.freeze([
|
|
812
|
+
["security-review", /\b(?:security review|security audit|vulnerability audit|threat model)\b/i],
|
|
813
|
+
["refactor", /\b(?:refactor|restructure|cleanup without behavior change|preserve behavior)\b/i],
|
|
814
|
+
["documentation", /\b(?:documentation only|write docs|update readme|document the|documentation task)\b/i],
|
|
815
|
+
["backend-api", /\b(?:api endpoint|backend api|rest api|graphql|controller|service endpoint|authenticated api)\b/i],
|
|
816
|
+
["frontend-product", /\b(?:landing page|marketing page|product page|pricing page|homepage|redesign|brand page)\b/i],
|
|
817
|
+
["frontend-utility", /\b(?:validator|search page|lookup|dashboard|form|calculator|tool|admin page|repository search)\b/i]
|
|
818
|
+
]);
|
|
819
|
+
function getWorkflowProfile(name = "generic") {
|
|
820
|
+
return PROFILE_DEFINITIONS[name] || PROFILE_DEFINITIONS.generic;
|
|
821
|
+
}
|
|
822
|
+
function resolveWorkflowProfile({ request = "", explicitProfile = null } = {}) {
|
|
823
|
+
if (explicitProfile && PROFILE_DEFINITIONS[explicitProfile]) return explicitProfile;
|
|
824
|
+
const text = String(request).trim();
|
|
825
|
+
for (const [profile, pattern] of PROFILE_PATTERNS) {
|
|
826
|
+
if (pattern.test(text)) return profile;
|
|
827
|
+
}
|
|
828
|
+
return "generic";
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// src/core/request-router.ts
|
|
832
|
+
import path4 from "path";
|
|
833
|
+
import fs5 from "fs";
|
|
834
|
+
var RequestRouter = class {
|
|
835
|
+
cwd;
|
|
836
|
+
constructor({ cwd = process.cwd() } = {}) {
|
|
837
|
+
this.cwd = cwd;
|
|
838
|
+
}
|
|
839
|
+
/**
|
|
840
|
+
* Understands and classifies a natural language request.
|
|
841
|
+
*/
|
|
842
|
+
understand(request) {
|
|
843
|
+
const text = String(request).trim();
|
|
844
|
+
if (!text) {
|
|
845
|
+
throw new Error("Cannot classify an empty request.");
|
|
846
|
+
}
|
|
847
|
+
const { operationType, requestedCapability } = this.extractCapability(text);
|
|
848
|
+
const mutationIntent = this.determineMutationIntent(text, operationType);
|
|
849
|
+
const riskAssessment = this.determineRiskAssessment(text, mutationIntent);
|
|
850
|
+
const riskLevel = riskAssessment.riskLevel;
|
|
851
|
+
const { safetyConstraints, requiredEvidence } = this.determineSafetyAndEvidence(text, mutationIntent);
|
|
852
|
+
return {
|
|
853
|
+
rawRequest: text,
|
|
854
|
+
taskGoal: `Perform ${operationType} operation under ${mutationIntent} intent`,
|
|
855
|
+
requestedActor: void 0,
|
|
856
|
+
invalidActor: void 0,
|
|
857
|
+
requestedCapability,
|
|
858
|
+
operationType,
|
|
859
|
+
mutationIntent,
|
|
860
|
+
riskLevel,
|
|
861
|
+
requiredEvidence,
|
|
862
|
+
safetyConstraints,
|
|
863
|
+
specPolicy: riskAssessment.specPolicy,
|
|
864
|
+
matchedSignals: riskAssessment.matchedSignals,
|
|
865
|
+
riskDrivers: riskAssessment.riskDrivers
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
extractCapability(text) {
|
|
869
|
+
let operationType = "explain";
|
|
870
|
+
let requestedCapability = void 0;
|
|
871
|
+
const capabilityMappings = [
|
|
872
|
+
{ op: "discover", pattern: /\b(discover|inspect|map|scan|structure|architecture|repo|search|find)\b/i },
|
|
873
|
+
{ op: "validate", pattern: /\b(validate|audit|review|check|verify|test)\b/i },
|
|
874
|
+
{ op: "implement", pattern: /\b(implement|implementa|build|create|crie|cria|add|develop|write|change|modify|atualiza|atualizar|refatora|refatorar)\b/i },
|
|
875
|
+
{ op: "fix", pattern: /\b(fix|repair|correct|corrige|corrigir|heal|solve|remediate)\b/i },
|
|
876
|
+
{ op: "release", pattern: /\b(release|tag|prepare release|prepara(?:r)?\s+(?:a\s+)?release)\b/i },
|
|
877
|
+
{ op: "deploy", pattern: /\b(deploy|publish|push|produção|producao)\b/i }
|
|
878
|
+
];
|
|
879
|
+
for (const mapping of capabilityMappings) {
|
|
880
|
+
const match = text.match(mapping.pattern);
|
|
881
|
+
if (match) {
|
|
882
|
+
operationType = mapping.op;
|
|
883
|
+
requestedCapability = match[1];
|
|
884
|
+
break;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
return { operationType, requestedCapability };
|
|
888
|
+
}
|
|
889
|
+
determineMutationIntent(text, operationType) {
|
|
890
|
+
const writePatterns = /\b(implement|implementa|build|fix|corrige|write|change|modify|create|crie|cria|add|refactor|refatora|refatorar|update|atualiza|atualizar)\b/i;
|
|
891
|
+
const readOnlyPatterns = /\b(analyze|check|inspect|review|find|search|show|list|read|view|verify|validate|discover|map)\b/i;
|
|
892
|
+
let mutationIntent = "write";
|
|
893
|
+
if (/\b(read-only|readonly)\b/i.test(text)) {
|
|
894
|
+
mutationIntent = "readonly";
|
|
895
|
+
} else if (writePatterns.test(text)) {
|
|
896
|
+
mutationIntent = "write";
|
|
897
|
+
} else if (readOnlyPatterns.test(text) || ["discover", "validate"].includes(operationType)) {
|
|
898
|
+
mutationIntent = "readonly";
|
|
899
|
+
}
|
|
900
|
+
if (/\b(publish|release|deploy|produção|producao|push to npm|push to remote)\b/i.test(text)) {
|
|
901
|
+
mutationIntent = "publish";
|
|
902
|
+
}
|
|
903
|
+
return mutationIntent;
|
|
904
|
+
}
|
|
905
|
+
determineRiskAssessment(text, mutationIntent) {
|
|
906
|
+
const matchedSignals = [];
|
|
907
|
+
const add = (signal, pattern) => {
|
|
908
|
+
if (pattern.test(text)) {
|
|
909
|
+
matchedSignals.push(signal);
|
|
910
|
+
return true;
|
|
911
|
+
}
|
|
912
|
+
return false;
|
|
913
|
+
};
|
|
914
|
+
const taskTypeSignals = {
|
|
915
|
+
docsCopy: add("task:docs-copy", /\b(README|docs?|documenta[cç][aã]o|typo|copy|frase|comment|coment[áa]rio)\b/i),
|
|
916
|
+
refactor: add("task:refactor", /\b(refactor|refatora|refatorar|simplificar|mais simples de manter|manuten[cç][aã]o)\b/i),
|
|
917
|
+
noBehaviorChange: add("scope:no-behavior-change", /\b(sem alterar (?:o )?comportamento|without changing behavior|without behaviour change|no behavior change|comportamento existente)\b/i),
|
|
918
|
+
landingPage: add("task:landing-page", /\b(landing page|hotsite|p[áa]gina de venda|p[áa]gina comercial)\b/i),
|
|
919
|
+
dashboard: add("task:dashboard", /\b(dashboard|painel)\b/i),
|
|
920
|
+
bugfix: add("task:bugfix", /\b(bug|fix|corrige|corrigir|erro|falha|n[aã]o envia|não envia)\b/i),
|
|
921
|
+
release: add("task:release", /\b(release|deploy|produ[cç][aã]o|publish|publicar|lan[cç]amento)\b/i)
|
|
922
|
+
};
|
|
923
|
+
const domainRiskSignals = {
|
|
924
|
+
adult: add("domain:adult", /\b(er[óo]tic[oa]s?|adult[oa]s?|sexual|sexo|porn(?:o|ografia)?|18\+)\b/i),
|
|
925
|
+
auth: add("domain:auth", /\b(login|auth|autentica[cç][aã]o|autoriza[cç][aã]o|permiss(?:ão|oes|ões)|admin|administrador|roles?|rbac)\b/i),
|
|
926
|
+
payment: add("domain:payment", /\b(pagamento|checkout|billing|cobran[cç]a|fatura[cç][aã]o|assinatura|cart[aã]o|stripe|paypal)\b/i),
|
|
927
|
+
securityGovernance: add("domain:security-governance", /\b(SECURITY\.md|seguran[cç]a|security policy|pol[ií]tica de seguran[cç]a|governance|governan[cç]a|compliance|branch safety)\b/i)
|
|
928
|
+
};
|
|
929
|
+
const impactSignals = {
|
|
930
|
+
personalData: add("impact:personal-data", /\b(leads?|dados pessoais|personal data|crm|contatos?|contactos?|clientes?|exporta[cç][aã]o|exportar|armazenamento|armazenar|integra[cç][aã]o|integrar)\b/i),
|
|
931
|
+
productionRelease: mutationIntent === "publish" || add("impact:production-release", /\b(produ[cç][aã]o|release|deploy|publish|publicar|npm)\b/i),
|
|
932
|
+
adminPermissions: add("impact:admin-permissions", /\b(admin|administrador|permiss(?:ão|oes|ões)|roles?|rbac)\b/i)
|
|
933
|
+
};
|
|
934
|
+
if (mutationIntent === "publish" && !matchedSignals.includes("impact:production-release")) {
|
|
935
|
+
matchedSignals.push("impact:production-release");
|
|
936
|
+
}
|
|
937
|
+
const scopeSignals = {
|
|
938
|
+
localized: add("scope:localized", /\b(localizado|uma frase|typo|pequeno|small|tiny|simples|README)\b/i),
|
|
939
|
+
broad: add("scope:broad", /\b(plataforma|sistema|completo|premium|end-to-end|amplo|v[aá]rias|m[úu]ltiplas|empresa)\b/i),
|
|
940
|
+
architectural: add("scope:architectural", /\b(deep|architectural|arquitetural|migration|migra[cç][aã]o|major|full|\[deep\])\b/i),
|
|
941
|
+
explicitHigh: add("scope:explicit-high", /\b(high-risk|high risk|alto risco)\b/i),
|
|
942
|
+
explicitRequiredSpec: add("scope:explicit-required-spec", /\b(formal spec|required spec|especifica[cç][aã]o formal)\b/i),
|
|
943
|
+
explicitRequiredEvidence: add("scope:explicit-required-evidence", /\b(required evidence|evidence required|evid[eê]ncia obrigat[óo]ria)\b/i)
|
|
944
|
+
};
|
|
945
|
+
let riskLevel = "medium";
|
|
946
|
+
const riskDrivers = [];
|
|
947
|
+
const highDrivers = [
|
|
948
|
+
["scope:explicit-high", scopeSignals.explicitHigh],
|
|
949
|
+
["scope:architectural", scopeSignals.architectural],
|
|
950
|
+
["domain:adult", domainRiskSignals.adult],
|
|
951
|
+
["domain:auth", domainRiskSignals.auth],
|
|
952
|
+
["domain:payment", domainRiskSignals.payment],
|
|
953
|
+
["domain:security-governance", domainRiskSignals.securityGovernance],
|
|
954
|
+
["impact:production-release", impactSignals.productionRelease],
|
|
955
|
+
["impact:admin-permissions", impactSignals.adminPermissions],
|
|
956
|
+
["impact:personal-data", taskTypeSignals.dashboard && impactSignals.personalData]
|
|
957
|
+
];
|
|
958
|
+
for (const [signal, active] of highDrivers) {
|
|
959
|
+
if (active) {
|
|
960
|
+
riskDrivers.push(signal);
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
if (riskDrivers.length > 0) {
|
|
964
|
+
riskLevel = "high";
|
|
965
|
+
} else if (mutationIntent === "readonly") {
|
|
966
|
+
riskLevel = "low";
|
|
967
|
+
riskDrivers.push("intent:readonly");
|
|
968
|
+
} else if (taskTypeSignals.docsCopy && scopeSignals.localized) {
|
|
969
|
+
riskLevel = "low";
|
|
970
|
+
riskDrivers.push("task:docs-copy");
|
|
971
|
+
} else if (taskTypeSignals.refactor || taskTypeSignals.bugfix || taskTypeSignals.landingPage || taskTypeSignals.dashboard) {
|
|
972
|
+
riskLevel = "medium";
|
|
973
|
+
if (taskTypeSignals.refactor) riskDrivers.push("task:refactor");
|
|
974
|
+
if (taskTypeSignals.bugfix) riskDrivers.push("task:bugfix");
|
|
975
|
+
if (taskTypeSignals.landingPage) riskDrivers.push("task:landing-page");
|
|
976
|
+
if (taskTypeSignals.dashboard) riskDrivers.push("task:dashboard");
|
|
977
|
+
}
|
|
978
|
+
const specPolicy = riskLevel === "high" || scopeSignals.explicitRequiredSpec || taskTypeSignals.landingPage && scopeSignals.broad ? "required" : "none";
|
|
979
|
+
return {
|
|
980
|
+
riskLevel,
|
|
981
|
+
specPolicy,
|
|
982
|
+
matchedSignals: [...new Set(matchedSignals)],
|
|
983
|
+
riskDrivers: [...new Set(riskDrivers)]
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
determineSafetyAndEvidence(text, mutationIntent) {
|
|
987
|
+
const safetyConstraints = [];
|
|
988
|
+
const requiredEvidence = [];
|
|
989
|
+
if (mutationIntent === "publish") {
|
|
990
|
+
safetyConstraints.push("Require release gate", "Awaiting user authorization");
|
|
991
|
+
requiredEvidence.push("release-decision", "tarball-hash");
|
|
992
|
+
} else if (mutationIntent === "write") {
|
|
993
|
+
safetyConstraints.push("Require branch gate", "No direct commits to main");
|
|
994
|
+
requiredEvidence.push("tests", "commit-hash");
|
|
995
|
+
} else {
|
|
996
|
+
safetyConstraints.push("No workspace mutations allowed");
|
|
997
|
+
}
|
|
998
|
+
if (/\b(bypass|force|direct)\b/i.test(text)) {
|
|
999
|
+
safetyConstraints.push("Block bypass attempts");
|
|
1000
|
+
}
|
|
1001
|
+
if (/\b(required evidence|evidence required)\b/i.test(text)) {
|
|
1002
|
+
requiredEvidence.push("user-required-evidence");
|
|
1003
|
+
}
|
|
1004
|
+
if (/\b(formal spec|required spec)\b/i.test(text)) {
|
|
1005
|
+
safetyConstraints.push("Require formal specification");
|
|
1006
|
+
}
|
|
1007
|
+
return { safetyConstraints, requiredEvidence };
|
|
1008
|
+
}
|
|
1009
|
+
/**
|
|
1010
|
+
* Decides the routing decision.
|
|
1011
|
+
*/
|
|
1012
|
+
route(understanding) {
|
|
1013
|
+
const { operationType, mutationIntent, rawRequest } = understanding;
|
|
1014
|
+
const requestedActor = void 0;
|
|
1015
|
+
if (/\b(bypass safety|bypass validation|force push|directly to main|push to remote without gate)\b/i.test(rawRequest)) {
|
|
1016
|
+
return {
|
|
1017
|
+
requestedActor,
|
|
1018
|
+
selectedActor: "Atlas",
|
|
1019
|
+
operationType,
|
|
1020
|
+
mutationIntent,
|
|
1021
|
+
permissionDecision: "blocked",
|
|
1022
|
+
reason: "Security block: Attempt to bypass safety or push directly is forbidden.",
|
|
1023
|
+
workflowPath: ["Atlas"]
|
|
1024
|
+
};
|
|
1025
|
+
}
|
|
1026
|
+
let { selectedActor, permissionDecision, reason } = this.evaluateActorPermission(operationType, mutationIntent);
|
|
1027
|
+
let workflowPath = ["Atlas"];
|
|
1028
|
+
if (selectedActor === "Phoenix") {
|
|
1029
|
+
const requestPath = path4.join(this.cwd, ".ai-workflow/remediation-request.json");
|
|
1030
|
+
let hasFindings = false;
|
|
1031
|
+
if (fs5.existsSync(requestPath)) {
|
|
1032
|
+
try {
|
|
1033
|
+
const reqData = JSON.parse(fs5.readFileSync(requestPath, "utf8"));
|
|
1034
|
+
if (reqData.affectedChecks && reqData.affectedChecks.length > 0) {
|
|
1035
|
+
hasFindings = true;
|
|
1036
|
+
}
|
|
1037
|
+
} catch {
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
if (!hasFindings) {
|
|
1041
|
+
permissionDecision = "blocked";
|
|
1042
|
+
reason = "Blocked: Phoenix acts only with concrete findings and bounded remediation.";
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
if (understanding.specPolicy === "required") {
|
|
1046
|
+
workflowPath.push("Nexus", "Orion");
|
|
1047
|
+
}
|
|
1048
|
+
if (selectedActor && !workflowPath.includes(selectedActor)) {
|
|
1049
|
+
workflowPath.push(selectedActor);
|
|
1050
|
+
}
|
|
1051
|
+
if (selectedActor === "Atlas" && ["discover", "validate", "fix", "release", "deploy"].includes(operationType)) {
|
|
1052
|
+
permissionDecision = "blocked";
|
|
1053
|
+
reason = "Atlas self-execution guard: Atlas cannot silently execute specialized actor or capability requests.";
|
|
1054
|
+
}
|
|
1055
|
+
if (selectedActor === "Astra" && understanding.riskLevel === "high") {
|
|
1056
|
+
workflowPath.push("Sage", "Phoenix", "Sage");
|
|
1057
|
+
}
|
|
1058
|
+
if (understanding.specPolicy === "none") {
|
|
1059
|
+
workflowPath = workflowPath.filter((actor) => {
|
|
1060
|
+
if (actor === "Nexus" && selectedActor !== "Nexus") return false;
|
|
1061
|
+
if (actor === "Orion" && selectedActor !== "Orion") return false;
|
|
1062
|
+
return true;
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
return {
|
|
1066
|
+
requestedActor,
|
|
1067
|
+
selectedActor,
|
|
1068
|
+
operationType,
|
|
1069
|
+
mutationIntent,
|
|
1070
|
+
permissionDecision,
|
|
1071
|
+
reason,
|
|
1072
|
+
workflowPath
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
evaluateActorPermission(operationType, mutationIntent) {
|
|
1076
|
+
let selectedActor;
|
|
1077
|
+
let permissionDecision = "allowed";
|
|
1078
|
+
let reason = "Routed successfully.";
|
|
1079
|
+
if (operationType === "discover") {
|
|
1080
|
+
selectedActor = "Nexus";
|
|
1081
|
+
} else if (operationType === "validate") {
|
|
1082
|
+
selectedActor = "Sage";
|
|
1083
|
+
} else if (operationType === "fix") {
|
|
1084
|
+
selectedActor = "Phoenix";
|
|
1085
|
+
} else if (operationType === "implement") {
|
|
1086
|
+
selectedActor = "Astra";
|
|
1087
|
+
} else if (operationType === "release" || operationType === "deploy") {
|
|
1088
|
+
selectedActor = "Orion";
|
|
1089
|
+
} else {
|
|
1090
|
+
selectedActor = mutationIntent === "write" ? "Astra" : "Atlas";
|
|
1091
|
+
}
|
|
1092
|
+
return { selectedActor, permissionDecision, reason };
|
|
1093
|
+
}
|
|
1094
|
+
};
|
|
1095
|
+
|
|
1096
|
+
// src/core/request-classifier.ts
|
|
1097
|
+
var RequestClassifier = class {
|
|
1098
|
+
/**
|
|
1099
|
+
* Classifies a natural request.
|
|
1100
|
+
*/
|
|
1101
|
+
classify(request = "", options = {}) {
|
|
1102
|
+
const text = String(request).trim();
|
|
1103
|
+
if (!text) {
|
|
1104
|
+
throw new Error("Cannot classify an empty request.");
|
|
1105
|
+
}
|
|
1106
|
+
const cwd = options.cwd || process.cwd();
|
|
1107
|
+
const profile = resolveWorkflowProfile({ request: text });
|
|
1108
|
+
const profileDef = getWorkflowProfile(profile);
|
|
1109
|
+
const readOnlyPatterns = /\b(analyze|check|inspect|review|find|search|show|list|read|view|verify)\b/i;
|
|
1110
|
+
const isReadOnly = readOnlyPatterns.test(text);
|
|
1111
|
+
const intent = isReadOnly ? "read-only" : "write";
|
|
1112
|
+
const router = new RequestRouter({ cwd });
|
|
1113
|
+
const requestUnderstanding = router.understand(text);
|
|
1114
|
+
const routingDecision = router.route(requestUnderstanding);
|
|
1115
|
+
const risk = requestUnderstanding.riskLevel;
|
|
1116
|
+
const explicitRequiredEvidence = /\b(required evidence|evidence required)\b/i.test(text);
|
|
1117
|
+
const explicitRequiredSpec = /\b(formal spec|required spec)\b/i.test(text);
|
|
1118
|
+
const specPolicy = requestUnderstanding.specPolicy === "required" || explicitRequiredSpec ? "required" : "none";
|
|
1119
|
+
const evidencePolicy = risk === "high" || requestUnderstanding.mutationIntent === "publish" || explicitRequiredEvidence ? "required" : "optional";
|
|
1120
|
+
const remediationLimit = risk === "high" ? 3 : risk === "low" ? 1 : 2;
|
|
1121
|
+
const specNeeded = specPolicy === "required";
|
|
1122
|
+
const validationNeeded = intent === "write";
|
|
1123
|
+
let legacyMode = "standard";
|
|
1124
|
+
if (/\b(deep|architectural|migration|major|full|\[deep\])\b/i.test(text)) {
|
|
1125
|
+
legacyMode = "full";
|
|
1126
|
+
} else if (/\b(tiny|small|quick|simple|only\s+update|typo|comment|\[tiny\])\b/i.test(text)) {
|
|
1127
|
+
const isPureDocsOrTypo = profile === "documentation" || /\b(typo|comment|readme|docs|documentation)\b/i.test(text);
|
|
1128
|
+
if (intent === "write" && !isPureDocsOrTypo) {
|
|
1129
|
+
legacyMode = "standard";
|
|
1130
|
+
} else {
|
|
1131
|
+
legacyMode = "quick";
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
return {
|
|
1135
|
+
request: text,
|
|
1136
|
+
profile,
|
|
1137
|
+
owner: routingDecision.selectedActor || profileDef.owner,
|
|
1138
|
+
intent: requestUnderstanding.mutationIntent === "readonly" ? "read-only" : "write",
|
|
1139
|
+
mode: legacyMode,
|
|
1140
|
+
risk,
|
|
1141
|
+
specPolicy,
|
|
1142
|
+
evidencePolicy,
|
|
1143
|
+
remediationLimit,
|
|
1144
|
+
riskDrivers: requestUnderstanding.riskDrivers || [],
|
|
1145
|
+
matchedSignals: requestUnderstanding.matchedSignals || [],
|
|
1146
|
+
specNeeded,
|
|
1147
|
+
validationNeeded,
|
|
1148
|
+
skills: [...profileDef.skills],
|
|
1149
|
+
requestUnderstanding,
|
|
1150
|
+
routingDecision
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
1153
|
+
};
|
|
1154
|
+
|
|
1155
|
+
// src/core/execution-planner.ts
|
|
1156
|
+
import path5 from "path";
|
|
1157
|
+
var ExecutionPlanner = class {
|
|
1158
|
+
cwd;
|
|
1159
|
+
constructor({ cwd = process.cwd() } = {}) {
|
|
1160
|
+
this.cwd = cwd;
|
|
1161
|
+
}
|
|
1162
|
+
/**
|
|
1163
|
+
* Generates an execution plan.
|
|
1164
|
+
*/
|
|
1165
|
+
plan(classification, taskSlug = "task") {
|
|
1166
|
+
const remediationLimit = classification.remediationLimit;
|
|
1167
|
+
const branchNeeded = classification.intent === "write";
|
|
1168
|
+
const specPath = classification.specNeeded ? path5.join("docs/workflows", taskSlug, "spec.md") : null;
|
|
1169
|
+
const owner = classification.routingDecision?.selectedActor || (classification.intent === "write" ? "Astra" : classification.owner);
|
|
1170
|
+
const validationsExpected = [];
|
|
1171
|
+
if (classification.validationNeeded) {
|
|
1172
|
+
validationsExpected.push("test");
|
|
1173
|
+
if (classification.profile.startsWith("frontend")) {
|
|
1174
|
+
validationsExpected.push("lint", "build");
|
|
1175
|
+
} else if (classification.profile === "backend-api") {
|
|
1176
|
+
validationsExpected.push("lint");
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
const restrictions = [
|
|
1180
|
+
"Never commit or push directly to main/master.",
|
|
1181
|
+
"Always execute behavior tests for new or modified behavior."
|
|
1182
|
+
];
|
|
1183
|
+
if (classification.risk === "high") {
|
|
1184
|
+
restrictions.push("Require independent validation review.");
|
|
1185
|
+
}
|
|
1186
|
+
return {
|
|
1187
|
+
objective: classification.request,
|
|
1188
|
+
scope: classification.intent === "read-only" ? "Analysis and verification" : "Implementation of requested behavior",
|
|
1189
|
+
restrictions,
|
|
1190
|
+
owner,
|
|
1191
|
+
skills: [...classification.skills],
|
|
1192
|
+
branchNeeded,
|
|
1193
|
+
branchName: branchNeeded ? `feat/${taskSlug}` : null,
|
|
1194
|
+
validationsExpected,
|
|
1195
|
+
remediationLimit,
|
|
1196
|
+
specPath,
|
|
1197
|
+
specPolicy: classification.specPolicy,
|
|
1198
|
+
evidencePolicy: classification.evidencePolicy,
|
|
1199
|
+
riskLevel: classification.risk,
|
|
1200
|
+
mode: classification.mode,
|
|
1201
|
+
profile: classification.profile
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
1204
|
+
};
|
|
1205
|
+
|
|
1206
|
+
// src/core/workflow-state-machine.ts
|
|
1207
|
+
var WorkflowStateMachine = class {
|
|
1208
|
+
state;
|
|
1209
|
+
history;
|
|
1210
|
+
validTransitions;
|
|
1211
|
+
constructor() {
|
|
1212
|
+
this.state = "RECEIVED";
|
|
1213
|
+
this.history = [{ state: this.state, timestamp: (/* @__PURE__ */ new Date()).toISOString() }];
|
|
1214
|
+
this.validTransitions = {
|
|
1215
|
+
RECEIVED: ["CLASSIFIED", "BLOCKED"],
|
|
1216
|
+
CLASSIFIED: ["PLANNED", "BLOCKED"],
|
|
1217
|
+
PLANNED: ["BRANCH_READY", "BLOCKED"],
|
|
1218
|
+
BRANCH_READY: ["DELEGATED", "BLOCKED"],
|
|
1219
|
+
DELEGATED: ["IMPLEMENTING", "BLOCKED"],
|
|
1220
|
+
IMPLEMENTING: ["IMPLEMENTED", "BLOCKED"],
|
|
1221
|
+
IMPLEMENTED: ["VALIDATING", "BLOCKED"],
|
|
1222
|
+
VALIDATING: ["COMPLETED", "COMPLETED_WITH_NOTES", "REMEDIATING", "BLOCKED"],
|
|
1223
|
+
REMEDIATING: ["REVALIDATING", "BLOCKED"],
|
|
1224
|
+
REVALIDATING: ["COMPLETED", "COMPLETED_WITH_NOTES", "REMEDIATING", "BLOCKED"],
|
|
1225
|
+
COMPLETED: [],
|
|
1226
|
+
COMPLETED_WITH_NOTES: [],
|
|
1227
|
+
BLOCKED: []
|
|
1228
|
+
};
|
|
1229
|
+
}
|
|
1230
|
+
/**
|
|
1231
|
+
* Transitions the machine to a new state.
|
|
1232
|
+
* @param newState - The state to transition to.
|
|
1233
|
+
*/
|
|
1234
|
+
transitionTo(newState) {
|
|
1235
|
+
const allowed = this.validTransitions[this.state];
|
|
1236
|
+
if (!allowed || !allowed.includes(newState)) {
|
|
1237
|
+
throw new Error(`Invalid state transition: ${this.state} -> ${newState}`);
|
|
1238
|
+
}
|
|
1239
|
+
this.state = newState;
|
|
1240
|
+
this.history.push({ state: this.state, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
1241
|
+
}
|
|
1242
|
+
getCurrentState() {
|
|
1243
|
+
return this.state;
|
|
1244
|
+
}
|
|
1245
|
+
getHistory() {
|
|
1246
|
+
return [...this.history];
|
|
1247
|
+
}
|
|
1248
|
+
};
|
|
1249
|
+
|
|
1250
|
+
// src/core/evidence/evidence-ledger.ts
|
|
1251
|
+
import fs6 from "fs/promises";
|
|
1252
|
+
import path6 from "path";
|
|
1253
|
+
var EvidenceLedger = class {
|
|
1254
|
+
cwd;
|
|
1255
|
+
workflowId;
|
|
1256
|
+
events;
|
|
1257
|
+
constructor({ cwd = process.cwd(), workflowId = "unknown" } = {}) {
|
|
1258
|
+
this.cwd = cwd;
|
|
1259
|
+
this.workflowId = workflowId;
|
|
1260
|
+
this.events = [];
|
|
1261
|
+
}
|
|
1262
|
+
/**
|
|
1263
|
+
* Logs a workflow event to the ledger.
|
|
1264
|
+
*/
|
|
1265
|
+
logEvent({
|
|
1266
|
+
actor,
|
|
1267
|
+
eventType,
|
|
1268
|
+
provenance,
|
|
1269
|
+
data = {},
|
|
1270
|
+
actorType,
|
|
1271
|
+
observed,
|
|
1272
|
+
runtime
|
|
1273
|
+
}) {
|
|
1274
|
+
const event = {
|
|
1275
|
+
workflowId: this.workflowId,
|
|
1276
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1277
|
+
actor,
|
|
1278
|
+
actorType: actorType !== void 0 ? actorType : void 0,
|
|
1279
|
+
observed: observed !== void 0 ? observed : void 0,
|
|
1280
|
+
runtime: runtime !== void 0 ? runtime : void 0,
|
|
1281
|
+
eventType,
|
|
1282
|
+
provenance,
|
|
1283
|
+
data
|
|
1284
|
+
};
|
|
1285
|
+
this.events.push(event);
|
|
1286
|
+
console.log(`[LEDGER] [${actor}] ${eventType}: ${JSON.stringify(data)}`);
|
|
1287
|
+
return event;
|
|
1288
|
+
}
|
|
1289
|
+
getEvents() {
|
|
1290
|
+
return this.events;
|
|
1291
|
+
}
|
|
1292
|
+
/**
|
|
1293
|
+
* Saves the ledger events to a JSON file.
|
|
1294
|
+
*/
|
|
1295
|
+
async save(filePath) {
|
|
1296
|
+
const targetPath = path6.isAbsolute(filePath) ? filePath : path6.join(this.cwd, filePath);
|
|
1297
|
+
await fs6.mkdir(path6.dirname(targetPath), { recursive: true });
|
|
1298
|
+
await fs6.writeFile(targetPath, JSON.stringify(this.events, null, 2), "utf8");
|
|
1299
|
+
}
|
|
1300
|
+
async verifyRoleConfinement() {
|
|
1301
|
+
const violations = [];
|
|
1302
|
+
const events = this.events;
|
|
1303
|
+
const atlasImplements = events.some((e) => e.actor === "Atlas" && (e.eventType === "implementation_start" || e.eventType === "implementation_complete"));
|
|
1304
|
+
if (atlasImplements) {
|
|
1305
|
+
violations.push("Atlas cannot implement directly");
|
|
1306
|
+
}
|
|
1307
|
+
const nexusMutates = events.some((e) => e.actor === "Nexus" && e.eventType === "file_mutate");
|
|
1308
|
+
if (nexusMutates) {
|
|
1309
|
+
violations.push("Nexus cannot mutate files during discovery");
|
|
1310
|
+
}
|
|
1311
|
+
const sageValidates = events.some((e) => e.actor === "Sage" && (e.eventType === "validation_start" || e.eventType === "validation_complete"));
|
|
1312
|
+
const hasFidelity = events.some((e) => e.eventType === "fidelity_check" || e.eventType === "fidelity_verification");
|
|
1313
|
+
if (sageValidates && !hasFidelity) {
|
|
1314
|
+
violations.push("Sage cannot validate without fidelity evidence");
|
|
1315
|
+
}
|
|
1316
|
+
const phoenixRemediates = events.find((e) => e.actor === "Phoenix" && e.eventType === "remediation_start");
|
|
1317
|
+
if (phoenixRemediates) {
|
|
1318
|
+
const findings = phoenixRemediates.data?.findings;
|
|
1319
|
+
if (!findings || findings.length === 0) {
|
|
1320
|
+
violations.push("Phoenix cannot perform remediation without findings");
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
const orionReleases = events.find((e) => e.actor === "Orion" && e.eventType === "release_start");
|
|
1324
|
+
if (orionReleases) {
|
|
1325
|
+
const approvals = orionReleases.data?.approvals;
|
|
1326
|
+
if (!approvals || approvals.length === 0) {
|
|
1327
|
+
violations.push("Orion cannot release without explicit approval");
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
const passed = violations.length === 0;
|
|
1331
|
+
return {
|
|
1332
|
+
passed,
|
|
1333
|
+
status: passed ? "PASS" : "BLOCKED",
|
|
1334
|
+
reason: violations.length > 0 ? `Role violations: ${violations.join("; ")}` : "Verification passed",
|
|
1335
|
+
violations
|
|
1336
|
+
};
|
|
1337
|
+
}
|
|
1338
|
+
};
|
|
1339
|
+
|
|
1340
|
+
// src/core/delegation-controller.ts
|
|
1341
|
+
var DelegationController = class {
|
|
1342
|
+
cwd;
|
|
1343
|
+
ledger;
|
|
1344
|
+
adapter;
|
|
1345
|
+
lastRoutingDecision;
|
|
1346
|
+
constructor({ cwd = process.cwd(), ledger, adapter } = {}) {
|
|
1347
|
+
this.cwd = cwd;
|
|
1348
|
+
this.ledger = ledger;
|
|
1349
|
+
this.adapter = adapter || new OpenCodeAdapter({ cwd });
|
|
1350
|
+
this.lastRoutingDecision = null;
|
|
1351
|
+
}
|
|
1352
|
+
/**
|
|
1353
|
+
* Logs routing decision by Atlas.
|
|
1354
|
+
*/
|
|
1355
|
+
async route(naturalRequest, classification) {
|
|
1356
|
+
const decision = classification.routingDecision ?? {
|
|
1357
|
+
operationType: "explain",
|
|
1358
|
+
mutationIntent: "readonly",
|
|
1359
|
+
permissionDecision: "allowed",
|
|
1360
|
+
reason: "no routing decision",
|
|
1361
|
+
workflowPath: []
|
|
1362
|
+
};
|
|
1363
|
+
this.lastRoutingDecision = decision;
|
|
1364
|
+
if (this.ledger) {
|
|
1365
|
+
this.ledger.logEvent({
|
|
1366
|
+
actor: "Atlas",
|
|
1367
|
+
actorType: "control-plane",
|
|
1368
|
+
observed: true,
|
|
1369
|
+
eventType: "routing",
|
|
1370
|
+
provenance: "request-classifier",
|
|
1371
|
+
data: {
|
|
1372
|
+
request: naturalRequest,
|
|
1373
|
+
requestedActor: decision.requestedActor || null,
|
|
1374
|
+
selectedActor: decision.selectedActor || null,
|
|
1375
|
+
operationType: decision.operationType || null,
|
|
1376
|
+
mutationIntent: decision.mutationIntent || null,
|
|
1377
|
+
permissionDecision: decision.permissionDecision || null,
|
|
1378
|
+
reason: decision.reason || null,
|
|
1379
|
+
workflowPath: decision.workflowPath || [],
|
|
1380
|
+
classification,
|
|
1381
|
+
event: "routing.completed",
|
|
1382
|
+
runtimeAgentRequested: null,
|
|
1383
|
+
runtimeAgentApplied: null,
|
|
1384
|
+
runtimeAgentSupported: false,
|
|
1385
|
+
status: decision.permissionDecision === "blocked" ? "BLOCKED" : "COMPLETED"
|
|
1386
|
+
}
|
|
1387
|
+
});
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
/**
|
|
1391
|
+
* Logs and executes the implementation process by Astra.
|
|
1392
|
+
*/
|
|
1393
|
+
async implement(promptMsg, owner, options = {}) {
|
|
1394
|
+
const decision = this.lastRoutingDecision ?? {
|
|
1395
|
+
operationType: "explain",
|
|
1396
|
+
mutationIntent: "readonly",
|
|
1397
|
+
permissionDecision: "allowed",
|
|
1398
|
+
reason: "no routing decision",
|
|
1399
|
+
workflowPath: []
|
|
1400
|
+
};
|
|
1401
|
+
const inspection = await this.adapter.inspect();
|
|
1402
|
+
const isSpecialized = ["Nexus", "Orion", "Astra", "Sage", "Phoenix"].includes(owner);
|
|
1403
|
+
const supportsAgent = inspection.supports?.agent;
|
|
1404
|
+
const canApply = !isSpecialized || supportsAgent;
|
|
1405
|
+
const runtimeAgentSelectionMode = isSpecialized ? supportsAgent ? "explicit" : "prompt-fallback" : "default";
|
|
1406
|
+
if (this.ledger) {
|
|
1407
|
+
this.ledger.logEvent({
|
|
1408
|
+
actor: canApply ? owner : "Atlas",
|
|
1409
|
+
actorType: canApply ? "runtime-agent" : "control-plane",
|
|
1410
|
+
observed: true,
|
|
1411
|
+
runtime: "opencode",
|
|
1412
|
+
eventType: "implementation_start",
|
|
1413
|
+
provenance: "opencode-adapter",
|
|
1414
|
+
data: {
|
|
1415
|
+
agent: owner,
|
|
1416
|
+
prompt: promptMsg,
|
|
1417
|
+
event: "implementation.started",
|
|
1418
|
+
requestedActor: decision.requestedActor || null,
|
|
1419
|
+
selectedActor: decision.selectedActor || null,
|
|
1420
|
+
runtimeAgentRequested: owner,
|
|
1421
|
+
runtimeAgentApplied: null,
|
|
1422
|
+
runtimeAgentSupported: supportsAgent || false,
|
|
1423
|
+
runtimeAgentSelectionMode,
|
|
1424
|
+
permissionDecision: decision.permissionDecision || null,
|
|
1425
|
+
reason: decision.reason || null,
|
|
1426
|
+
status: "started"
|
|
1427
|
+
}
|
|
1428
|
+
});
|
|
1429
|
+
}
|
|
1430
|
+
const runResult = await this.adapter.execute(promptMsg, { agent: owner, ...options });
|
|
1431
|
+
if (this.ledger) {
|
|
1432
|
+
const isApplied = runResult.runtimeAgentApplied === owner;
|
|
1433
|
+
this.ledger.logEvent({
|
|
1434
|
+
actor: isApplied ? owner : "Atlas",
|
|
1435
|
+
actorType: isApplied ? "runtime-agent" : "control-plane",
|
|
1436
|
+
observed: true,
|
|
1437
|
+
runtime: "opencode",
|
|
1438
|
+
eventType: "implementation_complete",
|
|
1439
|
+
provenance: "opencode-adapter",
|
|
1440
|
+
data: {
|
|
1441
|
+
success: runResult.success,
|
|
1442
|
+
commandsRun: runResult.commandsRun || [],
|
|
1443
|
+
eventCount: runResult.eventCount || 0,
|
|
1444
|
+
error: runResult.error,
|
|
1445
|
+
event: "implementation.completed",
|
|
1446
|
+
requestedActor: decision.requestedActor || null,
|
|
1447
|
+
selectedActor: decision.selectedActor || null,
|
|
1448
|
+
runtimeAgentRequested: runResult.runtimeAgentRequested || owner || null,
|
|
1449
|
+
runtimeAgentApplied: runResult.runtimeAgentApplied || null,
|
|
1450
|
+
runtimeAgentSupported: runResult.runtimeAgentSupported !== void 0 ? runResult.runtimeAgentSupported : supportsAgent || false,
|
|
1451
|
+
runtimeAgentSelectionMode: runResult.runtimeAgentSelectionMode || runtimeAgentSelectionMode,
|
|
1452
|
+
permissionDecision: decision.permissionDecision || null,
|
|
1453
|
+
reason: runResult.error || decision.reason || null,
|
|
1454
|
+
status: runResult.status || (runResult.success ? "COMPLETED" : "FAILED")
|
|
1455
|
+
}
|
|
1456
|
+
});
|
|
1457
|
+
}
|
|
1458
|
+
return runResult;
|
|
1459
|
+
}
|
|
1460
|
+
/**
|
|
1461
|
+
* Runs validation locally without logging Sage since no validation agent runtime was executed.
|
|
1462
|
+
*/
|
|
1463
|
+
async validate(taskSlug, profile, verifyFn, evidencePolicy) {
|
|
1464
|
+
const result = await verifyFn();
|
|
1465
|
+
const isRequired = evidencePolicy === "required";
|
|
1466
|
+
if (isRequired) {
|
|
1467
|
+
const decision = this.lastRoutingDecision ?? {
|
|
1468
|
+
operationType: "explain",
|
|
1469
|
+
mutationIntent: "readonly",
|
|
1470
|
+
permissionDecision: "allowed",
|
|
1471
|
+
reason: "no routing decision",
|
|
1472
|
+
workflowPath: []
|
|
1473
|
+
};
|
|
1474
|
+
const inspection = await this.adapter.inspect();
|
|
1475
|
+
const supportsAgent = inspection.supports?.agent;
|
|
1476
|
+
const canApply = supportsAgent;
|
|
1477
|
+
const runtimeAgentSelectionMode = supportsAgent ? "explicit" : "prompt-fallback";
|
|
1478
|
+
if (this.ledger) {
|
|
1479
|
+
this.ledger.logEvent({
|
|
1480
|
+
actor: canApply ? "Sage" : "Atlas",
|
|
1481
|
+
actorType: canApply ? "auditor" : "control-plane",
|
|
1482
|
+
observed: true,
|
|
1483
|
+
runtime: "opencode",
|
|
1484
|
+
eventType: "validation_start",
|
|
1485
|
+
provenance: "opencode-adapter",
|
|
1486
|
+
data: {
|
|
1487
|
+
agent: "Sage",
|
|
1488
|
+
event: "validation.started",
|
|
1489
|
+
requestedActor: decision.requestedActor || null,
|
|
1490
|
+
selectedActor: decision.selectedActor || null,
|
|
1491
|
+
runtimeAgentRequested: "Sage",
|
|
1492
|
+
runtimeAgentApplied: null,
|
|
1493
|
+
runtimeAgentSupported: supportsAgent || false,
|
|
1494
|
+
runtimeAgentSelectionMode,
|
|
1495
|
+
permissionDecision: decision.permissionDecision || null,
|
|
1496
|
+
reason: decision.reason || null,
|
|
1497
|
+
status: "started"
|
|
1498
|
+
}
|
|
1499
|
+
});
|
|
1500
|
+
}
|
|
1501
|
+
let gitDiff = "";
|
|
1502
|
+
try {
|
|
1503
|
+
const { execSync: execSync6 } = await import("child_process");
|
|
1504
|
+
gitDiff = execSync6("git diff HEAD", { cwd: this.cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
1505
|
+
} catch {
|
|
1506
|
+
}
|
|
1507
|
+
const prompt = `Please validate the implementation. Diff:
|
|
1508
|
+
${gitDiff}
|
|
1509
|
+
Local Evidence:
|
|
1510
|
+
${JSON.stringify(result, null, 2)}`;
|
|
1511
|
+
const runResult = await this.adapter.execute(prompt, { agent: "Sage" });
|
|
1512
|
+
if (this.ledger) {
|
|
1513
|
+
const isApplied = runResult.runtimeAgentApplied === "Sage";
|
|
1514
|
+
this.ledger.logEvent({
|
|
1515
|
+
actor: isApplied ? "Sage" : "Atlas",
|
|
1516
|
+
actorType: isApplied ? "auditor" : "control-plane",
|
|
1517
|
+
observed: true,
|
|
1518
|
+
runtime: "opencode",
|
|
1519
|
+
eventType: "validation_complete",
|
|
1520
|
+
provenance: "opencode-adapter",
|
|
1521
|
+
data: {
|
|
1522
|
+
success: runResult.success,
|
|
1523
|
+
commandsRun: runResult.commandsRun || [],
|
|
1524
|
+
eventCount: runResult.eventCount || 0,
|
|
1525
|
+
error: runResult.error,
|
|
1526
|
+
event: "validation.completed",
|
|
1527
|
+
requestedActor: decision.requestedActor || null,
|
|
1528
|
+
selectedActor: decision.selectedActor || null,
|
|
1529
|
+
runtimeAgentRequested: runResult.runtimeAgentRequested || "Sage",
|
|
1530
|
+
runtimeAgentApplied: runResult.runtimeAgentApplied || null,
|
|
1531
|
+
runtimeAgentSupported: runResult.runtimeAgentSupported !== void 0 ? runResult.runtimeAgentSupported : supportsAgent || false,
|
|
1532
|
+
runtimeAgentSelectionMode: runResult.runtimeAgentSelectionMode || runtimeAgentSelectionMode,
|
|
1533
|
+
permissionDecision: decision.permissionDecision || null,
|
|
1534
|
+
reason: runResult.error || decision.reason || null,
|
|
1535
|
+
status: runResult.status || (runResult.success ? "COMPLETED" : "FAILED")
|
|
1536
|
+
}
|
|
1537
|
+
});
|
|
1538
|
+
}
|
|
1539
|
+
if (!runResult.success) {
|
|
1540
|
+
result.overallStatus = "FAIL_QUALITY_GATE";
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
return result;
|
|
1544
|
+
}
|
|
1545
|
+
};
|
|
1546
|
+
|
|
1547
|
+
// src/core/finalization/workspace-snapshot.ts
|
|
1548
|
+
import fs7 from "fs/promises";
|
|
1549
|
+
import path7 from "path";
|
|
1550
|
+
import crypto2 from "crypto";
|
|
1551
|
+
import { execSync as execSync4 } from "child_process";
|
|
1552
|
+
var WorkspaceSnapshot = class {
|
|
1553
|
+
cwd;
|
|
1554
|
+
fastTrack;
|
|
1555
|
+
constructor({ cwd = process.cwd(), fastTrack = false } = {}) {
|
|
1556
|
+
this.cwd = cwd;
|
|
1557
|
+
this.fastTrack = fastTrack;
|
|
1558
|
+
}
|
|
1559
|
+
isIgnored(relativePath) {
|
|
1560
|
+
const norm = relativePath.replaceAll("\\", "/");
|
|
1561
|
+
if (norm === "node_modules" || norm.startsWith("node_modules/")) return true;
|
|
1562
|
+
if (norm === "vendor" || norm.startsWith("vendor/")) return true;
|
|
1563
|
+
if (norm === ".git" || norm.startsWith(".git/")) return true;
|
|
1564
|
+
if (norm === "EVIDENCE.json") return true;
|
|
1565
|
+
if (norm === ".ai-workflow/EVIDENCE.json") return true;
|
|
1566
|
+
if (norm.startsWith(".ai-workflow/history/")) return true;
|
|
1567
|
+
if (norm.startsWith(".ai-workflow/handoffs/")) return true;
|
|
1568
|
+
return false;
|
|
1569
|
+
}
|
|
1570
|
+
async capture() {
|
|
1571
|
+
const files = {};
|
|
1572
|
+
const isTest = process.env.NODE_ENV === "test" || process.env.VITEST === "true";
|
|
1573
|
+
if (!this.fastTrack || isTest) {
|
|
1574
|
+
await this.scanDir(this.cwd, files);
|
|
1575
|
+
}
|
|
1576
|
+
const execGit = (args) => {
|
|
1577
|
+
try {
|
|
1578
|
+
return execSync4(`git ${args}`, { cwd: this.cwd, encoding: "utf8", stdio: "pipe" }).trim();
|
|
1579
|
+
} catch {
|
|
1580
|
+
return "";
|
|
1581
|
+
}
|
|
1582
|
+
};
|
|
1583
|
+
const branch = execGit("rev-parse --abbrev-ref HEAD");
|
|
1584
|
+
const head = execGit("rev-parse HEAD");
|
|
1585
|
+
const indexTree = execGit("write-tree");
|
|
1586
|
+
const stagedDiff = execGit("diff --cached");
|
|
1587
|
+
const unstagedDiff = execGit("diff");
|
|
1588
|
+
const untrackedFiles = execGit("status --porcelain");
|
|
1589
|
+
const hashString = (str) => {
|
|
1590
|
+
return crypto2.createHash("sha256").update(str).digest("hex");
|
|
1591
|
+
};
|
|
1592
|
+
return {
|
|
1593
|
+
branch,
|
|
1594
|
+
head,
|
|
1595
|
+
indexTree,
|
|
1596
|
+
files,
|
|
1597
|
+
stagedDiffHash: hashString(stagedDiff),
|
|
1598
|
+
unstagedDiffHash: hashString(unstagedDiff),
|
|
1599
|
+
untrackedFilesHash: hashString(untrackedFiles)
|
|
1600
|
+
};
|
|
1601
|
+
}
|
|
1602
|
+
async scanDir(dir, filesMap) {
|
|
1603
|
+
try {
|
|
1604
|
+
const entries = await fs7.readdir(dir, { withFileTypes: true });
|
|
1605
|
+
for (const entry of entries) {
|
|
1606
|
+
const fullPath = path7.join(dir, entry.name);
|
|
1607
|
+
const relativePath = path7.relative(this.cwd, fullPath);
|
|
1608
|
+
if (this.isIgnored(relativePath)) {
|
|
1609
|
+
continue;
|
|
1610
|
+
}
|
|
1611
|
+
if (entry.isDirectory()) {
|
|
1612
|
+
await this.scanDir(fullPath, filesMap);
|
|
1613
|
+
} else if (entry.isFile()) {
|
|
1614
|
+
try {
|
|
1615
|
+
const stat = await fs7.stat(fullPath);
|
|
1616
|
+
const content = await fs7.readFile(fullPath);
|
|
1617
|
+
const sha256 = crypto2.createHash("sha256").update(content).digest("hex");
|
|
1618
|
+
const isExecutable = (stat.mode & 73) !== 0;
|
|
1619
|
+
const mode = isExecutable ? "100755" : "100644";
|
|
1620
|
+
filesMap[relativePath] = {
|
|
1621
|
+
sha256,
|
|
1622
|
+
mode
|
|
1623
|
+
};
|
|
1624
|
+
} catch {
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
} catch {
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
calculateHash(snapshot) {
|
|
1632
|
+
const hash = crypto2.createHash("sha256");
|
|
1633
|
+
hash.update(snapshot.branch || "");
|
|
1634
|
+
hash.update(snapshot.head || "");
|
|
1635
|
+
hash.update(snapshot.indexTree || "");
|
|
1636
|
+
hash.update(snapshot.stagedDiffHash || "");
|
|
1637
|
+
hash.update(snapshot.unstagedDiffHash || "");
|
|
1638
|
+
hash.update(snapshot.untrackedFilesHash || "");
|
|
1639
|
+
const sortedFiles = Object.keys(snapshot.files || {}).sort();
|
|
1640
|
+
for (const file of sortedFiles) {
|
|
1641
|
+
hash.update(file);
|
|
1642
|
+
hash.update(snapshot.files[file].sha256);
|
|
1643
|
+
hash.update(snapshot.files[file].mode);
|
|
1644
|
+
}
|
|
1645
|
+
return hash.digest("hex");
|
|
1646
|
+
}
|
|
1647
|
+
};
|
|
1648
|
+
|
|
1649
|
+
// src/core/finalization/finalizer.ts
|
|
1650
|
+
import { execSync as execSync5 } from "child_process";
|
|
1651
|
+
import path8 from "path";
|
|
1652
|
+
import fs8 from "fs/promises";
|
|
1653
|
+
var Finalizer = class {
|
|
1654
|
+
cwd;
|
|
1655
|
+
snapshotManager;
|
|
1656
|
+
fastTrack;
|
|
1657
|
+
constructor({ cwd = process.cwd(), fastTrack = false } = {}) {
|
|
1658
|
+
this.cwd = cwd;
|
|
1659
|
+
this.snapshotManager = new WorkspaceSnapshot({ cwd, fastTrack });
|
|
1660
|
+
this.fastTrack = fastTrack;
|
|
1661
|
+
}
|
|
1662
|
+
getChangedFilesFromGit() {
|
|
1663
|
+
try {
|
|
1664
|
+
const files = /* @__PURE__ */ new Set();
|
|
1665
|
+
const diff = execSync5("git diff --name-only HEAD", { cwd: this.cwd, encoding: "utf8" });
|
|
1666
|
+
diff.split("\n").map((f) => f.trim()).filter(Boolean).forEach((f) => files.add(f));
|
|
1667
|
+
const status = execSync5("git status --short --untracked-files=all", { cwd: this.cwd, encoding: "utf8" });
|
|
1668
|
+
status.split("\n").forEach((line) => {
|
|
1669
|
+
const match = line.match(/^(?:[ MADRCU?!]{2}\s+|[MADRCU?!]\s+)(.+)$/);
|
|
1670
|
+
if (match) {
|
|
1671
|
+
const candidate = match[1].trim();
|
|
1672
|
+
const renamed = candidate.includes(" -> ") ? candidate.split(" -> ").at(-1) : candidate;
|
|
1673
|
+
if (renamed) files.add(renamed);
|
|
1674
|
+
}
|
|
1675
|
+
});
|
|
1676
|
+
return [...files].sort();
|
|
1677
|
+
} catch {
|
|
1678
|
+
return [];
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
async verifyFidelity(arg) {
|
|
1682
|
+
let userRequest = null;
|
|
1683
|
+
let projectContext = null;
|
|
1684
|
+
let deliveryDecision = null;
|
|
1685
|
+
let changedFiles = null;
|
|
1686
|
+
let evidence = null;
|
|
1687
|
+
let explicitApprovals = [];
|
|
1688
|
+
if (Array.isArray(arg)) {
|
|
1689
|
+
changedFiles = arg;
|
|
1690
|
+
} else if (arg && typeof arg === "object") {
|
|
1691
|
+
userRequest = arg.userRequest;
|
|
1692
|
+
projectContext = arg.projectContext;
|
|
1693
|
+
deliveryDecision = arg.deliveryDecision;
|
|
1694
|
+
changedFiles = arg.changedFiles;
|
|
1695
|
+
evidence = arg.evidence;
|
|
1696
|
+
explicitApprovals = arg.explicitApprovals || [];
|
|
1697
|
+
}
|
|
1698
|
+
const filesToCheck = changedFiles || this.getChangedFilesFromGit();
|
|
1699
|
+
const decisionEngine = new DeliveryDecisionEngine({ cwd: this.cwd });
|
|
1700
|
+
const actualProjectContext = projectContext || await decisionEngine.detectContext();
|
|
1701
|
+
const actualUserRequest = userRequest || "";
|
|
1702
|
+
const actualDeliveryDecision = deliveryDecision || await decisionEngine.determineDecision({
|
|
1703
|
+
userRequest: actualUserRequest,
|
|
1704
|
+
projectContext: actualProjectContext,
|
|
1705
|
+
explicitApprovals
|
|
1706
|
+
});
|
|
1707
|
+
const actualDeliveryClass = await decisionEngine.determineClass(filesToCheck);
|
|
1708
|
+
const fidelityGate = new ArtifactFidelityGate({
|
|
1709
|
+
cwd: this.cwd,
|
|
1710
|
+
deliveryClass: actualDeliveryClass,
|
|
1711
|
+
userRequest: actualUserRequest,
|
|
1712
|
+
explicitApprovals
|
|
1713
|
+
});
|
|
1714
|
+
const requestFidelity = await fidelityGate.verifyRequestFidelity({
|
|
1715
|
+
userRequest: actualUserRequest,
|
|
1716
|
+
deliveryDecision: actualDeliveryDecision,
|
|
1717
|
+
projectContext: actualProjectContext,
|
|
1718
|
+
changedFiles: filesToCheck,
|
|
1719
|
+
evidence,
|
|
1720
|
+
explicitApprovals
|
|
1721
|
+
});
|
|
1722
|
+
const verifyResult = await fidelityGate.verify(filesToCheck);
|
|
1723
|
+
let passed = requestFidelity.passed && verifyResult.passed;
|
|
1724
|
+
let reason = !requestFidelity.passed ? requestFidelity.reason : verifyResult.reason;
|
|
1725
|
+
const evidencePath = path8.join(this.cwd, "EVIDENCE.json");
|
|
1726
|
+
const hasEvidence = await fs8.access(evidencePath).then(() => true).catch(() => false);
|
|
1727
|
+
if (hasEvidence) {
|
|
1728
|
+
const { EvidenceValidator } = await import("./evidence-validator-76ZQQYDU.js");
|
|
1729
|
+
const isTest = process.env.NODE_ENV === "test" || process.env.VITEST === "true";
|
|
1730
|
+
const evidenceValidator = new EvidenceValidator({ cwd: this.cwd, skipFileCheck: isTest, skipGitCheck: isTest });
|
|
1731
|
+
const valResult = await evidenceValidator.validate();
|
|
1732
|
+
if (!valResult.valid) {
|
|
1733
|
+
console.error("EVIDENCE VALIDATOR FAILED REASON:", valResult.reason);
|
|
1734
|
+
passed = false;
|
|
1735
|
+
reason = valResult.reason;
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
return {
|
|
1739
|
+
passed,
|
|
1740
|
+
status: requestFidelity.status || (passed ? "PASS" : "BLOCKED"),
|
|
1741
|
+
reason,
|
|
1742
|
+
deliveryClass: actualDeliveryClass,
|
|
1743
|
+
requestFidelity
|
|
1744
|
+
};
|
|
1745
|
+
}
|
|
1746
|
+
/**
|
|
1747
|
+
* Compares the current workspace state with a previously captured snapshot.
|
|
1748
|
+
*/
|
|
1749
|
+
async verifyIntegrity(previousSnapshot) {
|
|
1750
|
+
const currentSnapshot = await this.snapshotManager.capture();
|
|
1751
|
+
const added = [];
|
|
1752
|
+
const modified = [];
|
|
1753
|
+
const deleted = [];
|
|
1754
|
+
let valid = true;
|
|
1755
|
+
if (previousSnapshot.branch !== currentSnapshot.branch) {
|
|
1756
|
+
modified.push(`branch:${previousSnapshot.branch}->${currentSnapshot.branch}`);
|
|
1757
|
+
valid = false;
|
|
1758
|
+
}
|
|
1759
|
+
if (previousSnapshot.head !== currentSnapshot.head) {
|
|
1760
|
+
modified.push(`head:${previousSnapshot.head}->${currentSnapshot.head}`);
|
|
1761
|
+
valid = false;
|
|
1762
|
+
}
|
|
1763
|
+
if (previousSnapshot.indexTree !== currentSnapshot.indexTree) {
|
|
1764
|
+
modified.push(`indexTree:${previousSnapshot.indexTree}->${currentSnapshot.indexTree}`);
|
|
1765
|
+
valid = false;
|
|
1766
|
+
}
|
|
1767
|
+
if (previousSnapshot.stagedDiffHash !== currentSnapshot.stagedDiffHash) {
|
|
1768
|
+
modified.push("stagedDiff");
|
|
1769
|
+
valid = false;
|
|
1770
|
+
}
|
|
1771
|
+
if (previousSnapshot.unstagedDiffHash !== currentSnapshot.unstagedDiffHash) {
|
|
1772
|
+
modified.push("unstagedDiff");
|
|
1773
|
+
valid = false;
|
|
1774
|
+
}
|
|
1775
|
+
if (previousSnapshot.untrackedFilesHash !== currentSnapshot.untrackedFilesHash) {
|
|
1776
|
+
modified.push("untrackedFiles");
|
|
1777
|
+
valid = false;
|
|
1778
|
+
}
|
|
1779
|
+
const prevFiles = previousSnapshot.files || {};
|
|
1780
|
+
const currFiles = currentSnapshot.files || {};
|
|
1781
|
+
for (const [file, info] of Object.entries(currFiles)) {
|
|
1782
|
+
const prevInfo = prevFiles[file];
|
|
1783
|
+
if (!prevInfo) {
|
|
1784
|
+
added.push(file);
|
|
1785
|
+
} else if (prevInfo.sha256 !== info.sha256 || prevInfo.mode !== info.mode) {
|
|
1786
|
+
modified.push(file);
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
for (const file of Object.keys(prevFiles)) {
|
|
1790
|
+
if (!currFiles[file]) {
|
|
1791
|
+
deleted.push(file);
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
if (added.length > 0 || modified.length > 0 || deleted.length > 0) {
|
|
1795
|
+
valid = false;
|
|
1796
|
+
}
|
|
1797
|
+
return {
|
|
1798
|
+
valid,
|
|
1799
|
+
changes: { added, modified, deleted }
|
|
1800
|
+
};
|
|
1801
|
+
}
|
|
1802
|
+
};
|
|
1803
|
+
|
|
1804
|
+
export {
|
|
1805
|
+
readPackageFile,
|
|
1806
|
+
getPackageVersion,
|
|
1807
|
+
discoverPackageFiles,
|
|
1808
|
+
getFullSkillFiles,
|
|
1809
|
+
getFullAgentContent,
|
|
1810
|
+
isRecoverableGateFailure,
|
|
1811
|
+
isTerminalFailure,
|
|
1812
|
+
COMPLETION_STATUS_TEXT,
|
|
1813
|
+
OpenCodeAdapter,
|
|
1814
|
+
BranchGate,
|
|
1815
|
+
SpecValidator,
|
|
1816
|
+
HandoffEngine,
|
|
1817
|
+
HealerEngine,
|
|
1818
|
+
getWorkflowProfile,
|
|
1819
|
+
resolveWorkflowProfile,
|
|
1820
|
+
RequestClassifier,
|
|
1821
|
+
ExecutionPlanner,
|
|
1822
|
+
WorkflowStateMachine,
|
|
1823
|
+
EvidenceLedger,
|
|
1824
|
+
DelegationController,
|
|
1825
|
+
Finalizer
|
|
1826
|
+
};
|
|
1827
|
+
//# sourceMappingURL=chunk-UHLZX2EG.js.map
|