@williambeto/ai-workflow 2.7.1 → 2.8.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 +28 -0
- package/README.md +3 -2
- package/bin/ai-workflow.js +1015 -257
- package/bin/ai-workflow.js.map +1 -1
- package/chunk-AINIR25D.js +120 -0
- package/chunk-AY33SA5W.js +46 -0
- package/{evidence-validator-76ZQQYDU.js → chunk-FNT7DN3N.js} +22 -2
- package/{chunk-W4RTQWVQ.js → chunk-LI76KI7C.js} +977 -820
- package/{chunk-BDZPUAEX.js → chunk-Y4RLP6ZM.js} +11 -3
- package/chunk-YOBY5C72.js +76 -0
- package/core/index.d.ts +32 -1
- package/core/index.js +4 -2
- package/dist-assets/docs/cli-reference.md +47 -2
- package/dist-assets/schemas/approval-receipt.schema.json +35 -0
- package/dist-assets/schemas/evidence.schema.json +33 -0
- package/evidence-validator-HS3NTWAB.js +8 -0
- package/package.json +17 -2
- package/skill-4MEGJ3DO.js +211 -0
- package/skill-frontmatter-linter-FMJADOK4.js +14 -0
- package/{validate-A46WUBVZ.js → validate-IEHLAVZ6.js} +2 -2
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// src/core/gates/branch-gate.ts
|
|
2
|
+
import { execSync } from "child_process";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import path from "path";
|
|
5
|
+
function slugify(value = "task") {
|
|
6
|
+
return String(value).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48) || "task";
|
|
7
|
+
}
|
|
8
|
+
var BranchGate = class {
|
|
9
|
+
protectedBranches;
|
|
10
|
+
logPath;
|
|
11
|
+
cwd;
|
|
12
|
+
constructor({ protectedBranches = ["main", "master"], memoryDir, cwd = process.cwd() } = {}) {
|
|
13
|
+
this.protectedBranches = protectedBranches;
|
|
14
|
+
this.logPath = memoryDir ? path.join(memoryDir, "GATE_ALERTS.log") : null;
|
|
15
|
+
this.cwd = cwd;
|
|
16
|
+
}
|
|
17
|
+
run(command) {
|
|
18
|
+
return execSync(command, {
|
|
19
|
+
cwd: this.cwd,
|
|
20
|
+
encoding: "utf8",
|
|
21
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
22
|
+
}).trim();
|
|
23
|
+
}
|
|
24
|
+
log(event) {
|
|
25
|
+
if (!this.logPath) return;
|
|
26
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
27
|
+
const logEntry = `[${timestamp}] ${event}
|
|
28
|
+
`;
|
|
29
|
+
try {
|
|
30
|
+
if (!fs.existsSync(path.dirname(this.logPath))) fs.mkdirSync(path.dirname(this.logPath), { recursive: true });
|
|
31
|
+
fs.appendFileSync(this.logPath, logEntry);
|
|
32
|
+
} catch (error) {
|
|
33
|
+
console.error(`Failed to write to gate log: ${error.message}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
getDirtyState() {
|
|
37
|
+
const lines = this.run("git status --short").split("\n").filter(Boolean);
|
|
38
|
+
const tracked = lines.filter((line) => !line.startsWith("??"));
|
|
39
|
+
const untracked = lines.filter((line) => line.startsWith("??"));
|
|
40
|
+
return { clean: lines.length === 0, tracked, untracked, lines };
|
|
41
|
+
}
|
|
42
|
+
createScopedBranch(taskSlug) {
|
|
43
|
+
const base = `feat/${slugify(taskSlug)}`;
|
|
44
|
+
let candidate = base;
|
|
45
|
+
let suffix = 2;
|
|
46
|
+
while (true) {
|
|
47
|
+
try {
|
|
48
|
+
this.run(`git show-ref --verify --quiet refs/heads/${candidate}`);
|
|
49
|
+
candidate = `${base}-${suffix++}`;
|
|
50
|
+
} catch {
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
this.run(`git switch -c ${candidate}`);
|
|
55
|
+
return candidate;
|
|
56
|
+
}
|
|
57
|
+
getCurrentBranch() {
|
|
58
|
+
try {
|
|
59
|
+
return this.run("git branch --show-current") || this.run("git symbolic-ref --short HEAD") || "unknown";
|
|
60
|
+
} catch {
|
|
61
|
+
try {
|
|
62
|
+
return this.run("git symbolic-ref --short HEAD") || "unknown";
|
|
63
|
+
} catch {
|
|
64
|
+
return "unknown";
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Strictly verifies branch safety without any override bypasses.
|
|
70
|
+
*/
|
|
71
|
+
check(overrideIgnored = "", { taskSlug = "implementation", readOnly = false } = {}) {
|
|
72
|
+
if (readOnly) {
|
|
73
|
+
const currentBranch = this.getCurrentBranch();
|
|
74
|
+
return { blocked: false, branch: currentBranch, recovered: false, readOnly: true };
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
try {
|
|
78
|
+
this.run("git rev-parse --is-inside-work-tree");
|
|
79
|
+
} catch (e) {
|
|
80
|
+
const reason = "Git is unavailable or not inside a Git repository. Implementation work is blocked.";
|
|
81
|
+
this.log(`BLOCKED: ${reason}`);
|
|
82
|
+
return { blocked: true, branch: "unknown", reason };
|
|
83
|
+
}
|
|
84
|
+
const currentBranch = this.getCurrentBranch();
|
|
85
|
+
if (!currentBranch || currentBranch === "unknown" || currentBranch.trim() === "") {
|
|
86
|
+
const reason = "Could not determine current Git branch name. Implementation work is blocked.";
|
|
87
|
+
this.log(`BLOCKED: ${reason}`);
|
|
88
|
+
return { blocked: true, branch: "unknown", reason };
|
|
89
|
+
}
|
|
90
|
+
const isProtected = this.protectedBranches.includes(currentBranch);
|
|
91
|
+
if (!isProtected) {
|
|
92
|
+
return { blocked: false, branch: currentBranch, recovered: false };
|
|
93
|
+
}
|
|
94
|
+
const dirty = this.getDirtyState();
|
|
95
|
+
if (!dirty.clean) {
|
|
96
|
+
const reason = `Direct writes to protected branch '${currentBranch}' are prohibited and the branch is dirty (has changes).`;
|
|
97
|
+
this.log(`BLOCKED: ${reason}`);
|
|
98
|
+
return { blocked: true, branch: currentBranch, reason, dirtyState: dirty };
|
|
99
|
+
}
|
|
100
|
+
const recoveredBranch = this.createScopedBranch(taskSlug);
|
|
101
|
+
this.log(`AUTO-RECOVERED '${currentBranch}' -> '${recoveredBranch}'`);
|
|
102
|
+
return {
|
|
103
|
+
blocked: false,
|
|
104
|
+
branch: recoveredBranch,
|
|
105
|
+
branchBefore: currentBranch,
|
|
106
|
+
recovered: true,
|
|
107
|
+
dirtyState: dirty
|
|
108
|
+
};
|
|
109
|
+
} catch (error) {
|
|
110
|
+
const reason = `Git command failure on branch gate check: ${error.message}`;
|
|
111
|
+
this.log(`BLOCKED: ${reason}`);
|
|
112
|
+
return { blocked: true, branch: "unknown", error: error.message, reason, recovered: false };
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export {
|
|
118
|
+
BranchGate
|
|
119
|
+
};
|
|
120
|
+
//# sourceMappingURL=chunk-AINIR25D.js.map
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// src/core/backup.ts
|
|
2
|
+
import fs from "fs/promises";
|
|
3
|
+
import path from "path";
|
|
4
|
+
function escapeRelativePath(relativePath) {
|
|
5
|
+
return relativePath.replace(/[\\/]/g, "__");
|
|
6
|
+
}
|
|
7
|
+
async function createManagedBackup(filePath, { cwd, backupRoot, maxPerFile = 20 }) {
|
|
8
|
+
const relativePath = path.relative(cwd, filePath);
|
|
9
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
10
|
+
const backupName = `${escapeRelativePath(relativePath)}.${stamp}.bak`;
|
|
11
|
+
const absoluteBackupRoot = path.join(cwd, backupRoot);
|
|
12
|
+
const backupPath = path.join(absoluteBackupRoot, backupName);
|
|
13
|
+
await fs.mkdir(absoluteBackupRoot, { recursive: true });
|
|
14
|
+
await fs.copyFile(filePath, backupPath);
|
|
15
|
+
const prefix = `${escapeRelativePath(relativePath)}.`;
|
|
16
|
+
const entries = await fs.readdir(absoluteBackupRoot);
|
|
17
|
+
const matching = entries.filter((entry) => entry.startsWith(prefix) && entry.endsWith(".bak")).sort();
|
|
18
|
+
if (matching.length > maxPerFile) {
|
|
19
|
+
const excess = matching.slice(0, matching.length - maxPerFile);
|
|
20
|
+
await Promise.all(excess.map((entry) => fs.unlink(path.join(absoluteBackupRoot, entry))));
|
|
21
|
+
}
|
|
22
|
+
return backupPath;
|
|
23
|
+
}
|
|
24
|
+
async function createManagedPathBackup(targetPath, { cwd, backupRoot, maxPerFile = 20 }) {
|
|
25
|
+
const relativePath = path.relative(cwd, targetPath);
|
|
26
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
27
|
+
const backupName = `${escapeRelativePath(relativePath)}.${stamp}.bak`;
|
|
28
|
+
const absoluteBackupRoot = path.join(cwd, backupRoot);
|
|
29
|
+
const backupPath = path.join(absoluteBackupRoot, backupName);
|
|
30
|
+
await fs.mkdir(absoluteBackupRoot, { recursive: true });
|
|
31
|
+
await fs.rename(targetPath, backupPath);
|
|
32
|
+
const prefix = `${escapeRelativePath(relativePath)}.`;
|
|
33
|
+
const entries = await fs.readdir(absoluteBackupRoot);
|
|
34
|
+
const matching = entries.filter((entry) => entry.startsWith(prefix) && entry.endsWith(".bak")).sort();
|
|
35
|
+
if (matching.length > maxPerFile) {
|
|
36
|
+
const excess = matching.slice(0, matching.length - maxPerFile);
|
|
37
|
+
await Promise.all(excess.map((entry) => fs.rm(path.join(absoluteBackupRoot, entry), { recursive: true, force: true })));
|
|
38
|
+
}
|
|
39
|
+
return backupPath;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export {
|
|
43
|
+
createManagedBackup,
|
|
44
|
+
createManagedPathBackup
|
|
45
|
+
};
|
|
46
|
+
//# sourceMappingURL=chunk-AY33SA5W.js.map
|
|
@@ -6552,10 +6552,12 @@ var EvidenceValidator = class {
|
|
|
6552
6552
|
cwd;
|
|
6553
6553
|
skipFileCheck;
|
|
6554
6554
|
skipGitCheck;
|
|
6555
|
-
|
|
6555
|
+
requireWorkflowEvidence;
|
|
6556
|
+
constructor({ cwd = process.cwd(), skipFileCheck = false, skipGitCheck = false, requireWorkflowEvidence = false } = {}) {
|
|
6556
6557
|
this.cwd = cwd;
|
|
6557
6558
|
this.skipFileCheck = skipFileCheck;
|
|
6558
6559
|
this.skipGitCheck = skipGitCheck;
|
|
6560
|
+
this.requireWorkflowEvidence = requireWorkflowEvidence;
|
|
6559
6561
|
}
|
|
6560
6562
|
async validate() {
|
|
6561
6563
|
const evidencePath = path.join(this.cwd, "EVIDENCE.json");
|
|
@@ -6595,6 +6597,21 @@ var EvidenceValidator = class {
|
|
|
6595
6597
|
} catch (e) {
|
|
6596
6598
|
return { valid: false, reason: `Evidence schema validation failed: ${e.message}` };
|
|
6597
6599
|
}
|
|
6600
|
+
if (this.requireWorkflowEvidence) {
|
|
6601
|
+
const phases = evidence.phaseOrder || [];
|
|
6602
|
+
const actors = evidence.confirmedActors || [];
|
|
6603
|
+
const hashes = evidence.artifactHashes || {};
|
|
6604
|
+
if (!evidence.workflowId || !evidence.baseSha || !hashes.approved || !hashes.technicalPlan) {
|
|
6605
|
+
return { valid: false, reason: "Required executable SDD evidence fields are missing." };
|
|
6606
|
+
}
|
|
6607
|
+
if (phases.join(",") !== "specification,planning,implementation" && phases.join(",") !== "planning,implementation") {
|
|
6608
|
+
return { valid: false, reason: "Executable SDD phase order is invalid." };
|
|
6609
|
+
}
|
|
6610
|
+
const expectedActors = phases.map((phase) => phase === "specification" ? "Nexus" : phase === "planning" ? "Orion" : "Astra");
|
|
6611
|
+
if (actors.length !== expectedActors.length || actors.some((actor, index) => actor.requested !== expectedActors[index] || actor.applied !== expectedActors[index])) {
|
|
6612
|
+
return { valid: false, reason: "Executable SDD confirmed actor evidence is incomplete or out of order." };
|
|
6613
|
+
}
|
|
6614
|
+
}
|
|
6598
6615
|
const changedFiles = evidence.changedFiles || [];
|
|
6599
6616
|
const resolvedCwd = path.resolve(this.cwd);
|
|
6600
6617
|
for (const file of changedFiles) {
|
|
@@ -6655,7 +6672,10 @@ var EvidenceValidator = class {
|
|
|
6655
6672
|
return { valid: true };
|
|
6656
6673
|
}
|
|
6657
6674
|
};
|
|
6675
|
+
|
|
6658
6676
|
export {
|
|
6677
|
+
require_codegen,
|
|
6678
|
+
require_ajv,
|
|
6659
6679
|
EvidenceValidator
|
|
6660
6680
|
};
|
|
6661
|
-
//# sourceMappingURL=
|
|
6681
|
+
//# sourceMappingURL=chunk-FNT7DN3N.js.map
|