@williambeto/ai-workflow 2.4.8 → 2.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/bin/ai-workflow.d.ts +3 -0
  2. package/bin/ai-workflow.js +2669 -1
  3. package/bin/ai-workflow.js.map +1 -0
  4. package/chunk-2AOV2ATY.js +749 -0
  5. package/chunk-5WRI5ZAA.js +31 -0
  6. package/chunk-6EMG6FAO.js +734 -0
  7. package/chunk-UHLZX2EG.js +1827 -0
  8. package/core/index.d.ts +440 -0
  9. package/core/index.js +32 -0
  10. package/core/index.js.map +1 -0
  11. package/dist-assets/AGENTS.md +23 -6
  12. package/dist-assets/agents/atlas.md +8 -8
  13. package/dist-assets/agents/nexus.md +4 -4
  14. package/dist-assets/agents/orion.md +1 -1
  15. package/dist-assets/agents/phoenix.md +1 -1
  16. package/dist-assets/agents/sage.md +1 -1
  17. package/dist-assets/commands/implement.md +1 -1
  18. package/dist-assets/docs/QUICKSTART.md +8 -8
  19. package/dist-assets/docs/cli-reference.md +7 -7
  20. package/dist-assets/docs/compatibility/provider-usage.md +1 -9
  21. package/dist-assets/docs/compatibility/runtime-matrix.md +5 -6
  22. package/dist-assets/docs/consumer-onboarding.md +2 -2
  23. package/dist-assets/docs/design-patterns-policy.md +1 -1
  24. package/dist-assets/docs/full-documentation.md +0 -2
  25. package/dist-assets/docs/npm-consumer-quickstart.md +70 -109
  26. package/dist-assets/docs/policies/01-BRANCH_GATE.md +1 -1
  27. package/dist-assets/docs/policies/06-FINAL_EVIDENCE_CONTRACT.md +2 -2
  28. package/dist-assets/docs/policies/08-PRODUCT_TRUTHFULNESS_AND_PROJECT_DOCS.md +1 -1
  29. package/dist-assets/docs/policies/10-BEHAVIORAL_CONTRACT_HARDENING.md +1 -1
  30. package/dist-assets/docs/policies/ORCHESTRATION_PROTOCOL.md +8 -9
  31. package/dist-assets/docs/policies/PROCEDURE_DELIVERY_ARTIFACTS.md +4 -10
  32. package/dist-assets/docs/policies/SKILLS_COMMON_GOVERNANCE.md +1 -1
  33. package/dist-assets/docs/profiles/README.md +2 -2
  34. package/dist-assets/docs/specs/runtime-operational-contract.md +12 -12
  35. package/dist-assets/docs/troubleshooting-guide.md +2 -2
  36. package/dist-assets/docs/visual-validation-guide.md +65 -3
  37. package/dist-assets/prompts/01-create-requirement.md +2 -2
  38. package/dist-assets/prompts/02-create-spec.md +2 -2
  39. package/dist-assets/prompts/03-create-tech-plan.md +4 -4
  40. package/dist-assets/prompts/04-breakdown-prs.md +4 -4
  41. package/dist-assets/prompts/05-implement-pr.md +5 -5
  42. package/dist-assets/prompts/06-review-and-fix.md +3 -3
  43. package/dist-assets/prompts/07-apply-design.md +2 -2
  44. package/dist-assets/prompts/08-validate.md +5 -5
  45. package/dist-assets/prompts/09-deploy.md +5 -5
  46. package/dist-assets/prompts/commands/implement.md +1 -1
  47. package/dist-assets/prompts/commands/tech-plan.md +2 -2
  48. package/dist-assets/prompts/commands/validate.md +2 -2
  49. package/dist-assets/runbooks/deploy-checklist.md +5 -5
  50. package/dist-assets/runbooks/how-to-use-skills.md +10 -0
  51. package/dist-assets/runbooks/validation-checklist.md +5 -5
  52. package/dist-assets/schemas/evidence.schema.json +109 -0
  53. package/dist-assets/templates/ANTIGRAVITY.md.template +7 -5
  54. package/dist-assets/templates/CLAUDE.md.template +7 -5
  55. package/dist-assets/templates/CODEX.md.template +7 -5
  56. package/evidence-validator-76ZQQYDU.js +6661 -0
  57. package/package.json +7 -65
  58. package/{src/core/workspace/read-only-workspace.js → read-only-workspace-PZBE7KAX.js} +14 -31
  59. package/{src/commands/validate.js → validate-Q2NLLQ5G.js} +30 -22
  60. package/AGENTS.md +0 -34
  61. package/CHANGELOG.md +0 -237
  62. package/LICENSE +0 -21
  63. package/PUBLISH_MANIFEST.json +0 -34
  64. package/README.md +0 -129
  65. package/dist-assets/runbooks/use-linear-for-operational-planning.md +0 -45
  66. package/dist-assets/runbooks/use-napkin-project-memory.md +0 -33
  67. package/dist-assets/templates/.geminiignore.template +0 -8
  68. package/dist-assets/templates/GEMINI.md.template +0 -20
  69. package/docs/compatibility/provider-usage.md +0 -46
  70. package/docs/compatibility/runtime-matrix.md +0 -31
  71. package/docs/getting-started/DESKTOP_PROMPT.md +0 -52
  72. package/docs/getting-started/quickstart.md +0 -117
  73. package/docs/getting-started/troubleshooting.md +0 -123
  74. package/docs/getting-started/upgrading-to-v2.md +0 -62
  75. package/read_only_safety_verification.md +0 -48
  76. package/src/adapters/index.js +0 -4
  77. package/src/adapters/platforms/antigravity.js +0 -382
  78. package/src/adapters/platforms/claude.js +0 -126
  79. package/src/adapters/platforms/codex.js +0 -113
  80. package/src/adapters/platforms/gemini.js +0 -245
  81. package/src/cli.js +0 -155
  82. package/src/commands/clean.js +0 -106
  83. package/src/commands/collect-evidence.js +0 -25
  84. package/src/commands/doctor.js +0 -202
  85. package/src/commands/execute.js +0 -382
  86. package/src/commands/init.js +0 -359
  87. package/src/commands/run.js +0 -112
  88. package/src/core/backup.js +0 -65
  89. package/src/core/completion-contract.js +0 -35
  90. package/src/core/delegation-controller.js +0 -193
  91. package/src/core/evidence/evidence-ledger.js +0 -98
  92. package/src/core/execution-planner.js +0 -62
  93. package/src/core/filesystem.js +0 -141
  94. package/src/core/finalization/finalizer.js +0 -165
  95. package/src/core/finalization/workspace-snapshot.js +0 -110
  96. package/src/core/gates/branch-gate.js +0 -134
  97. package/src/core/gates/merge-gate.js +0 -74
  98. package/src/core/handoff/handoff-engine.js +0 -104
  99. package/src/core/healing/cli-remediation-executor.js +0 -153
  100. package/src/core/healing/healer-engine.js +0 -212
  101. package/src/core/healing/runtime-remediation-executor.js +0 -136
  102. package/src/core/identity.js +0 -43
  103. package/src/core/install-plan.js +0 -34
  104. package/src/core/opencode-merge.js +0 -150
  105. package/src/core/package-assets.js +0 -141
  106. package/src/core/request-classifier.js +0 -73
  107. package/src/core/request-router.js +0 -289
  108. package/src/core/runtime/opencode-adapter.js +0 -202
  109. package/src/core/sdd/validator.js +0 -67
  110. package/src/core/statuses.js +0 -29
  111. package/src/core/symlink-layout.js +0 -93
  112. package/src/core/templates.js +0 -229
  113. package/src/core/validation/artifact-fidelity-gate.js +0 -445
  114. package/src/core/validation/canonical-finalization.js +0 -43
  115. package/src/core/validation/delivery-decision-engine.js +0 -344
  116. package/src/core/validation/evidence-collector.js +0 -245
  117. package/src/core/validation/quality-guard.js +0 -251
  118. package/src/core/validation/stack-detector.js +0 -65
  119. package/src/core/validation/validation-planner.js +0 -134
  120. package/src/core/validation/visual-verifier.js +0 -175
  121. package/src/core/workflow-profiles.js +0 -107
  122. package/src/core/workflow-state-machine.js +0 -46
@@ -1,110 +0,0 @@
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
- }
@@ -1,134 +0,0 @@
1
- import { execSync } from "node:child_process";
2
- import fs from "node:fs";
3
- import path from "node:path";
4
-
5
- function slugify(value = "task") {
6
- return String(value)
7
- .toLowerCase()
8
- .replace(/[^a-z0-9]+/g, "-")
9
- .replace(/^-+|-+$/g, "")
10
- .slice(0, 48) || "task";
11
- }
12
-
13
- /**
14
- * BranchGate - Protects main/master and can recover safely onto a scoped branch.
15
- */
16
- export class BranchGate {
17
- constructor({ protectedBranches = ["main", "master"], memoryDir, cwd = process.cwd() } = {}) {
18
- this.protectedBranches = protectedBranches;
19
- this.logPath = memoryDir ? path.join(memoryDir, "GATE_ALERTS.log") : null;
20
- this.cwd = cwd;
21
- }
22
-
23
- run(command) {
24
- return execSync(command, {
25
- cwd: this.cwd,
26
- encoding: "utf8",
27
- stdio: ["ignore", "pipe", "pipe"]
28
- }).trim();
29
- }
30
-
31
- log(event) {
32
- if (!this.logPath) return;
33
- const timestamp = new Date().toISOString();
34
- const logEntry = `[${timestamp}] ${event}\n`;
35
- try {
36
- if (!fs.existsSync(path.dirname(this.logPath))) fs.mkdirSync(path.dirname(this.logPath), { recursive: true });
37
- fs.appendFileSync(this.logPath, logEntry);
38
- } catch (error) {
39
- console.error(`Failed to write to gate log: ${error.message}`);
40
- }
41
- }
42
-
43
- getDirtyState() {
44
- const lines = this.run("git status --short").split("\n").filter(Boolean);
45
- const tracked = lines.filter((line) => !line.startsWith("??"));
46
- const untracked = lines.filter((line) => line.startsWith("??"));
47
- return { clean: lines.length === 0, tracked, untracked, lines };
48
- }
49
-
50
- createScopedBranch(taskSlug) {
51
- const base = `feat/${slugify(taskSlug)}`;
52
- let candidate = base;
53
- let suffix = 2;
54
- while (true) {
55
- try {
56
- this.run(`git show-ref --verify --quiet refs/heads/${candidate}`);
57
- candidate = `${base}-${suffix++}`;
58
- } catch {
59
- break;
60
- }
61
- }
62
- this.run(`git switch -c ${candidate}`);
63
- return candidate;
64
- }
65
-
66
- getCurrentBranch() {
67
- try {
68
- return this.run("git branch --show-current") || this.run("git symbolic-ref --short HEAD") || "unknown";
69
- } catch {
70
- try {
71
- return this.run("git symbolic-ref --short HEAD") || "unknown";
72
- } catch {
73
- return "unknown";
74
- }
75
- }
76
- }
77
-
78
- /**
79
- * Strictly verifies branch safety without any override bypasses.
80
- */
81
- check(overrideIgnored = "", { taskSlug = "implementation", readOnly = false } = {}) {
82
- if (readOnly) {
83
- const currentBranch = this.getCurrentBranch();
84
- return { blocked: false, branch: currentBranch, recovered: false, readOnly: true };
85
- }
86
-
87
- try {
88
- // 1. Verify inside worktree (Git availability)
89
- try {
90
- this.run("git rev-parse --is-inside-work-tree");
91
- } catch (e) {
92
- const reason = "Git is unavailable or not inside a Git repository. Implementation work is blocked.";
93
- this.log(`BLOCKED: ${reason}`);
94
- return { blocked: true, branch: "unknown", reason };
95
- }
96
-
97
- // 2. Get current branch
98
- const currentBranch = this.getCurrentBranch();
99
- if (!currentBranch || currentBranch === "unknown" || currentBranch.trim() === "") {
100
- const reason = "Could not determine current Git branch name. Implementation work is blocked.";
101
- this.log(`BLOCKED: ${reason}`);
102
- return { blocked: true, branch: "unknown", reason };
103
- }
104
-
105
- const isProtected = this.protectedBranches.includes(currentBranch);
106
- if (!isProtected) {
107
- return { blocked: false, branch: currentBranch, recovered: false };
108
- }
109
-
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 };
116
- }
117
-
118
- // 4. Create scoped branch (Auto-recover)
119
- const recoveredBranch = this.createScopedBranch(taskSlug);
120
- this.log(`AUTO-RECOVERED '${currentBranch}' -> '${recoveredBranch}'`);
121
- return {
122
- blocked: false,
123
- branch: recoveredBranch,
124
- branchBefore: currentBranch,
125
- recovered: true,
126
- dirtyState: dirty
127
- };
128
- } catch (error) {
129
- const reason = `Git command failure on branch gate check: ${error.message}`;
130
- this.log(`BLOCKED: ${reason}`);
131
- return { blocked: true, branch: "unknown", error: error.message, reason, recovered: false };
132
- }
133
- }
134
- }
@@ -1,74 +0,0 @@
1
- import { execSync } from "node:child_process";
2
-
3
- /**
4
- * MergeGate verifies that commit history on integration branches respects
5
- * the non-fast-forward (--no-ff) merge strategy, preserving feature branch
6
- * topology instead of flattening/fast-forwarding.
7
- */
8
- export class MergeGate {
9
- constructor({ cwd = process.cwd() } = {}) {
10
- this.cwd = cwd;
11
- }
12
-
13
- runGit(command) {
14
- return execSync(command, {
15
- cwd: this.cwd,
16
- encoding: "utf8",
17
- stdio: ["ignore", "pipe", "pipe"]
18
- }).trim();
19
- }
20
-
21
- /**
22
- * Checks the commit history of the repository/branch.
23
- * @param {string} branchName The branch to check.
24
- * @returns {{ status: "PASS" | "FAIL_QUALITY_GATE" | "ERROR", reason?: string }}
25
- */
26
- check(branchName) {
27
- try {
28
- // Get the last 50 commits with parent hashes and subjects
29
- const logOutput = this.runGit('git log -n 50 --format="%H|%P|%s"');
30
- const lines = logOutput.split("\n").filter(Boolean);
31
-
32
- const commits = lines.map((line) => {
33
- const parts = line.split("|");
34
- const hash = parts[0] || "";
35
- const parentsStr = parts[1] || "";
36
- const parents = parentsStr.split(" ").filter(Boolean);
37
- const subject = parts[2] || "";
38
- return { hash, parents, subject };
39
- });
40
-
41
- // 1. Enforce that any commit containing merge-like patterns in its subject must have at least 2 parents
42
- for (const commit of commits) {
43
- const isMergeSubject = /merge\b/i.test(commit.subject) || /pull request/i.test(commit.subject);
44
- if (isMergeSubject && commit.parents.length < 2) {
45
- return {
46
- status: "FAIL_QUALITY_GATE",
47
- reason: `Linear or fast-forward merge detected for commit '${commit.hash.slice(0, 7)}' ("${commit.subject}"). Merges into integration branches must use the non-fast-forward (--no-ff) strategy to preserve topology.`
48
- };
49
- }
50
- }
51
-
52
- // 2. If the current branch is an integration branch, ensure it contains at least one merge commit (2+ parents)
53
- // to establish non-linear history, unless the repository has very few commits (e.g. initial setup).
54
- const isIntegrationBranch = ["main", "master"].includes(branchName) || String(branchName).startsWith("release");
55
- if (isIntegrationBranch && commits.length > 5) {
56
- const hasMergeCommit = commits.some((c) => c.parents.length >= 2);
57
- if (!hasMergeCommit) {
58
- return {
59
- status: "FAIL_QUALITY_GATE",
60
- reason: `Integration branch '${branchName}' has a purely linear commit history. Integrated branches must use non-fast-forward (--no-ff) merges to preserve feature branches.`
61
- };
62
- }
63
- }
64
-
65
- return { status: "PASS" };
66
- } catch (error) {
67
- // If we are not inside a git repo or git is not available, we fail-closed or report error
68
- return {
69
- status: "ERROR",
70
- reason: `Failed to check merge integrity: ${error.message}`
71
- };
72
- }
73
- }
74
- }
@@ -1,104 +0,0 @@
1
- import { execSync } from "node:child_process";
2
- import fs from "node:fs/promises";
3
- import path from "node:path";
4
-
5
- /**
6
- * HandoffEngine - Bundles project context into a single Markdown packet.
7
- */
8
- export class HandoffEngine {
9
- constructor({ cwd }) {
10
- this.cwd = cwd;
11
- }
12
-
13
- /**
14
- * Generates a handoff packet.
15
- * @param {Object} data - Context data.
16
- * @returns {Promise<string>} The generated packet content.
17
- */
18
- async generate({ taskId, author, status, specPaths = [], nextActions, evidence = null }) {
19
- const timestamp = new Date().toISOString();
20
-
21
- // 1. Collect Specs content
22
- let specsContent = "";
23
- for (const specPath of specPaths) {
24
- const absolutePath = path.isAbsolute(specPath) ? specPath : path.join(this.cwd, specPath);
25
- try {
26
- const content = await fs.readFile(absolutePath, "utf8");
27
- specsContent += `### File: ${specPath}\n\n${content}\n\n`;
28
- } catch (err) {
29
- specsContent += `### File: ${specPath}\n\n[Error reading file: ${err.message}]\n\n`;
30
- }
31
- }
32
-
33
- // 2. Capture Git Diff
34
- let gitDiff = "No changes detected.";
35
- try {
36
- gitDiff = execSync("git diff HEAD", { cwd: this.cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() || "No changes detected.";
37
- } catch (err) {
38
- gitDiff = `[Error capturing diff: ${err.message}]`;
39
- }
40
-
41
- // 3. Read EVIDENCE.json or use provided in-memory evidence
42
- let evidenceJson = "{}";
43
- let evidenceSummary = "No evidence found.";
44
- if (evidence) {
45
- const data = typeof evidence === "string" ? JSON.parse(evidence) : evidence;
46
- evidenceJson = JSON.stringify(data, null, 2);
47
- evidenceSummary = `Status: ${data.status || data.internalStatus || "unknown"}, Commands: ${data.commands?.length || 0}`;
48
- } else {
49
- const evidencePath = path.join(this.cwd, "EVIDENCE.json");
50
- try {
51
- const content = await fs.readFile(evidencePath, "utf8");
52
- const data = JSON.parse(content);
53
- evidenceJson = JSON.stringify(data, null, 2);
54
- evidenceSummary = `Status: ${data.status}, Commands: ${data.commands?.length || 0}`;
55
- } catch (err) {
56
- // Optional: don't fail if evidence is missing
57
- }
58
- }
59
-
60
- // 4. Populate Template
61
- const possibleTemplatePaths = [
62
- path.join(this.cwd, ".ai-workflow/templates/HANDOFF.template.md"),
63
- path.join(this.cwd, ".ai-workflow/harness/handoffs/HANDOFF.template.md"),
64
- path.join(import.meta.dirname, "../../../dist-assets/templates/HANDOFF.template.md"),
65
- path.join(import.meta.dirname, "../../../assets/templates/HANDOFF.template.md")
66
- ];
67
-
68
- let template = null;
69
- let lastError = null;
70
-
71
- for (const tPath of possibleTemplatePaths) {
72
- try {
73
- template = await fs.readFile(tPath, "utf8");
74
- break;
75
- } catch (err) {
76
- lastError = err;
77
- }
78
- }
79
-
80
- if (!template) {
81
- throw new Error(`CRITICAL FAILURE: Could not locate HANDOFF.template.md. Tried paths:\n${possibleTemplatePaths.join("\n")}\nLast error: ${lastError?.message}`);
82
- }
83
-
84
- const packet = template
85
- .replace("${TASK_ID}", taskId || "unknown")
86
- .replace("${TIMESTAMP}", timestamp)
87
- .replace("${AUTHOR}", author || "AI Workflow Kit")
88
- .replace("${STATUS}", status || "IN_PROGRESS")
89
- .replace("${SPECS_CONTENT}", specsContent || "No specs provided.")
90
- .replace("${EVIDENCE_SUMMARY}", evidenceSummary)
91
- .replace("${EVIDENCE_JSON}", evidenceJson)
92
- .replace("${GIT_DIFF}", gitDiff)
93
- .replace("${NEXT_ACTIONS}", nextActions || "Awaiting manager assignment.");
94
-
95
- const handoffDir = path.join(this.cwd, ".ai-workflow/handoffs");
96
- await fs.mkdir(handoffDir, { recursive: true });
97
-
98
- const packetName = `HANDOFF-${taskId || "TMP"}-${Date.now()}.md`;
99
- const packetPath = path.join(handoffDir, packetName);
100
- await fs.writeFile(packetPath, packet);
101
-
102
- return packetPath;
103
- }
104
- }
@@ -1,153 +0,0 @@
1
- import { spawnSync, execSync } from "node:child_process";
2
- import fs from "node:fs/promises";
3
- import path from "node:path";
4
-
5
- /**
6
- * Determines which npm scripts are available in the consumer project.
7
- * @param {string} cwd
8
- * @returns {Promise<Record<string, string>>}
9
- */
10
- async function readScripts(cwd) {
11
- try {
12
- const pkg = JSON.parse(await fs.readFile(path.join(cwd, "package.json"), "utf8"));
13
- return pkg.scripts || {};
14
- } catch {
15
- return {};
16
- }
17
- }
18
-
19
- /**
20
- * Returns the list of files changed since the last commit.
21
- * Used to detect whether remediation produced any observable change.
22
- * @param {string} cwd
23
- * @returns {string[]}
24
- */
25
- function getChangedFiles(cwd) {
26
- try {
27
- const output = execSync("git status --short", { cwd, encoding: "utf8" });
28
- return output
29
- .split("\n")
30
- .filter(Boolean)
31
- .map((line) => line.slice(3).trim())
32
- .filter(Boolean);
33
- } catch {
34
- return [];
35
- }
36
- }
37
-
38
- /**
39
- * Maps an affectedCheck source string to likely npm script names to re-run.
40
- * e.g. "check:branchSafety" → [] (no command can fix a branch issue)
41
- * "evidence" with failed test → ["test"]
42
- * @param {string[]} affectedChecks
43
- * @param {Record<string, string>} scripts
44
- * @returns {string[]} ordered list of script names to retry
45
- */
46
- function resolveRetryScripts(affectedChecks, scripts) {
47
- const retry = new Set();
48
-
49
- for (const check of affectedChecks) {
50
- // Evidence failures → re-run the failing validation scripts
51
- if (check === "evidence" || check === "result") {
52
- if (scripts.test) retry.add("test");
53
- if (scripts.lint) retry.add("lint");
54
- if (scripts.build) retry.add("build");
55
- if (scripts.typecheck) retry.add("typecheck");
56
- }
57
- // Specific check types
58
- if (check.includes("test")) retry.add("test");
59
- if (check.includes("lint")) retry.add("lint");
60
- if (check.includes("build")) retry.add("build");
61
- if (check.includes("typecheck")) retry.add("typecheck");
62
- }
63
-
64
- // Filter to only scripts that actually exist
65
- return [...retry].filter((name) => !!scripts[name]);
66
- }
67
-
68
- /**
69
- * CLI Remediation Executor — default implementation for `ai-workflow run`.
70
- *
71
- * Plugs into HealerEngine as the `remediate` function. On each attempt it:
72
- * 1. Reads the remediation-request.json to understand what failed
73
- * 2. Re-runs the affected npm scripts (lint, test, build, typecheck)
74
- * 3. Reports changed files and whether any script now passes
75
- *
76
- * Design constraints:
77
- * - This executor does NOT modify source code — it only re-runs validation commands.
78
- * - It is effective for transient failures (e.g. flaky tests, stale build artifacts).
79
- * - For structural failures (missing tests, broken implementation), it will exhaust
80
- * its attempt budget and return applied:false, triggering a BLOCKED status.
81
- * - The user is shown clear output on each attempt.
82
- *
83
- * @param {Object} ctx - Context provided by HealerEngine.run()
84
- * @param {number} ctx.attempt
85
- * @param {number} ctx.maxAttempts
86
- * @param {string} ctx.mode
87
- * @param {string} ctx.taskSlug
88
- * @param {Object} ctx.result - Current combined validation result
89
- * @param {string[]} ctx.affectedChecks
90
- * @param {string} ctx.requestPath - Path to remediation-request.json
91
- * @param {string} cwd - Consumer project working directory (bound via closure)
92
- * @returns {Promise<{applied: boolean, changedFiles?: string[], evidenceAdded?: string[], reason?: string}>}
93
- */
94
- export function createCliRemediationExecutor(cwd) {
95
- return async function remediationExecutor({ attempt, maxAttempts, mode, affectedChecks, requestPath }) {
96
- console.log(`\n[HEALER] Attempt ${attempt}/${maxAttempts} — mode: ${mode}`);
97
- console.log(`[HEALER] Affected: ${affectedChecks.join(", ")}`);
98
-
99
- const scripts = await readScripts(cwd);
100
- const filesBefore = getChangedFiles(cwd);
101
- const toRetry = resolveRetryScripts(affectedChecks, scripts);
102
-
103
- if (toRetry.length === 0) {
104
- console.log("[HEALER] No retryable scripts found for the affected checks.");
105
- console.log(`[HEALER] Review ${requestPath} for details.`);
106
- return {
107
- applied: false,
108
- reason: `No npm scripts available to address: ${affectedChecks.join(", ")}. Manual remediation required.`
109
- };
110
- }
111
-
112
- console.log(`[HEALER] Re-running: ${toRetry.map((s) => `npm run ${s}`).join(", ")}`);
113
-
114
- const results = [];
115
- let anyPassed = false;
116
-
117
- for (const scriptName of toRetry) {
118
- process.stdout.write(` → npm run ${scriptName} ... `);
119
- const isWin32 = process.platform === "win32";
120
- const cmd = isWin32 ? "npm.cmd" : "npm";
121
- const result = spawnSync(cmd, ["run", scriptName], {
122
- cwd,
123
- shell: false,
124
- encoding: "utf8",
125
- timeout: 120000
126
- });
127
- const passed = result.status === 0;
128
- if (passed) anyPassed = true;
129
- console.log(passed ? "PASS" : "FAIL");
130
- results.push({ script: scriptName, passed, exitCode: result.status });
131
- }
132
-
133
- const filesAfter = getChangedFiles(cwd);
134
- const changedFiles = filesAfter.filter((f) => !filesBefore.includes(f));
135
-
136
- if (!anyPassed && changedFiles.length === 0) {
137
- console.log("[HEALER] No scripts passed and no files changed — remediation had no effect.");
138
- return {
139
- applied: false,
140
- reason: `Re-ran ${toRetry.join(", ")} — all failed. Structural fix required by the implementing agent.`
141
- };
142
- }
143
-
144
- console.log(`[HEALER] Applied: ${results.filter((r) => r.passed).map((r) => r.script).join(", ") || "none passed"}`);
145
- if (changedFiles.length) console.log(`[HEALER] Changed files: ${changedFiles.join(", ")}`);
146
-
147
- return {
148
- applied: true,
149
- changedFiles,
150
- evidenceAdded: anyPassed ? ["validation-output"] : []
151
- };
152
- };
153
- }