@williambeto/ai-workflow 2.2.7 → 2.3.1

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.
Files changed (68) hide show
  1. package/AGENTS.md +27 -0
  2. package/CHANGELOG.md +34 -0
  3. package/dist-assets/agents/astra.md +74 -45
  4. package/dist-assets/agents/atlas.md +110 -152
  5. package/dist-assets/agents/nexus.md +64 -19
  6. package/dist-assets/agents/orion.md +72 -27
  7. package/dist-assets/agents/phoenix.md +64 -19
  8. package/dist-assets/agents/sage.md +67 -36
  9. package/dist-assets/commands/atlas.md +66 -6
  10. package/dist-assets/commands/audit.md +62 -4
  11. package/dist-assets/commands/deploy.md +66 -5
  12. package/dist-assets/commands/discover.md +72 -4
  13. package/dist-assets/commands/implement.md +66 -18
  14. package/dist-assets/commands/optimize-tokens.md +60 -4
  15. package/dist-assets/commands/plan.md +64 -4
  16. package/dist-assets/commands/release.md +76 -5
  17. package/dist-assets/commands/run.md +62 -16
  18. package/dist-assets/commands/spec-create.md +66 -4
  19. package/dist-assets/commands/spec-implement.md +68 -4
  20. package/dist-assets/commands/spec-review.md +63 -4
  21. package/dist-assets/commands/update-memory.md +62 -4
  22. package/dist-assets/commands/validate.md +70 -6
  23. package/dist-assets/docs/cli-reference.md +38 -10
  24. package/dist-assets/skills/architecture/SKILL.md +62 -7
  25. package/dist-assets/skills/backend-development/SKILL.md +62 -7
  26. package/dist-assets/skills/deployment/SKILL.md +62 -7
  27. package/dist-assets/skills/design-principles/SKILL.md +59 -7
  28. package/dist-assets/skills/documentation/SKILL.md +61 -7
  29. package/dist-assets/skills/frontend-development/SKILL.md +62 -7
  30. package/dist-assets/skills/full-stack-development/SKILL.md +62 -7
  31. package/dist-assets/skills/optimize-tokens/SKILL.md +61 -7
  32. package/dist-assets/skills/pr-workflow/SKILL.md +65 -7
  33. package/dist-assets/skills/product-discovery/SKILL.md +81 -7
  34. package/dist-assets/skills/product-planning/SKILL.md +62 -7
  35. package/dist-assets/skills/project-memory/SKILL.md +55 -22
  36. package/dist-assets/skills/prompt-engineer/SKILL.md +59 -7
  37. package/dist-assets/skills/qa-workflow/SKILL.md +72 -7
  38. package/dist-assets/skills/refactoring/SKILL.md +68 -7
  39. package/dist-assets/skills/release-workflow/SKILL.md +72 -7
  40. package/dist-assets/skills/spec-driven-development/SKILL.md +75 -7
  41. package/dist-assets/skills/technical-leadership/SKILL.md +61 -7
  42. package/dist-assets/skills/ui-ux-design/SKILL.md +60 -7
  43. package/docs/compatibility/provider-usage.md +46 -0
  44. package/docs/compatibility/runtime-matrix.md +31 -0
  45. package/docs/getting-started/DESKTOP_PROMPT.md +52 -0
  46. package/docs/getting-started/quickstart.md +17 -0
  47. package/docs/getting-started/upgrading-to-v2.md +55 -0
  48. package/package.json +20 -4
  49. package/read_only_safety_verification.md +48 -0
  50. package/src/cli.js +4 -2
  51. package/src/commands/collect-evidence.js +3 -39
  52. package/src/commands/doctor.js +16 -0
  53. package/src/commands/execute.js +312 -119
  54. package/src/core/delegation-controller.js +193 -0
  55. package/src/core/evidence/evidence-ledger.js +53 -0
  56. package/src/core/execution-planner.js +4 -1
  57. package/src/core/finalization/finalizer.js +77 -0
  58. package/src/core/finalization/workspace-snapshot.js +110 -0
  59. package/src/core/gates/branch-gate.js +23 -35
  60. package/src/core/healing/healer-engine.js +34 -1
  61. package/src/core/healing/runtime-remediation-executor.js +136 -0
  62. package/src/core/request-classifier.js +15 -5
  63. package/src/core/request-router.js +289 -0
  64. package/src/core/runtime/opencode-adapter.js +170 -62
  65. package/src/core/validation/evidence-collector.js +26 -3
  66. package/src/core/validation/stack-detector.js +65 -0
  67. package/src/core/validation/validation-planner.js +134 -0
  68. package/src/core/workspace/read-only-workspace.js +119 -0
@@ -0,0 +1,53 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ /**
5
+ * EvidenceLedger - Traces structural workflow events to make delegation observable.
6
+ */
7
+ export class EvidenceLedger {
8
+ constructor({ cwd = process.cwd(), workflowId = "unknown" } = {}) {
9
+ this.cwd = cwd;
10
+ this.workflowId = workflowId;
11
+ this.events = [];
12
+ }
13
+
14
+ /**
15
+ * Logs a workflow event to the ledger.
16
+ * @param {Object} eventParam
17
+ * @param {string} eventParam.actor - The agent name (Atlas, Astra, Sage, Phoenix).
18
+ * @param {string} eventParam.eventType - Type of event.
19
+ * @param {string} eventParam.provenance - Originating component.
20
+ * @param {Object} [eventParam.data] - Additional metadata.
21
+ * @returns {Object} The logged event.
22
+ */
23
+ logEvent({ actor, eventType, provenance, data = {}, actorType, observed, runtime }) {
24
+ const event = {
25
+ workflowId: this.workflowId,
26
+ timestamp: new Date().toISOString(),
27
+ actor,
28
+ actorType: actorType !== undefined ? actorType : undefined,
29
+ observed: observed !== undefined ? observed : undefined,
30
+ runtime: runtime !== undefined ? runtime : undefined,
31
+ eventType,
32
+ provenance,
33
+ data
34
+ };
35
+ this.events.push(event);
36
+ console.log(`[LEDGER] [${actor}] ${eventType}: ${JSON.stringify(data)}`);
37
+ return event;
38
+ }
39
+
40
+ getEvents() {
41
+ return this.events;
42
+ }
43
+
44
+ /**
45
+ * Saves the ledger events to a JSON file.
46
+ * @param {string} filePath - Absolute or relative path.
47
+ */
48
+ async save(filePath) {
49
+ const targetPath = path.isAbsolute(filePath) ? filePath : path.join(this.cwd, filePath);
50
+ await fs.mkdir(path.dirname(targetPath), { recursive: true });
51
+ await fs.writeFile(targetPath, JSON.stringify(this.events, null, 2), "utf8");
52
+ }
53
+ }
@@ -21,6 +21,9 @@ export class ExecutionPlanner {
21
21
  ? path.join("docs/workflows", taskSlug, "spec.md")
22
22
  : null;
23
23
 
24
+ const owner = classification.routingDecision?.selectedActor ||
25
+ (classification.intent === "write" ? "Astra" : classification.owner);
26
+
24
27
  // Build default expected validations based on the resolved profile
25
28
  const validationsExpected = [];
26
29
  if (classification.validationNeeded) {
@@ -45,7 +48,7 @@ export class ExecutionPlanner {
45
48
  objective: classification.request,
46
49
  scope: classification.intent === "read-only" ? "Analysis and verification" : "Implementation of requested behavior",
47
50
  restrictions,
48
- owner: classification.owner,
51
+ owner,
49
52
  skills: [...classification.skills],
50
53
  branchNeeded,
51
54
  branchName: branchNeeded ? `feat/${taskSlug}` : null,
@@ -0,0 +1,77 @@
1
+ import { WorkspaceSnapshot } from "./workspace-snapshot.js";
2
+
3
+ export class Finalizer {
4
+ constructor({ cwd = process.cwd() } = {}) {
5
+ this.cwd = cwd;
6
+ this.snapshotManager = new WorkspaceSnapshot({ cwd });
7
+ }
8
+
9
+ /**
10
+ * Compares the current workspace state with a previously captured snapshot.
11
+ * @param {Object} previousSnapshot - The snapshot object captured earlier.
12
+ * @returns {Object} { valid: boolean, changes: { added: string[], modified: string[], deleted: string[] } }
13
+ */
14
+ async verifyIntegrity(previousSnapshot) {
15
+ const currentSnapshot = await this.snapshotManager.capture();
16
+ const added = [];
17
+ const modified = [];
18
+ const deleted = [];
19
+
20
+ let valid = true;
21
+
22
+ // Compare metadata and git trees
23
+ if (previousSnapshot.branch !== currentSnapshot.branch) {
24
+ modified.push(`branch:${previousSnapshot.branch}->${currentSnapshot.branch}`);
25
+ valid = false;
26
+ }
27
+ if (previousSnapshot.head !== currentSnapshot.head) {
28
+ modified.push(`head:${previousSnapshot.head}->${currentSnapshot.head}`);
29
+ valid = false;
30
+ }
31
+ if (previousSnapshot.indexTree !== currentSnapshot.indexTree) {
32
+ modified.push(`indexTree:${previousSnapshot.indexTree}->${currentSnapshot.indexTree}`);
33
+ valid = false;
34
+ }
35
+ if (previousSnapshot.stagedDiffHash !== currentSnapshot.stagedDiffHash) {
36
+ modified.push("stagedDiff");
37
+ valid = false;
38
+ }
39
+ if (previousSnapshot.unstagedDiffHash !== currentSnapshot.unstagedDiffHash) {
40
+ modified.push("unstagedDiff");
41
+ valid = false;
42
+ }
43
+ if (previousSnapshot.untrackedFilesHash !== currentSnapshot.untrackedFilesHash) {
44
+ modified.push("untrackedFiles");
45
+ valid = false;
46
+ }
47
+
48
+ // Check for additions and modifications in files
49
+ const prevFiles = previousSnapshot.files || {};
50
+ const currFiles = currentSnapshot.files || {};
51
+
52
+ for (const [file, info] of Object.entries(currFiles)) {
53
+ const prevInfo = prevFiles[file];
54
+ if (!prevInfo) {
55
+ added.push(file);
56
+ } else if (prevInfo.sha256 !== info.sha256 || prevInfo.mode !== info.mode) {
57
+ modified.push(file);
58
+ }
59
+ }
60
+
61
+ // Check for deletions in files
62
+ for (const file of Object.keys(prevFiles)) {
63
+ if (!currFiles[file]) {
64
+ deleted.push(file);
65
+ }
66
+ }
67
+
68
+ if (added.length > 0 || modified.length > 0 || deleted.length > 0) {
69
+ valid = false;
70
+ }
71
+
72
+ return {
73
+ valid,
74
+ changes: { added, modified, deleted }
75
+ };
76
+ }
77
+ }
@@ -0,0 +1,110 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import crypto from "node:crypto";
4
+ import { execSync } from "node:child_process";
5
+
6
+ export class WorkspaceSnapshot {
7
+ constructor({ cwd = process.cwd() } = {}) {
8
+ this.cwd = cwd;
9
+ }
10
+
11
+ isIgnored(relativePath) {
12
+ const norm = relativePath.replaceAll("\\", "/");
13
+ if (norm === "node_modules" || norm.startsWith("node_modules/")) return true;
14
+ if (norm === "vendor" || norm.startsWith("vendor/")) return true;
15
+ if (norm === ".git" || norm.startsWith(".git/")) return true;
16
+ if (norm === "EVIDENCE.json") return true;
17
+ if (norm === ".ai-workflow/EVIDENCE.json") return true;
18
+ if (norm.startsWith(".ai-workflow/history/")) return true;
19
+ if (norm.startsWith(".ai-workflow/handoffs/")) return true;
20
+ return false;
21
+ }
22
+
23
+ async capture() {
24
+ const files = {};
25
+ await this.scanDir(this.cwd, files);
26
+
27
+ const execGit = (args) => {
28
+ try {
29
+ return execSync(`git ${args}`, { cwd: this.cwd, encoding: "utf8", stdio: "pipe" }).trim();
30
+ } catch {
31
+ return "";
32
+ }
33
+ };
34
+
35
+ const branch = execGit("rev-parse --abbrev-ref HEAD");
36
+ const head = execGit("rev-parse HEAD");
37
+ const indexTree = execGit("write-tree");
38
+
39
+ const stagedDiff = execGit("diff --cached");
40
+ const unstagedDiff = execGit("diff");
41
+ const untrackedFiles = execGit("status --porcelain");
42
+
43
+ const hashString = (str) => {
44
+ return crypto.createHash("sha256").update(str).digest("hex");
45
+ };
46
+
47
+ return {
48
+ branch,
49
+ head,
50
+ indexTree,
51
+ files,
52
+ stagedDiffHash: hashString(stagedDiff),
53
+ unstagedDiffHash: hashString(unstagedDiff),
54
+ untrackedFilesHash: hashString(untrackedFiles)
55
+ };
56
+ }
57
+
58
+ async scanDir(dir, filesMap) {
59
+ try {
60
+ const entries = await fs.readdir(dir, { withFileTypes: true });
61
+ for (const entry of entries) {
62
+ const fullPath = path.join(dir, entry.name);
63
+ const relativePath = path.relative(this.cwd, fullPath);
64
+
65
+ if (this.isIgnored(relativePath)) {
66
+ continue;
67
+ }
68
+
69
+ if (entry.isDirectory()) {
70
+ await this.scanDir(fullPath, filesMap);
71
+ } else if (entry.isFile()) {
72
+ try {
73
+ const stat = await fs.stat(fullPath);
74
+ const content = await fs.readFile(fullPath);
75
+ const sha256 = crypto.createHash("sha256").update(content).digest("hex");
76
+ const isExecutable = (stat.mode & 0o111) !== 0;
77
+ const mode = isExecutable ? "100755" : "100644";
78
+
79
+ filesMap[relativePath] = {
80
+ sha256,
81
+ mode
82
+ };
83
+ } catch {
84
+ // ignore unreadable/locked files
85
+ }
86
+ }
87
+ }
88
+ } catch {
89
+ // ignore
90
+ }
91
+ }
92
+
93
+ calculateHash(snapshot) {
94
+ const hash = crypto.createHash("sha256");
95
+ hash.update(snapshot.branch || "");
96
+ hash.update(snapshot.head || "");
97
+ hash.update(snapshot.indexTree || "");
98
+ hash.update(snapshot.stagedDiffHash || "");
99
+ hash.update(snapshot.unstagedDiffHash || "");
100
+ hash.update(snapshot.untrackedFilesHash || "");
101
+
102
+ const sortedFiles = Object.keys(snapshot.files || {}).sort();
103
+ for (const file of sortedFiles) {
104
+ hash.update(file);
105
+ hash.update(snapshot.files[file].sha256);
106
+ hash.update(snapshot.files[file].mode);
107
+ }
108
+ return hash.digest("hex");
109
+ }
110
+ }
@@ -76,66 +76,54 @@ export class BranchGate {
76
76
  }
77
77
 
78
78
  /**
79
- * @param {string} override
80
- * @param {{autoRecover?: boolean, taskSlug?: string, readOnly?: boolean}} options
79
+ * Strictly verifies branch safety without any override bypasses.
81
80
  */
82
- check(override = "", { autoRecover = false, taskSlug = "implementation", readOnly = false } = {}) {
81
+ check(overrideIgnored = "", { taskSlug = "implementation", readOnly = false } = {}) {
83
82
  if (readOnly) {
84
83
  const currentBranch = this.getCurrentBranch();
85
84
  return { blocked: false, branch: currentBranch, recovered: false, readOnly: true };
86
85
  }
87
86
 
88
87
  try {
89
- // Enforce fail-closed: verify inside worktree first
88
+ // 1. Verify inside worktree (Git availability)
90
89
  try {
91
90
  this.run("git rev-parse --is-inside-work-tree");
92
91
  } catch (e) {
93
- const reason = "Not inside a Git repository or Git is unavailable. Implementation work is blocked.";
92
+ const reason = "Git is unavailable or not inside a Git repository. Implementation work is blocked.";
94
93
  this.log(`BLOCKED: ${reason}`);
95
94
  return { blocked: true, branch: "unknown", reason };
96
95
  }
97
96
 
97
+ // 2. Get current branch
98
98
  const currentBranch = this.getCurrentBranch();
99
- if (currentBranch === "unknown") {
99
+ if (!currentBranch || currentBranch === "unknown" || currentBranch.trim() === "") {
100
100
  const reason = "Could not determine current Git branch name. Implementation work is blocked.";
101
101
  this.log(`BLOCKED: ${reason}`);
102
102
  return { blocked: true, branch: "unknown", reason };
103
103
  }
104
- const isProtected = this.protectedBranches.includes(currentBranch);
105
- if (!isProtected) return { blocked: false, branch: currentBranch, recovered: false };
106
104
 
107
- if (override && override.trim().length >= 20) {
108
- this.log(`AUTHORIZED BYPASS on '${currentBranch}': ${override.trim()}`);
109
- return { blocked: false, branch: currentBranch, authorized: true, recovered: false };
105
+ const isProtected = this.protectedBranches.includes(currentBranch);
106
+ if (!isProtected) {
107
+ return { blocked: false, branch: currentBranch, recovered: false };
110
108
  }
111
109
 
112
- if (autoRecover) {
113
- const dirty = this.getDirtyState();
114
- if (dirty.tracked.length > 0) {
115
- const reason = `Unsafe tracked changes prevent Branch Gate Auto-Recovery: ${dirty.tracked.join(", ")}`;
116
- this.log(`BLOCKED AUTO-RECOVERY on '${currentBranch}': ${reason}`);
117
- return { blocked: true, branch: currentBranch, reason, dirtyState: dirty };
118
- }
119
-
120
- const recoveredBranch = this.createScopedBranch(taskSlug);
121
- this.log(`AUTO-RECOVERED '${currentBranch}' -> '${recoveredBranch}'`);
122
- return {
123
- blocked: false,
124
- branch: recoveredBranch,
125
- branchBefore: currentBranch,
126
- recovered: true,
127
- dirtyState: dirty
128
- };
110
+ // 3. Protected branch checks - any dirty file blocks
111
+ const dirty = this.getDirtyState();
112
+ if (!dirty.clean) {
113
+ const reason = `Direct writes to protected branch '${currentBranch}' are prohibited and the branch is dirty (has changes).`;
114
+ this.log(`BLOCKED: ${reason}`);
115
+ return { blocked: true, branch: currentBranch, reason, dirtyState: dirty };
129
116
  }
130
117
 
131
- const reason = override && override.trim().length < 20
132
- ? "Override justification too short (min 20 chars)."
133
- : `Direct commits to '${currentBranch}' are prohibited.`;
134
- this.log(`BLOCKED ATTEMPT on '${currentBranch}': ${reason}`);
118
+ // 4. Create scoped branch (Auto-recover)
119
+ const recoveredBranch = this.createScopedBranch(taskSlug);
120
+ this.log(`AUTO-RECOVERED '${currentBranch}' -> '${recoveredBranch}'`);
135
121
  return {
136
- blocked: true,
137
- branch: currentBranch,
138
- reason: `${reason} Enable safe auto-recovery or use AI_OVERRIDE with a concrete justification.`
122
+ blocked: false,
123
+ branch: recoveredBranch,
124
+ branchBefore: currentBranch,
125
+ recovered: true,
126
+ dirtyState: dirty
139
127
  };
140
128
  } catch (error) {
141
129
  const reason = `Git command failure on branch gate check: ${error.message}`;
@@ -36,11 +36,12 @@ function collectBlockingFindings(result = {}) {
36
36
  }
37
37
 
38
38
  export class HealerEngine {
39
- constructor({ cwd, mode = "standard", taskSlug = "implementation", maxAttempts = null } = {}) {
39
+ constructor({ cwd, mode = "standard", taskSlug = "implementation", maxAttempts = null, ledger = null } = {}) {
40
40
  this.cwd = cwd;
41
41
  this.mode = MODE_ATTEMPTS[mode] ? mode : "standard";
42
42
  this.taskSlug = taskSlug;
43
43
  this.maxAttempts = maxAttempts ?? MODE_ATTEMPTS[this.mode];
44
+ this.ledger = ledger;
44
45
  }
45
46
 
46
47
  get statePath() {
@@ -139,6 +140,38 @@ export class HealerEngine {
139
140
 
140
141
  const next = await validate({ affectedChecks: this.affectedChecks(current), attempt });
141
142
  const progress = this.hasProgress(current, next, remediation);
143
+
144
+ // Verify strict conditions to declare Phoenix remediation applied/completed
145
+ const findingsConcrete = this.affectedChecks(current).length > 0;
146
+ const hashAltered = remediation.changedFiles && remediation.changedFiles.length > 0;
147
+ const inScope = hashAltered && remediation.changedFiles.every(file => {
148
+ const norm = file.replaceAll("\\", "/");
149
+ if (norm.startsWith(".ai-workflow/")) return false;
150
+ if (norm === "package.json" || norm === "package-lock.json" || norm === "tsconfig.json" || norm === ".gitignore" || norm === "opencode.jsonc" || norm === "EVIDENCE.json") {
151
+ return false;
152
+ }
153
+ return true;
154
+ });
155
+ const validationPassed = next.overallStatus === "PASS" || next.overallStatus === "PASS_WITH_NOTES";
156
+
157
+ const phoenixSuccessful = findingsConcrete && hashAltered && inScope && validationPassed;
158
+
159
+ if (this.ledger && phoenixSuccessful) {
160
+ this.ledger.logEvent({
161
+ actor: "Phoenix",
162
+ actorType: "runtime-agent",
163
+ observed: true,
164
+ runtime: "opencode",
165
+ eventType: "remediation_complete",
166
+ provenance: "opencode-adapter",
167
+ data: {
168
+ success: true,
169
+ changedFiles: remediation.changedFiles,
170
+ event: "remediation.completed"
171
+ }
172
+ });
173
+ }
174
+
142
175
  history.push({
143
176
  attempt,
144
177
  before: current.overallStatus,
@@ -0,0 +1,136 @@
1
+ import { OpenCodeAdapter } from "../runtime/opencode-adapter.js";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { execSync } from "node:child_process";
5
+
6
+ export function createRuntimeRemediationExecutor(cwd, ledger = null) {
7
+ const adapter = new OpenCodeAdapter({ cwd });
8
+
9
+ function getChangedFiles() {
10
+ try {
11
+ const output = execSync("git status --short", { cwd, encoding: "utf8" });
12
+ return output
13
+ .split("\n")
14
+ .filter(Boolean)
15
+ .map((line) => line.slice(3).trim())
16
+ .filter(Boolean);
17
+ } catch {
18
+ return [];
19
+ }
20
+ }
21
+
22
+ return async function runtimeRemediationExecutor({ attempt, maxAttempts, mode, affectedChecks, requestPath, result }) {
23
+ console.log(`\n[HEALER] Running Runtime/Phoenix Structural Remediation [Attempt ${attempt}/${maxAttempts}]`);
24
+ console.log(`[HEALER] Affected checks: ${affectedChecks.join(", ")}`);
25
+
26
+ if (ledger) {
27
+ ledger.logEvent({
28
+ actor: "Phoenix",
29
+ actorType: "runtime-agent",
30
+ observed: true,
31
+ runtime: "opencode",
32
+ eventType: "remediation_start",
33
+ provenance: "opencode-adapter",
34
+ data: {
35
+ attempt,
36
+ affectedChecks,
37
+ event: "remediation.started"
38
+ }
39
+ });
40
+ }
41
+
42
+ let requestContent = {};
43
+ try {
44
+ requestContent = JSON.parse(await fs.readFile(requestPath, "utf8"));
45
+ } catch {
46
+ // ignore/fallback
47
+ }
48
+
49
+ const filesBefore = getChangedFiles();
50
+ const fileContentsBefore = {};
51
+ for (const file of filesBefore) {
52
+ try {
53
+ fileContentsBefore[file] = await fs.readFile(path.join(cwd, file), "utf8");
54
+ } catch {
55
+ fileContentsBefore[file] = null;
56
+ }
57
+ }
58
+
59
+ // Extract concrete command outputs for failing checks
60
+ const failedCommands = (result?.evidence?.commands || [])
61
+ .filter(cmd => ["FAIL", "BLOCKED"].includes(cmd.status));
62
+
63
+ let concreteOutput = "";
64
+ if (failedCommands.length > 0) {
65
+ concreteOutput = failedCommands.map(cmd => {
66
+ return `Command: ${cmd.name} (${Array.isArray(cmd.command) ? cmd.command.join(" ") : cmd.command})
67
+ Exit Code: ${cmd.exitCode}
68
+ Output:
69
+ ${cmd.output}`;
70
+ }).join("\n\n");
71
+ } else {
72
+ concreteOutput = JSON.stringify(result?.quality || result || {}, null, 2);
73
+ }
74
+
75
+ const promptMsg = `We are in remediation mode (attempt ${attempt}/${maxAttempts}).
76
+ The following validation checks have failed:
77
+ ${affectedChecks.join(", ")}
78
+
79
+ Details of the failure:
80
+ \`\`\`
81
+ ${concreteOutput}
82
+ \`\`\`
83
+
84
+ Please inspect the code, identify the root cause of these failures (e.g. failing tests, syntax issues, build errors), and write a correction to the relevant files in the workspace to make the tests/validations pass.`;
85
+
86
+ console.log(`[HEALER] Sending structural healing request to Phoenix agent...`);
87
+ const runResult = await adapter.execute(promptMsg, {
88
+ agent: "Phoenix"
89
+ });
90
+
91
+ const filesAfter = getChangedFiles();
92
+ const changedFiles = [];
93
+ for (const file of filesAfter) {
94
+ if (!filesBefore.includes(file)) {
95
+ changedFiles.push(file);
96
+ } else {
97
+ try {
98
+ const contentAfter = await fs.readFile(path.join(cwd, file), "utf8");
99
+ if (contentAfter !== fileContentsBefore[file]) {
100
+ changedFiles.push(file);
101
+ }
102
+ } catch {
103
+ if (fileContentsBefore[file] !== null) {
104
+ changedFiles.push(file);
105
+ }
106
+ }
107
+ }
108
+ }
109
+ const success = runResult.success && changedFiles.length > 0;
110
+
111
+ if (!runResult.success) {
112
+ console.log(`[HEALER] Phoenix remediation failed: ${runResult.error || "Unknown error"}`);
113
+ return {
114
+ applied: false,
115
+ reason: `Phoenix runtime execution failed: ${runResult.error || "Unknown error"}`
116
+ };
117
+ }
118
+
119
+ if (changedFiles.length === 0) {
120
+ console.log(`[HEALER] Phoenix execution succeeded but no files were modified.`);
121
+ return {
122
+ applied: false,
123
+ reason: "Phoenix runtime executed but did not modify any files in the workspace."
124
+ };
125
+ }
126
+
127
+ console.log(`[HEALER] Phoenix successfully executed remediation commands.`);
128
+ console.log(`[HEALER] Files modified: ${changedFiles.join(", ")}`);
129
+
130
+ return {
131
+ applied: true,
132
+ changedFiles,
133
+ evidenceAdded: ["phoenix-healing-run"]
134
+ };
135
+ };
136
+ }
@@ -1,4 +1,5 @@
1
1
  import { resolveWorkflowProfile, getWorkflowProfile } from "./workflow-profiles.js";
2
+ import { RequestRouter } from "./request-router.js";
2
3
 
3
4
  /**
4
5
  * RequestClassifier - Classifies natural requests into structured, testable contracts.
@@ -9,12 +10,14 @@ export class RequestClassifier {
9
10
  * @param {string} request - The natural language request string.
10
11
  * @returns {Object} Structured classification contract.
11
12
  */
12
- classify(request = "") {
13
+ classify(request = "", options = {}) {
13
14
  const text = String(request).trim();
14
15
  if (!text) {
15
16
  throw new Error("Cannot classify an empty request.");
16
17
  }
17
18
 
19
+ const cwd = options.cwd || process.cwd();
20
+
18
21
  // 1. Resolve Profile
19
22
  const profile = resolveWorkflowProfile({ request: text });
20
23
  const profileDef = getWorkflowProfile(profile);
@@ -43,16 +46,23 @@ export class RequestClassifier {
43
46
  const specNeeded = mode === "full" && intent === "write";
44
47
  const validationNeeded = intent === "write";
45
48
 
49
+ // Integrate central RequestRouter
50
+ const router = new RequestRouter({ cwd });
51
+ const requestUnderstanding = router.understand(text);
52
+ const routingDecision = router.route(requestUnderstanding);
53
+
46
54
  return {
47
55
  request: text,
48
56
  profile,
49
- owner: profileDef.owner,
50
- intent,
57
+ owner: routingDecision.selectedActor || profileDef.owner,
58
+ intent: requestUnderstanding.mutationIntent === "readonly" ? "read-only" : "write",
51
59
  mode,
52
- risk,
60
+ risk: requestUnderstanding.riskLevel,
53
61
  specNeeded,
54
62
  validationNeeded,
55
- skills: [...profileDef.skills]
63
+ skills: [...profileDef.skills],
64
+ requestUnderstanding,
65
+ routingDecision
56
66
  };
57
67
  }
58
68
  }