@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.
- package/AGENTS.md +27 -0
- package/CHANGELOG.md +34 -0
- package/dist-assets/agents/astra.md +74 -45
- package/dist-assets/agents/atlas.md +110 -152
- package/dist-assets/agents/nexus.md +64 -19
- package/dist-assets/agents/orion.md +72 -27
- package/dist-assets/agents/phoenix.md +64 -19
- package/dist-assets/agents/sage.md +67 -36
- package/dist-assets/commands/atlas.md +66 -6
- package/dist-assets/commands/audit.md +62 -4
- package/dist-assets/commands/deploy.md +66 -5
- package/dist-assets/commands/discover.md +72 -4
- package/dist-assets/commands/implement.md +66 -18
- package/dist-assets/commands/optimize-tokens.md +60 -4
- package/dist-assets/commands/plan.md +64 -4
- package/dist-assets/commands/release.md +76 -5
- package/dist-assets/commands/run.md +62 -16
- package/dist-assets/commands/spec-create.md +66 -4
- package/dist-assets/commands/spec-implement.md +68 -4
- package/dist-assets/commands/spec-review.md +63 -4
- package/dist-assets/commands/update-memory.md +62 -4
- package/dist-assets/commands/validate.md +70 -6
- package/dist-assets/docs/cli-reference.md +38 -10
- package/dist-assets/skills/architecture/SKILL.md +62 -7
- package/dist-assets/skills/backend-development/SKILL.md +62 -7
- package/dist-assets/skills/deployment/SKILL.md +62 -7
- package/dist-assets/skills/design-principles/SKILL.md +59 -7
- package/dist-assets/skills/documentation/SKILL.md +61 -7
- package/dist-assets/skills/frontend-development/SKILL.md +62 -7
- package/dist-assets/skills/full-stack-development/SKILL.md +62 -7
- package/dist-assets/skills/optimize-tokens/SKILL.md +61 -7
- package/dist-assets/skills/pr-workflow/SKILL.md +65 -7
- package/dist-assets/skills/product-discovery/SKILL.md +81 -7
- package/dist-assets/skills/product-planning/SKILL.md +62 -7
- package/dist-assets/skills/project-memory/SKILL.md +55 -22
- package/dist-assets/skills/prompt-engineer/SKILL.md +59 -7
- package/dist-assets/skills/qa-workflow/SKILL.md +72 -7
- package/dist-assets/skills/refactoring/SKILL.md +68 -7
- package/dist-assets/skills/release-workflow/SKILL.md +72 -7
- package/dist-assets/skills/spec-driven-development/SKILL.md +75 -7
- package/dist-assets/skills/technical-leadership/SKILL.md +61 -7
- package/dist-assets/skills/ui-ux-design/SKILL.md +60 -7
- package/docs/compatibility/provider-usage.md +46 -0
- package/docs/compatibility/runtime-matrix.md +31 -0
- package/docs/getting-started/DESKTOP_PROMPT.md +52 -0
- package/docs/getting-started/quickstart.md +17 -0
- package/docs/getting-started/upgrading-to-v2.md +55 -0
- package/package.json +20 -4
- package/read_only_safety_verification.md +48 -0
- package/src/cli.js +4 -2
- package/src/commands/collect-evidence.js +3 -39
- package/src/commands/doctor.js +16 -0
- package/src/commands/execute.js +312 -119
- package/src/core/delegation-controller.js +193 -0
- package/src/core/evidence/evidence-ledger.js +53 -0
- package/src/core/execution-planner.js +4 -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/healing/healer-engine.js +34 -1
- package/src/core/healing/runtime-remediation-executor.js +136 -0
- package/src/core/request-classifier.js +15 -5
- package/src/core/request-router.js +289 -0
- package/src/core/runtime/opencode-adapter.js +170 -62
- package/src/core/validation/evidence-collector.js +26 -3
- 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
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { StackDetector } from "./stack-detector.js";
|
|
4
|
+
|
|
5
|
+
export class ValidationPlanner {
|
|
6
|
+
constructor({ cwd = process.cwd(), qualityGuard } = {}) {
|
|
7
|
+
this.cwd = cwd;
|
|
8
|
+
this.qualityGuard = qualityGuard;
|
|
9
|
+
this.stackDetector = new StackDetector({ cwd });
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async plan(profile = "generic") {
|
|
13
|
+
const stacks = await this.stackDetector.detect();
|
|
14
|
+
const tasks = [];
|
|
15
|
+
|
|
16
|
+
const changedFiles = this.qualityGuard.getChangedFiles().filter((file) => {
|
|
17
|
+
return !/(^|\/)(node_modules|vendor|dist|coverage|\.cache)(\/|$)/.test(String(file).replaceAll("\\", "/"));
|
|
18
|
+
});
|
|
19
|
+
const executableBehavior = await this.qualityGuard.hasExecutableBehaviorChanges();
|
|
20
|
+
|
|
21
|
+
// Node Stack Planning
|
|
22
|
+
if (stacks.includes("node")) {
|
|
23
|
+
try {
|
|
24
|
+
const pkg = JSON.parse(await fs.readFile(path.join(this.cwd, "package.json"), "utf8"));
|
|
25
|
+
const scripts = pkg.scripts || {};
|
|
26
|
+
|
|
27
|
+
this.addNodeScriptTask(tasks, scripts, "lint", ["npm", "run", "lint"]);
|
|
28
|
+
this.addNodeScriptTask(tasks, scripts, "test", ["npm", "test"], { rejectNoOp: executableBehavior });
|
|
29
|
+
this.addNodeScriptTask(tasks, scripts, "build", ["npm", "run", "build"]);
|
|
30
|
+
this.addNodeScriptTask(tasks, scripts, "typecheck", ["npm", "run", "typecheck"]);
|
|
31
|
+
|
|
32
|
+
if (scripts.validate && !String(scripts.validate).includes("ai-workflow collect-evidence")) {
|
|
33
|
+
this.addNodeScriptTask(tasks, scripts, "validate", ["npm", "run", "validate"]);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const hasTs = changedFiles.some((file) => /\.(ts|tsx)$/.test(file)) ||
|
|
37
|
+
await fs.access(path.join(this.cwd, "tsconfig.json")).then(() => true).catch(() => false);
|
|
38
|
+
if (hasTs && !scripts.typecheck) {
|
|
39
|
+
tasks.push({
|
|
40
|
+
name: "typecheck",
|
|
41
|
+
command: ["npm", "run", "typecheck"],
|
|
42
|
+
kind: "typecheck",
|
|
43
|
+
presetStatus: "FAIL_QUALITY_GATE",
|
|
44
|
+
summary: "TypeScript detected but no typecheck script is configured."
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
} catch {
|
|
48
|
+
// Fallback or ignore if pkg read fails
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Python Stack Planning
|
|
53
|
+
if (stacks.includes("python")) {
|
|
54
|
+
const pythonFiles = changedFiles.filter(f => f.endsWith(".py"));
|
|
55
|
+
|
|
56
|
+
// Compiler syntax check for changed python files
|
|
57
|
+
if (pythonFiles.length > 0) {
|
|
58
|
+
tasks.push({
|
|
59
|
+
name: "python-syntax",
|
|
60
|
+
command: ["python", "-m", "py_compile", ...pythonFiles],
|
|
61
|
+
kind: "lint"
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Check if testing is needed
|
|
66
|
+
if (executableBehavior) {
|
|
67
|
+
// Check if pytest or unittest is configured
|
|
68
|
+
const hasPytestConfig = await fs.access(path.join(this.cwd, "pytest.ini")).then(() => true).catch(() => false) ||
|
|
69
|
+
await fs.access(path.join(this.cwd, "pyproject.toml")).then(() => true).catch(() => false);
|
|
70
|
+
|
|
71
|
+
if (hasPytestConfig || pythonFiles.length > 0) {
|
|
72
|
+
tasks.push({
|
|
73
|
+
name: "python-test",
|
|
74
|
+
command: ["pytest"],
|
|
75
|
+
kind: "test"
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// PHP Stack Planning
|
|
82
|
+
if (stacks.includes("php")) {
|
|
83
|
+
const phpFiles = changedFiles.filter(f => f.endsWith(".php") || f.endsWith(".inc"));
|
|
84
|
+
|
|
85
|
+
// Compiler syntax check for each changed PHP file - split into separate tasks
|
|
86
|
+
if (phpFiles.length > 0) {
|
|
87
|
+
for (const file of phpFiles) {
|
|
88
|
+
tasks.push({
|
|
89
|
+
name: `php-syntax:${file}`,
|
|
90
|
+
command: ["php", "-l", file],
|
|
91
|
+
kind: "lint"
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Check if testing is needed
|
|
97
|
+
if (executableBehavior) {
|
|
98
|
+
const hasPhpUnitXml = await fs.access(path.join(this.cwd, "phpunit.xml")).then(() => true).catch(() => false) ||
|
|
99
|
+
await fs.access(path.join(this.cwd, "phpunit.xml.dist")).then(() => true).catch(() => false);
|
|
100
|
+
if (hasPhpUnitXml || phpFiles.length > 0) {
|
|
101
|
+
tasks.push({
|
|
102
|
+
name: "php-test",
|
|
103
|
+
command: ["vendor/bin/phpunit"],
|
|
104
|
+
kind: "test"
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return tasks;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
addNodeScriptTask(tasks, scripts, name, command, { rejectNoOp = false } = {}) {
|
|
114
|
+
const script = scripts[name];
|
|
115
|
+
if (!script) return;
|
|
116
|
+
if (!rejectNoOp && name === "test" && this.isNoOpScript(script)) return;
|
|
117
|
+
if (rejectNoOp && this.isNoOpScript(script)) {
|
|
118
|
+
tasks.push({
|
|
119
|
+
name,
|
|
120
|
+
command,
|
|
121
|
+
kind: name === "test" ? "test" : name,
|
|
122
|
+
presetStatus: "FAIL_QUALITY_GATE",
|
|
123
|
+
summary: `Configured ${name} script is a no-op: ${script}`
|
|
124
|
+
});
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
tasks.push({ name, command, kind: name === "test" ? "test" : name });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
isNoOpScript(script = "") {
|
|
131
|
+
const value = String(script).trim().toLowerCase();
|
|
132
|
+
return /^(echo\b|true$|exit\s+0$|node\s+-e\s+["']?console\.log)/.test(value) || value.includes("tests-pass");
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import crypto from "node:crypto";
|
|
5
|
+
|
|
6
|
+
async function copyDirectory(src, dest) {
|
|
7
|
+
const entries = await fs.readdir(src, { withFileTypes: true });
|
|
8
|
+
for (const entry of entries) {
|
|
9
|
+
if (entry.name === ".git" || entry.name === "node_modules" || entry.name === "tmp") {
|
|
10
|
+
continue;
|
|
11
|
+
}
|
|
12
|
+
const srcPath = path.join(src, entry.name);
|
|
13
|
+
const destPath = path.join(dest, entry.name);
|
|
14
|
+
|
|
15
|
+
if (entry.isDirectory()) {
|
|
16
|
+
await fs.mkdir(destPath, { recursive: true });
|
|
17
|
+
await copyDirectory(srcPath, destPath);
|
|
18
|
+
} else if (entry.isFile()) {
|
|
19
|
+
await fs.copyFile(srcPath, destPath);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function captureHashSnapshot(dir) {
|
|
25
|
+
const snapshot = {};
|
|
26
|
+
await scanDir(dir, dir, snapshot);
|
|
27
|
+
return snapshot;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function scanDir(baseDir, currentDir, snapshot) {
|
|
31
|
+
const entries = await fs.readdir(currentDir, { withFileTypes: true });
|
|
32
|
+
for (const entry of entries) {
|
|
33
|
+
if (entry.name === "opencode.jsonc" || entry.name === "node_modules" || entry.name === ".git" || entry.name === "EVIDENCE.json" || entry.name === "tmp") {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
37
|
+
const relPath = path.relative(baseDir, fullPath);
|
|
38
|
+
|
|
39
|
+
if (entry.isDirectory()) {
|
|
40
|
+
await scanDir(baseDir, fullPath, snapshot);
|
|
41
|
+
} else if (entry.isFile()) {
|
|
42
|
+
try {
|
|
43
|
+
const content = await fs.readFile(fullPath);
|
|
44
|
+
const hash = crypto.createHash("sha256").update(content).digest("hex");
|
|
45
|
+
snapshot[relPath] = hash;
|
|
46
|
+
} catch {
|
|
47
|
+
// ignore
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Runs a function inside a temporary copy of the workspace.
|
|
55
|
+
* Mutating the workspace inside this temp directory will trigger a BLOCKED result.
|
|
56
|
+
*/
|
|
57
|
+
export async function runInReadOnlyWorkspace(originalCwd, runFn) {
|
|
58
|
+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-ro-"));
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
// 1. Copy repository files recursively (excluding git/node_modules/tmp)
|
|
62
|
+
await copyDirectory(originalCwd, tempDir);
|
|
63
|
+
|
|
64
|
+
// 2. Write the required opencode.jsonc permission config file
|
|
65
|
+
const opencodeConfig = {
|
|
66
|
+
permission: {
|
|
67
|
+
edit: "deny",
|
|
68
|
+
external_directory: "deny",
|
|
69
|
+
bash: {
|
|
70
|
+
"*": "deny",
|
|
71
|
+
"git status*": "allow",
|
|
72
|
+
"git diff*": "allow",
|
|
73
|
+
"git log*": "allow",
|
|
74
|
+
"grep *": "allow",
|
|
75
|
+
"find *": "allow"
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
await fs.writeFile(
|
|
80
|
+
path.join(tempDir, "opencode.jsonc"),
|
|
81
|
+
JSON.stringify(opencodeConfig, null, 2)
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
// 3. Capture initial file hash snapshot
|
|
85
|
+
const snapshotBefore = await captureHashSnapshot(tempDir);
|
|
86
|
+
|
|
87
|
+
// 4. Execute the callback function inside the temp directory
|
|
88
|
+
const runResult = await runFn(tempDir);
|
|
89
|
+
|
|
90
|
+
// 5. Capture post-run snapshot and verify no file mutations occurred
|
|
91
|
+
const snapshotAfter = await captureHashSnapshot(tempDir);
|
|
92
|
+
|
|
93
|
+
const mutated = [];
|
|
94
|
+
for (const [file, hash] of Object.entries(snapshotAfter)) {
|
|
95
|
+
if (snapshotBefore[file] !== hash) {
|
|
96
|
+
mutated.push(file);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
for (const file of Object.keys(snapshotBefore)) {
|
|
100
|
+
if (snapshotAfter[file] === undefined) {
|
|
101
|
+
mutated.push(file);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (mutated.length > 0) {
|
|
106
|
+
return {
|
|
107
|
+
success: false,
|
|
108
|
+
error: `Read-only confinement violation: mutating changes detected inside temporary workspace: ${mutated.join(", ")}`,
|
|
109
|
+
commandsRun: runResult.commandsRun || [],
|
|
110
|
+
eventCount: runResult.eventCount || 0
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return runResult;
|
|
115
|
+
} finally {
|
|
116
|
+
// 6. Delete the temporary workspace copy
|
|
117
|
+
await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {});
|
|
118
|
+
}
|
|
119
|
+
}
|