@williambeto/ai-workflow 2.2.6 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/dist-assets/docs/cli-reference.md +38 -10
- package/dist-assets/docs/compatibility/provider-usage.md +8 -0
- package/dist-assets/docs/compatibility/runtime-matrix.md +1 -0
- package/dist-assets/templates/.antigravityignore.template +8 -0
- package/dist-assets/templates/ANTIGRAVITY.md.template +20 -0
- package/docs/releases/v2.3.0-release-decision.md +35 -0
- package/package.json +14 -4
- package/read_only_safety_verification.md +48 -0
- package/src/adapters/index.js +1 -0
- package/src/adapters/platforms/antigravity.js +382 -0
- package/src/adapters/platforms/codex.js +13 -0
- package/src/adapters/platforms/gemini.js +14 -1
- package/src/cli.js +6 -3
- package/src/commands/collect-evidence.js +3 -39
- package/src/commands/doctor.js +16 -0
- package/src/commands/execute.js +303 -119
- package/src/commands/init.js +25 -2
- package/src/core/delegation-controller.js +134 -0
- package/src/core/evidence/evidence-ledger.js +53 -0
- package/src/core/execution-planner.js +5 -1
- package/src/core/finalization/finalizer.js +77 -0
- package/src/core/finalization/workspace-snapshot.js +110 -0
- package/src/core/gates/branch-gate.js +23 -35
- package/src/core/gates/merge-gate.js +74 -0
- package/src/core/healing/healer-engine.js +34 -1
- package/src/core/healing/runtime-remediation-executor.js +136 -0
- package/src/core/runtime/opencode-adapter.js +137 -62
- package/src/core/templates.js +8 -0
- package/src/core/validation/evidence-collector.js +30 -3
- package/src/core/validation/quality-guard.js +8 -0
- package/src/core/validation/stack-detector.js +65 -0
- package/src/core/validation/validation-planner.js +134 -0
- package/src/core/workspace/read-only-workspace.js +119 -0
|
@@ -87,6 +87,19 @@ Every task completion MUST provide the following payload:
|
|
|
87
87
|
const content = await fs.readFile(path.join(sourceCommandsDir, file), "utf8");
|
|
88
88
|
await fs.writeFile(path.join(codexPromptsDir, targetFileName), content);
|
|
89
89
|
}
|
|
90
|
+
|
|
91
|
+
// 4. Copy governance policy into .agents/docs/policies/ so the relative
|
|
92
|
+
// link ../../docs/policies/SKILLS_COMMON_GOVERNANCE.md in each
|
|
93
|
+
// .agents/skills/{skill}/SKILL.md resolves correctly.
|
|
94
|
+
const codexPoliciesDir = path.join(this.cwd, ".agents", "docs", "policies");
|
|
95
|
+
await fs.mkdir(codexPoliciesDir, { recursive: true });
|
|
96
|
+
const sourcePoliciesDir = path.join(this.cwd, installRoot, "opencode", "docs", "policies");
|
|
97
|
+
const policyFiles = await fs.readdir(sourcePoliciesDir).catch(() => []);
|
|
98
|
+
for (const file of policyFiles) {
|
|
99
|
+
if (!file.endsWith(".md")) continue;
|
|
100
|
+
const policyContent = await fs.readFile(path.join(sourcePoliciesDir, file), "utf8");
|
|
101
|
+
await fs.writeFile(path.join(codexPoliciesDir, file), policyContent);
|
|
102
|
+
}
|
|
90
103
|
}
|
|
91
104
|
|
|
92
105
|
async exists(p) {
|
|
@@ -150,7 +150,20 @@ Every task completion MUST provide the following payload:
|
|
|
150
150
|
}
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
-
// 4.
|
|
153
|
+
// 4. Copy governance policy into .gemini/docs/policies/ so the relative
|
|
154
|
+
// link ../../docs/policies/SKILLS_COMMON_GOVERNANCE.md in each
|
|
155
|
+
// .gemini/skills/{skill}/SKILL.md resolves correctly.
|
|
156
|
+
const geminiPoliciesDir = path.join(geminiDir, "docs", "policies");
|
|
157
|
+
await fs.mkdir(geminiPoliciesDir, { recursive: true });
|
|
158
|
+
const sourcePoliciesDir = path.join(this.cwd, installRoot, "opencode", "docs", "policies");
|
|
159
|
+
const policyFiles = await fs.readdir(sourcePoliciesDir).catch(() => []);
|
|
160
|
+
for (const file of policyFiles) {
|
|
161
|
+
if (!file.endsWith(".md")) continue;
|
|
162
|
+
const content = await fs.readFile(path.join(sourcePoliciesDir, file), "utf8");
|
|
163
|
+
await fs.writeFile(path.join(geminiPoliciesDir, file), content);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// 5. Inject project settings.json
|
|
154
167
|
const settingsPath = path.join(geminiDir, "settings.json");
|
|
155
168
|
if (!(await this.exists(settingsPath))) {
|
|
156
169
|
const settings = {
|
package/src/cli.js
CHANGED
|
@@ -11,7 +11,7 @@ function printHelp() {
|
|
|
11
11
|
Usage:
|
|
12
12
|
ai-workflow execute "<request>" [--task=<slug>] [--request="<request>"]
|
|
13
13
|
ai-workflow run --spec-path=<path>
|
|
14
|
-
ai-workflow init [--yes] [--force] [--dry-run] [--no-install] [--no-overwrite] [--gemini] [--claude] [--codex] [--profile=<profile>]
|
|
14
|
+
ai-workflow init [--yes] [--force] [--dry-run] [--no-install] [--no-overwrite] [--gemini] [--claude] [--codex] [--antigravity] [--profile=<profile>]
|
|
15
15
|
ai-workflow collect-evidence [--task=<slug>] [--mode=<quick|standard|full>] [--dry-run]
|
|
16
16
|
ai-workflow doctor
|
|
17
17
|
|
|
@@ -46,6 +46,7 @@ function parseFlags(args) {
|
|
|
46
46
|
gemini: args.includes("--gemini"),
|
|
47
47
|
claude: args.includes("--claude"),
|
|
48
48
|
codex: args.includes("--codex"),
|
|
49
|
+
antigravity: args.includes("--antigravity"),
|
|
49
50
|
"dev-mode": args.includes("--dev-mode"),
|
|
50
51
|
specPath: specPathArg ? specPathArg.replace("--spec-path=", "") : undefined,
|
|
51
52
|
profile: profileVal || undefined,
|
|
@@ -73,12 +74,14 @@ export async function runCli(args) {
|
|
|
73
74
|
const flags = parseFlags(args.slice(1));
|
|
74
75
|
const positionals = args.slice(1).filter((arg) => !arg.startsWith("-"));
|
|
75
76
|
const request = flags.request || positionals.join(" ");
|
|
76
|
-
await runExecute({
|
|
77
|
+
const result = await runExecute({
|
|
77
78
|
cwd: process.cwd(),
|
|
78
79
|
naturalRequest: request,
|
|
79
|
-
override: args.includes("--override") ? "authorized-override-token" : undefined,
|
|
80
80
|
taskSlug: flags.taskSlug
|
|
81
81
|
});
|
|
82
|
+
if (result && (result.overallStatus === "FAIL_QUALITY_GATE" || result.overallStatus === "BLOCKED" || result.overallStatus === "FAIL")) {
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
82
85
|
return;
|
|
83
86
|
}
|
|
84
87
|
|
|
@@ -1,50 +1,14 @@
|
|
|
1
1
|
import { EvidenceCollector } from "../core/validation/evidence-collector.js";
|
|
2
2
|
import { QualityGuard } from "../core/validation/quality-guard.js";
|
|
3
|
+
import { ValidationPlanner } from "../core/validation/validation-planner.js";
|
|
3
4
|
import { buildDeliverySummary, formatDeliverySummary } from "../core/validation/canonical-finalization.js";
|
|
4
5
|
import fs from "node:fs/promises";
|
|
5
6
|
import path from "node:path";
|
|
6
7
|
|
|
7
|
-
function isNoOpScript(script = "") {
|
|
8
|
-
const value = String(script).trim().toLowerCase();
|
|
9
|
-
return /^(echo\b|true$|exit\s+0$|node\s+-e\s+["']?console\.log)/.test(value) || value.includes("tests-pass");
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function addScriptTask(tasks, scripts, name, command, { rejectNoOp = false } = {}) {
|
|
13
|
-
const script = scripts[name];
|
|
14
|
-
if (!script) return;
|
|
15
|
-
if (!rejectNoOp && name === "test" && isNoOpScript(script)) return;
|
|
16
|
-
if (rejectNoOp && isNoOpScript(script)) {
|
|
17
|
-
tasks.push({ name, command, kind: name === "test" ? "test" : name, presetStatus: "FAIL_QUALITY_GATE", summary: `Configured ${name} script is a no-op: ${script}` });
|
|
18
|
-
return;
|
|
19
|
-
}
|
|
20
|
-
tasks.push({ name, command, kind: name === "test" ? "test" : name });
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function isManagedValidationPath(file = "") {
|
|
24
|
-
return /(^|\/)(node_modules|vendor|dist|coverage|\.cache)(\/|$)/.test(String(file).replaceAll("\\", "/"));
|
|
25
|
-
}
|
|
26
|
-
|
|
27
8
|
export async function runCollectEvidence({ cwd, exitOnError = true, taskSlug = null, mode = null, dryRun = false, profile = "generic", branchRecovery = "NOT_RECORDED" }) {
|
|
28
|
-
const tasks = [];
|
|
29
9
|
const qualityGuard = new QualityGuard({ cwd, taskSlug, mode });
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const scripts = pkg.scripts || {};
|
|
33
|
-
const executableBehavior = await qualityGuard.hasExecutableBehaviorChanges();
|
|
34
|
-
addScriptTask(tasks, scripts, "lint", "npm run lint");
|
|
35
|
-
addScriptTask(tasks, scripts, "test", "npm test", { rejectNoOp: executableBehavior });
|
|
36
|
-
addScriptTask(tasks, scripts, "build", "npm run build");
|
|
37
|
-
addScriptTask(tasks, scripts, "typecheck", "npm run typecheck");
|
|
38
|
-
if (scripts.validate && !String(scripts.validate).includes("ai-workflow collect-evidence")) addScriptTask(tasks, scripts, "validate", "npm run validate");
|
|
39
|
-
|
|
40
|
-
const changed = qualityGuard.getChangedFiles().filter((file) => !isManagedValidationPath(file));
|
|
41
|
-
const hasTs = changed.some((file) => /\.(ts|tsx)$/.test(file)) || await fs.access(path.join(cwd, "tsconfig.json")).then(() => true).catch(() => false);
|
|
42
|
-
if (hasTs && !scripts.typecheck) {
|
|
43
|
-
tasks.push({ name: "typecheck", command: "npm run typecheck", kind: "typecheck", presetStatus: "FAIL_QUALITY_GATE", summary: "TypeScript detected but no typecheck script is configured." });
|
|
44
|
-
}
|
|
45
|
-
} catch {
|
|
46
|
-
// Non-Node projects can still supply validation through project-specific commands.
|
|
47
|
-
}
|
|
10
|
+
const planner = new ValidationPlanner({ cwd, qualityGuard });
|
|
11
|
+
const tasks = await planner.plan(profile);
|
|
48
12
|
|
|
49
13
|
const persist = !dryRun && mode === "full";
|
|
50
14
|
const collector = new EvidenceCollector({ cwd, timeout: 60000, taskSlug, mode, profile, branchRecovery });
|
package/src/commands/doctor.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { exists, readJson, readJsonc } from "../core/filesystem.js";
|
|
4
|
+
import { OpenCodeAdapter } from "../core/runtime/opencode-adapter.js";
|
|
4
5
|
|
|
5
6
|
const REQUIRED_FILES = [
|
|
6
7
|
".ai-workflow",
|
|
@@ -23,6 +24,21 @@ export async function runDoctor({ cwd }) {
|
|
|
23
24
|
|
|
24
25
|
console.log("Doctor report:");
|
|
25
26
|
|
|
27
|
+
// Check OpenCode runtime
|
|
28
|
+
const adapter = new OpenCodeAdapter({ cwd });
|
|
29
|
+
const inspection = await adapter.inspect();
|
|
30
|
+
if (inspection.available) {
|
|
31
|
+
console.log("PASS opencode CLI is available");
|
|
32
|
+
const supportedStr = Object.entries(inspection.supports)
|
|
33
|
+
.filter(([_, v]) => v)
|
|
34
|
+
.map(([k, _]) => k)
|
|
35
|
+
.join(", ");
|
|
36
|
+
console.log(`PASS opencode capabilities detected: ${supportedStr || "none"}`);
|
|
37
|
+
} else {
|
|
38
|
+
hasFailure = true;
|
|
39
|
+
console.log("FAIL opencode CLI is not installed or not available in PATH");
|
|
40
|
+
}
|
|
41
|
+
|
|
26
42
|
for (const relativePath of REQUIRED_FILES) {
|
|
27
43
|
const ok = await exists(path.join(cwd, relativePath));
|
|
28
44
|
|
package/src/commands/execute.js
CHANGED
|
@@ -2,15 +2,89 @@ import { RequestClassifier } from "../core/request-classifier.js";
|
|
|
2
2
|
import { ExecutionPlanner } from "../core/execution-planner.js";
|
|
3
3
|
import { WorkflowStateMachine } from "../core/workflow-state-machine.js";
|
|
4
4
|
import { BranchGate } from "../core/gates/branch-gate.js";
|
|
5
|
+
import { EvidenceLedger } from "../core/evidence/evidence-ledger.js";
|
|
6
|
+
import { DelegationController } from "../core/delegation-controller.js";
|
|
5
7
|
import { OpenCodeAdapter } from "../core/runtime/opencode-adapter.js";
|
|
6
8
|
import { runCollectEvidence } from "./collect-evidence.js";
|
|
7
9
|
import { QualityGuard } from "../core/validation/quality-guard.js";
|
|
8
10
|
import { HandoffEngine } from "../core/handoff/handoff-engine.js";
|
|
9
11
|
import { HealerEngine } from "../core/healing/healer-engine.js";
|
|
10
|
-
import {
|
|
12
|
+
import { createRuntimeRemediationExecutor } from "../core/healing/runtime-remediation-executor.js";
|
|
11
13
|
import { isRecoverableGateFailure, isTerminalFailure } from "../core/statuses.js";
|
|
14
|
+
import { Finalizer } from "../core/finalization/finalizer.js";
|
|
12
15
|
import fs from "node:fs/promises";
|
|
13
16
|
import path from "node:path";
|
|
17
|
+
import { execSync } from "node:child_process";
|
|
18
|
+
|
|
19
|
+
async function captureReadOnlyState(cwd) {
|
|
20
|
+
const finalizer = new Finalizer({ cwd });
|
|
21
|
+
const snapshot = await finalizer.snapshotManager.capture();
|
|
22
|
+
|
|
23
|
+
const execGit = (args) => {
|
|
24
|
+
try {
|
|
25
|
+
return execSync(`git ${args}`, { cwd, encoding: "utf8", stdio: "pipe" }).trim();
|
|
26
|
+
} catch {
|
|
27
|
+
return "";
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const head = execGit("rev-parse HEAD");
|
|
32
|
+
const branch = execGit("rev-parse --abbrev-ref HEAD");
|
|
33
|
+
const indexTree = execGit("write-tree");
|
|
34
|
+
const porcelainStatus = execGit("status --porcelain");
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
snapshot,
|
|
38
|
+
head,
|
|
39
|
+
branch,
|
|
40
|
+
indexTree,
|
|
41
|
+
porcelainStatus
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function compareReadOnlyState(before, after) {
|
|
46
|
+
const violations = [];
|
|
47
|
+
|
|
48
|
+
// 1. Compare branch
|
|
49
|
+
if (before.branch !== after.branch) {
|
|
50
|
+
violations.push(`Branch changed from '${before.branch}' to '${after.branch}'`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 2. Compare HEAD
|
|
54
|
+
if (before.head !== after.head) {
|
|
55
|
+
violations.push(`Git HEAD changed from '${before.head}' to '${after.head}' (commit or reset made inside read-only task)`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 3. Compare index tree
|
|
59
|
+
if (before.indexTree !== after.indexTree) {
|
|
60
|
+
violations.push(`Git index tree changed from '${before.indexTree}' to '${after.indexTree}'`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 4. Compare porcelain status
|
|
64
|
+
if (before.porcelainStatus !== after.porcelainStatus) {
|
|
65
|
+
violations.push(`Git status changed. Before:\n${before.porcelainStatus}\nAfter:\n${after.porcelainStatus}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 5. Compare workspace files (SHA-256 hashes)
|
|
69
|
+
const beforeFiles = before.snapshot.files || {};
|
|
70
|
+
const afterFiles = after.snapshot.files || {};
|
|
71
|
+
|
|
72
|
+
for (const [file, info] of Object.entries(afterFiles)) {
|
|
73
|
+
const prevInfo = beforeFiles[file];
|
|
74
|
+
if (!prevInfo) {
|
|
75
|
+
violations.push(`File added: ${file}`);
|
|
76
|
+
} else if (prevInfo.sha256 !== info.sha256) {
|
|
77
|
+
violations.push(`File content modified: ${file}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
for (const file of Object.keys(beforeFiles)) {
|
|
81
|
+
if (!afterFiles[file]) {
|
|
82
|
+
violations.push(`File deleted: ${file}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return violations;
|
|
87
|
+
}
|
|
14
88
|
|
|
15
89
|
function slugify(text) {
|
|
16
90
|
return text
|
|
@@ -34,7 +108,7 @@ function combineValidation(evidence, quality) {
|
|
|
34
108
|
/**
|
|
35
109
|
* runExecute - Coordinators natural request execution.
|
|
36
110
|
*/
|
|
37
|
-
export async function runExecute({ cwd, naturalRequest,
|
|
111
|
+
export async function runExecute({ cwd, naturalRequest, taskSlug: taskSlugOverride }) {
|
|
38
112
|
if (!naturalRequest || !naturalRequest.trim()) {
|
|
39
113
|
throw new Error("Missing request. Please provide a natural request string via positional arguments or --request flag.");
|
|
40
114
|
}
|
|
@@ -42,131 +116,241 @@ export async function runExecute({ cwd, naturalRequest, override, taskSlug: task
|
|
|
42
116
|
const taskSlug = taskSlugOverride || slugify(naturalRequest);
|
|
43
117
|
console.log(`\n[AI WORKFLOW] Executing natural request intake [slug: ${taskSlug}]...\n`);
|
|
44
118
|
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
// 1. Classification
|
|
48
|
-
const classifier = new RequestClassifier();
|
|
49
|
-
const classification = classifier.classify(naturalRequest);
|
|
50
|
-
console.log(`[CLASSIFIED] Intent: ${classification.intent}, Mode: ${classification.mode}, Profile: ${classification.profile}`);
|
|
51
|
-
stateMachine.transitionTo("CLASSIFIED");
|
|
52
|
-
|
|
53
|
-
// 2. Planning
|
|
54
|
-
const planner = new ExecutionPlanner({ cwd });
|
|
55
|
-
const plan = planner.plan(classification, taskSlug);
|
|
56
|
-
console.log(`[PLANNED] Owner: ${plan.owner}, Remediation limit: ${plan.remediationLimit}`);
|
|
57
|
-
stateMachine.transitionTo("PLANNED");
|
|
58
|
-
|
|
59
|
-
// 3. Branch Gate
|
|
60
|
-
const branchGate = new BranchGate({ memoryDir: path.join(cwd, ".ai-workflow"), cwd });
|
|
61
|
-
const gateResult = branchGate.check(override, {
|
|
62
|
-
autoRecover: true,
|
|
63
|
-
taskSlug,
|
|
64
|
-
readOnly: !plan.branchNeeded
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
if (gateResult.blocked) {
|
|
68
|
-
stateMachine.transitionTo("BLOCKED");
|
|
69
|
-
throw new Error(`[GATE BLOCKED] ${gateResult.reason}`);
|
|
70
|
-
}
|
|
71
|
-
console.log(`[PASS] Branch Gate: ${gateResult.recovered ? `${gateResult.branchBefore} -> ${gateResult.branch}` : `${gateResult.branch} is authorized`}.`);
|
|
72
|
-
stateMachine.transitionTo("BRANCH_READY");
|
|
73
|
-
|
|
74
|
-
// 4. Spec template creation (for full/deep mode)
|
|
75
|
-
if (plan.specPath) {
|
|
76
|
-
const fullSpecPath = path.join(cwd, plan.specPath);
|
|
77
|
-
const specExists = await fs.access(fullSpecPath).then(() => true).catch(() => false);
|
|
78
|
-
if (!specExists) {
|
|
79
|
-
await fs.mkdir(path.dirname(fullSpecPath), { recursive: true });
|
|
80
|
-
const specTemplatePath = path.join(cwd, ".ai-workflow/templates/specs/standard.md");
|
|
81
|
-
const templateContent = await fs.readFile(specTemplatePath, "utf8").catch(() => {
|
|
82
|
-
return `# [STANDARD] Specification: ${classification.request}\n\n## Metadata\n\n- ID: SPEC-${taskSlug}\n- Author: Spec-Engineer\n- Status: DRAFT\n- Date: ${new Date().toISOString().split("T")[0]}\n\n## Functional Requirements\n\n- ${classification.request}\n\n## Technical Implementation Plan\n\n### Files to Create/Modify\n\n- \`src/index.js\`\n\n## Acceptance Criteria\n\n- [ ] Implemented successfully\n\n## Testing Strategy\n\n- [ ] Behavior tests pass`;
|
|
83
|
-
});
|
|
84
|
-
await fs.writeFile(fullSpecPath, templateContent);
|
|
85
|
-
console.log(`[EXECUTE] Created DRAFT specification template at: ${plan.specPath}`);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
119
|
+
const ledger = new EvidenceLedger({ cwd, workflowId: taskSlug });
|
|
120
|
+
const delegationController = new DelegationController({ cwd, ledger, adapter: new OpenCodeAdapter({ cwd }) });
|
|
88
121
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
122
|
+
let plan = null;
|
|
123
|
+
try {
|
|
124
|
+
const stateMachine = new WorkflowStateMachine();
|
|
92
125
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
126
|
+
// 1. Classification
|
|
127
|
+
const classifier = new RequestClassifier();
|
|
128
|
+
const classification = classifier.classify(naturalRequest);
|
|
129
|
+
console.log(`[CLASSIFIED] Intent: ${classification.intent}, Mode: ${classification.mode}, Profile: ${classification.profile}`);
|
|
130
|
+
stateMachine.transitionTo("CLASSIFIED");
|
|
98
131
|
|
|
99
|
-
|
|
100
|
-
agent: plan.owner,
|
|
101
|
-
dangerouslySkipPermissions: true
|
|
102
|
-
});
|
|
132
|
+
await delegationController.route(naturalRequest, classification);
|
|
103
133
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
exitOnError: false,
|
|
134
|
+
// 2. Planning
|
|
135
|
+
const planner = new ExecutionPlanner({ cwd });
|
|
136
|
+
plan = planner.plan(classification, taskSlug);
|
|
137
|
+
console.log(`[PLANNED] Owner: ${plan.owner}, Remediation limit: ${plan.remediationLimit}`);
|
|
138
|
+
stateMachine.transitionTo("PLANNED");
|
|
139
|
+
|
|
140
|
+
// 3. Branch Gate
|
|
141
|
+
const branchGate = new BranchGate({ memoryDir: path.join(cwd, ".ai-workflow"), cwd });
|
|
142
|
+
const gateResult = branchGate.check(undefined, {
|
|
143
|
+
autoRecover: true,
|
|
115
144
|
taskSlug,
|
|
116
|
-
|
|
117
|
-
profile: plan.profile,
|
|
118
|
-
branchRecovery: gateResult.recovered ? `${gateResult.branchBefore} -> ${gateResult.branch}` : "NOT_REQUIRED"
|
|
145
|
+
readOnly: !plan.branchNeeded
|
|
119
146
|
});
|
|
120
|
-
const quality = await new QualityGuard({ cwd, taskSlug, mode: plan.mode }).verify();
|
|
121
|
-
return combineValidation(evidence, quality);
|
|
122
|
-
};
|
|
123
147
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
148
|
+
if (gateResult.blocked) {
|
|
149
|
+
stateMachine.transitionTo("BLOCKED");
|
|
150
|
+
throw new Error(`[GATE BLOCKED] ${gateResult.reason}`);
|
|
151
|
+
}
|
|
152
|
+
console.log(`[PASS] Branch Gate: ${gateResult.recovered ? `${gateResult.branchBefore} -> ${gateResult.branch}` : `${gateResult.branch} is authorized`}.`);
|
|
153
|
+
stateMachine.transitionTo("BRANCH_READY");
|
|
154
|
+
|
|
155
|
+
const checkBranchSafety = () => {
|
|
156
|
+
if (plan.branchNeeded) {
|
|
157
|
+
const currentBranch = branchGate.getCurrentBranch();
|
|
158
|
+
if (branchGate.protectedBranches.includes(currentBranch)) {
|
|
159
|
+
stateMachine.transitionTo("BLOCKED");
|
|
160
|
+
throw new Error(`[WORKFLOW BLOCKED] Security violation: current branch is protected '${currentBranch}'!`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
// 4. Spec template creation (for full/deep mode)
|
|
166
|
+
if (plan.specPath) {
|
|
167
|
+
const fullSpecPath = path.join(cwd, plan.specPath);
|
|
168
|
+
const specExists = await fs.access(fullSpecPath).then(() => true).catch(() => false);
|
|
169
|
+
if (!specExists) {
|
|
170
|
+
await fs.mkdir(path.dirname(fullSpecPath), { recursive: true });
|
|
171
|
+
const specTemplatePath = path.join(cwd, ".ai-workflow/templates/specs/standard.md");
|
|
172
|
+
const templateContent = await fs.readFile(specTemplatePath, "utf8").catch(() => {
|
|
173
|
+
return `# [STANDARD] Specification: ${classification.request}\n\n## Metadata\n\n- ID: SPEC-${taskSlug}\n- Author: Spec-Engineer\n- Status: DRAFT\n- Date: ${new Date().toISOString().split("T")[0]}\n\n## Functional Requirements\n\n- ${classification.request}\n\n## Technical Implementation Plan\n\n### Files to Create/Modify\n\n- \`src/index.js\`\n\n## Acceptance Criteria\n\n- [ ] Implemented successfully\n\n## Testing Strategy\n\n- [ ] Behavior tests pass`;
|
|
174
|
+
});
|
|
175
|
+
await fs.writeFile(fullSpecPath, templateContent);
|
|
176
|
+
console.log(`[EXECUTE] Created DRAFT specification template at: ${plan.specPath}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// 5. Delegation
|
|
181
|
+
stateMachine.transitionTo("DELEGATED");
|
|
182
|
+
stateMachine.transitionTo("IMPLEMENTING");
|
|
183
|
+
|
|
184
|
+
let promptMsg = classification.request;
|
|
185
|
+
if (plan.specPath) {
|
|
186
|
+
promptMsg = `Please review and fill in the specification file at: ${plan.specPath}. Make sure to change the Status field to 'APPROVED' in the Metadata section, and then implement the behavior described.`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
let readOnlyStateBefore = null;
|
|
190
|
+
if (!plan.branchNeeded) {
|
|
191
|
+
readOnlyStateBefore = await captureReadOnlyState(cwd);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const runResult = await delegationController.implement(promptMsg, plan.owner, { readOnly: !plan.branchNeeded });
|
|
195
|
+
|
|
196
|
+
if (!runResult.success) {
|
|
197
|
+
console.error(`[EXECUTE] OpenCode adapter execution reported failure: ${runResult.error || "Unknown error"}`);
|
|
198
|
+
stateMachine.transitionTo("BLOCKED");
|
|
199
|
+
throw new Error(`[WORKFLOW BLOCKED] OpenCode runtime execution failed: ${runResult.error || "Unknown error"}`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (!plan.branchNeeded) {
|
|
203
|
+
const readOnlyStateAfter = await captureReadOnlyState(cwd);
|
|
204
|
+
const violations = compareReadOnlyState(readOnlyStateBefore, readOnlyStateAfter);
|
|
205
|
+
if (violations.length > 0) {
|
|
206
|
+
console.error(`[EXECUTE] Read-only confinement violation detected:`);
|
|
207
|
+
for (const v of violations) {
|
|
208
|
+
console.error(` - ${v}`);
|
|
209
|
+
}
|
|
210
|
+
stateMachine.transitionTo("BLOCKED");
|
|
211
|
+
throw new Error(`[WORKFLOW BLOCKED] Read-only confinement violation: files were written/modified, index or commits modified: \n${violations.join("\n")}`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
stateMachine.transitionTo("IMPLEMENTED");
|
|
215
|
+
stateMachine.transitionTo("VALIDATING");
|
|
216
|
+
stateMachine.transitionTo("COMPLETED");
|
|
217
|
+
|
|
218
|
+
console.log("\n--- Final Handoff ---");
|
|
219
|
+
console.log(`\n[AI WORKFLOW COMPLETE] COMPLETED`);
|
|
220
|
+
console.log(`Handoff Packet: [IN-MEMORY] (No handoff file written in read-only mode)\n`);
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
overallStatus: "PASS",
|
|
224
|
+
evidence: {
|
|
225
|
+
internalStatus: "PASS",
|
|
226
|
+
commandsRun: []
|
|
227
|
+
},
|
|
228
|
+
quality: {
|
|
229
|
+
overallStatus: "PASS"
|
|
230
|
+
},
|
|
231
|
+
stateHistory: stateMachine.getHistory()
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
stateMachine.transitionTo("IMPLEMENTED");
|
|
236
|
+
checkBranchSafety();
|
|
237
|
+
|
|
238
|
+
// Capture snapshot immediately after last implementation write
|
|
239
|
+
const finalizer = new Finalizer({ cwd });
|
|
240
|
+
let lastWriteSnapshot = await finalizer.snapshotManager.capture();
|
|
241
|
+
|
|
242
|
+
// 6. Validation
|
|
243
|
+
stateMachine.transitionTo("VALIDATING");
|
|
244
|
+
const validateWorkflow = async () => {
|
|
245
|
+
const evidence = await runCollectEvidence({
|
|
246
|
+
cwd,
|
|
247
|
+
exitOnError: false,
|
|
248
|
+
taskSlug,
|
|
249
|
+
mode: plan.mode,
|
|
250
|
+
profile: plan.profile,
|
|
251
|
+
branchRecovery: gateResult.recovered ? `${gateResult.branchBefore} -> ${gateResult.branch}` : "NOT_REQUIRED"
|
|
252
|
+
});
|
|
253
|
+
const quality = await new QualityGuard({ cwd, taskSlug, mode: plan.mode }).verify();
|
|
254
|
+
return combineValidation(evidence, quality);
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
let result = await delegationController.validate(taskSlug, plan.mode, plan.profile, validateWorkflow);
|
|
258
|
+
|
|
259
|
+
// 7. Bounded Remediation
|
|
260
|
+
if (isRecoverableGateFailure(result.overallStatus)) {
|
|
261
|
+
console.log(`\n[REMEDIATION REQUIRED] ${result.overallStatus}. Starting bounded ${plan.mode} remediation.`);
|
|
262
|
+
const executor = createRuntimeRemediationExecutor(cwd, ledger);
|
|
263
|
+
const healer = new HealerEngine({ cwd, mode: plan.mode, taskSlug, ledger });
|
|
264
|
+
|
|
265
|
+
result = await healer.run({
|
|
266
|
+
initialResult: result,
|
|
267
|
+
validate: async () => {
|
|
268
|
+
stateMachine.transitionTo("REVALIDATING");
|
|
269
|
+
const res = await delegationController.validate(taskSlug, plan.mode, plan.profile, validateWorkflow);
|
|
270
|
+
return res;
|
|
271
|
+
},
|
|
272
|
+
remediate: async (remedCtx) => {
|
|
273
|
+
stateMachine.transitionTo("REMEDIATING");
|
|
274
|
+
const remRes = await executor(remedCtx);
|
|
275
|
+
if (remRes.applied) {
|
|
276
|
+
checkBranchSafety();
|
|
277
|
+
// Update snapshot because remediation wrote new changes
|
|
278
|
+
lastWriteSnapshot = await finalizer.snapshotManager.capture();
|
|
279
|
+
}
|
|
280
|
+
return remRes;
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (isTerminalFailure(result.overallStatus)) {
|
|
286
|
+
stateMachine.transitionTo("BLOCKED");
|
|
287
|
+
throw new Error(`[WORKFLOW BLOCKED] ${result.overallStatus}: Validation could not be resolved safely.`);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
checkBranchSafety();
|
|
291
|
+
|
|
292
|
+
// 8. Finalizer / Stabilization Loop
|
|
293
|
+
let integrity = await finalizer.verifyIntegrity(lastWriteSnapshot);
|
|
294
|
+
let revalidationCount = 0;
|
|
295
|
+
const maxRevalidations = 2;
|
|
296
|
+
|
|
297
|
+
while (!integrity.valid && revalidationCount < maxRevalidations) {
|
|
298
|
+
console.log(`\n[FINALIZER WARNING] Workspace mutated during validation! Re-running validation to stabilize.`);
|
|
299
|
+
console.log(`Changes detected:`, integrity.changes);
|
|
300
|
+
|
|
301
|
+
checkBranchSafety();
|
|
302
|
+
|
|
303
|
+
// Update snapshot to current workspace state before validation runs
|
|
304
|
+
lastWriteSnapshot = await finalizer.snapshotManager.capture();
|
|
305
|
+
revalidationCount++;
|
|
306
|
+
|
|
307
|
+
// Re-run validation
|
|
308
|
+
result = await delegationController.validate(taskSlug, plan.mode, plan.profile, validateWorkflow);
|
|
309
|
+
|
|
310
|
+
checkBranchSafety();
|
|
311
|
+
|
|
312
|
+
// Verify integrity against the snapshot taken before this validation iteration
|
|
313
|
+
integrity = await finalizer.verifyIntegrity(lastWriteSnapshot);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
let finalStatus = result.overallStatus;
|
|
317
|
+
if (!integrity.valid) {
|
|
318
|
+
console.log(`\n[FINALIZER BLOCKED] BLOCKED: workspace did not stabilize`);
|
|
319
|
+
console.log(`Final changes detected:`, integrity.changes);
|
|
320
|
+
finalStatus = "FAIL_QUALITY_GATE";
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const finalState = finalStatus === "PASS"
|
|
324
|
+
? "COMPLETED"
|
|
325
|
+
: finalStatus === "PASS_WITH_NOTES"
|
|
326
|
+
? "COMPLETED_WITH_NOTES"
|
|
327
|
+
: "BLOCKED";
|
|
328
|
+
|
|
329
|
+
stateMachine.transitionTo(finalState);
|
|
330
|
+
|
|
331
|
+
console.log("\n--- Final Handoff ---");
|
|
332
|
+
const handoffEngine = new HandoffEngine({ cwd });
|
|
333
|
+
const handoffPath = await handoffEngine.generate({
|
|
334
|
+
taskId: taskSlug,
|
|
335
|
+
status: finalState,
|
|
336
|
+
specPaths: plan.specPath ? [plan.specPath] : [],
|
|
337
|
+
evidence: result.evidence,
|
|
338
|
+
nextActions: `Profile ${plan.profile}. Observed execution completed with status ${finalState}.`
|
|
141
339
|
});
|
|
142
|
-
}
|
|
143
340
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
throw new Error(`[WORKFLOW BLOCKED] ${result.overallStatus}: Validation could not be resolved safely.`);
|
|
147
|
-
}
|
|
341
|
+
console.log(`\n[AI WORKFLOW COMPLETE] ${finalState}`);
|
|
342
|
+
console.log(`Handoff Packet: ${path.relative(cwd, handoffPath)}\n`);
|
|
148
343
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
taskId: taskSlug,
|
|
162
|
-
status: finalState,
|
|
163
|
-
specPaths: plan.specPath ? [plan.specPath] : [],
|
|
164
|
-
evidence: result.evidence,
|
|
165
|
-
nextActions: `Profile ${plan.profile}. Observed execution completed with status ${finalState}.`
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
console.log(`\n[AI WORKFLOW COMPLETE] ${finalState}`);
|
|
169
|
-
console.log(`Handoff Packet: ${path.relative(cwd, handoffPath)}\n`);
|
|
170
|
-
|
|
171
|
-
return { ...result, stateHistory: stateMachine.getHistory() };
|
|
344
|
+
return { ...result, overallStatus: finalStatus, stateHistory: stateMachine.getHistory() };
|
|
345
|
+
} finally {
|
|
346
|
+
if (typeof plan !== "undefined" && plan && !plan.branchNeeded) {
|
|
347
|
+
console.log(`\n[READ-ONLY LEDGER] (In-Memory/Stdout)\n${JSON.stringify(ledger.getEvents(), null, 2)}\n`);
|
|
348
|
+
} else {
|
|
349
|
+
try {
|
|
350
|
+
await ledger.save(`.ai-workflow/history/${taskSlug}-ledger.json`);
|
|
351
|
+
} catch (err) {
|
|
352
|
+
// ignore or log
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
172
356
|
}
|