@bx-h/meta-flow 0.1.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 +7 -0
- package/LICENSE +21 -0
- package/README.md +172 -0
- package/bin/meta-flow.js +7 -0
- package/examples/sample-task/adjudication-report.json +14 -0
- package/examples/sample-task/final-report.md +26 -0
- package/examples/sample-task/goal-contract.json +44 -0
- package/examples/sample-task/milestone-plan.json +31 -0
- package/examples/sample-task/milestones/M1/direction-evaluation.json +16 -0
- package/examples/sample-task/milestones/M1/task-list.json +41 -0
- package/examples/sample-task/milestones/M1/tasks/T1/execution-report.json +25 -0
- package/examples/sample-task/milestones/M1/tasks/T1/task-spec.json +37 -0
- package/examples/sample-task/milestones/M1/tasks/T1/verification-report.json +23 -0
- package/examples/sample-task/proposal-summary.md +21 -0
- package/examples/sample-task/proposal.md +29 -0
- package/examples/sample-task/questioning-report.json +31 -0
- package/examples/sample-task/raw-request.md +1 -0
- package/examples/sample-task/review-aggregate.json +34 -0
- package/examples/sample-task/reviews/product-reviewer.json +16 -0
- package/examples/sample-task/reviews/risk-reviewer.json +16 -0
- package/examples/sample-task/reviews/technical-reviewer.json +16 -0
- package/examples/sample-task/reviews/verification-reviewer.json +15 -0
- package/examples/sample-task/state.json +35 -0
- package/marketplace/marketplace.json +20 -0
- package/package.json +50 -0
- package/plugin/.codex-plugin/plugin.json +36 -0
- package/plugin/agent-templates/adjudicator.toml +26 -0
- package/plugin/agent-templates/direction-evaluator.toml +18 -0
- package/plugin/agent-templates/executor.toml +21 -0
- package/plugin/agent-templates/final-summarizer.toml +17 -0
- package/plugin/agent-templates/planner.toml +18 -0
- package/plugin/agent-templates/product-reviewer.toml +15 -0
- package/plugin/agent-templates/proposal-summarizer.toml +17 -0
- package/plugin/agent-templates/questioner.toml +22 -0
- package/plugin/agent-templates/researcher-proposer.toml +18 -0
- package/plugin/agent-templates/result-verifier.toml +20 -0
- package/plugin/agent-templates/risk-reviewer.toml +14 -0
- package/plugin/agent-templates/task-decomposer.toml +19 -0
- package/plugin/agent-templates/technical-reviewer.toml +14 -0
- package/plugin/agent-templates/verification-reviewer.toml +15 -0
- package/plugin/assets/icon.svg +10 -0
- package/plugin/assets/screenshot-placeholder.md +3 -0
- package/plugin/scripts/_common.py +95 -0
- package/plugin/scripts/aggregate_reviews.py +99 -0
- package/plugin/scripts/new_task.py +78 -0
- package/plugin/scripts/status.py +95 -0
- package/plugin/scripts/validate_adjudication.py +47 -0
- package/plugin/scripts/validate_goal_contract.py +56 -0
- package/plugin/scripts/validate_milestone_plan.py +49 -0
- package/plugin/scripts/validate_task_list.py +59 -0
- package/plugin/scripts/validate_task_verification.py +49 -0
- package/plugin/skills/meta-flow/SKILL.md +159 -0
- package/plugin/skills/meta-flow/references/adjudication-policy.md +30 -0
- package/plugin/skills/meta-flow/references/direction-evaluation-policy.md +24 -0
- package/plugin/skills/meta-flow/references/execution-policy.md +35 -0
- package/plugin/skills/meta-flow/references/review-rubric.md +28 -0
- package/plugin/skills/meta-flow/references/role-contracts.md +29 -0
- package/plugin/templates/adjudication-report.json +11 -0
- package/plugin/templates/direction-evaluation.json +16 -0
- package/plugin/templates/final-report.md +11 -0
- package/plugin/templates/goal-contract.json +20 -0
- package/plugin/templates/milestone-plan.json +15 -0
- package/plugin/templates/proposal-summary.md +11 -0
- package/plugin/templates/proposal.md +17 -0
- package/plugin/templates/questioning-report.json +15 -0
- package/plugin/templates/raw-request.md +3 -0
- package/plugin/templates/review-aggregate.json +14 -0
- package/plugin/templates/reviewer-report.json +10 -0
- package/plugin/templates/state.json +23 -0
- package/plugin/templates/task-execution-report.json +13 -0
- package/plugin/templates/task-list.json +20 -0
- package/plugin/templates/task-spec.json +16 -0
- package/plugin/templates/task-verification-report.json +13 -0
- package/src/cli/commands/doctor.js +171 -0
- package/src/cli/commands/install.js +120 -0
- package/src/cli/commands/print_paths.js +20 -0
- package/src/cli/commands/uninstall.js +56 -0
- package/src/cli/commands/verify.js +197 -0
- package/src/cli/index.js +53 -0
- package/src/cli/lib/agents.js +89 -0
- package/src/cli/lib/args.js +49 -0
- package/src/cli/lib/codex_config.js +106 -0
- package/src/cli/lib/fs_safe.js +142 -0
- package/src/cli/lib/logger.js +23 -0
- package/src/cli/lib/marketplace.js +72 -0
- package/src/cli/lib/paths.js +37 -0
- package/src/cli/lib/plugin.js +57 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { parseOptions, helpRequested } from "../lib/args.js";
|
|
2
|
+
import { uninstallAgents } from "../lib/agents.js";
|
|
3
|
+
import { createLogger } from "../lib/logger.js";
|
|
4
|
+
import { uninstallMarketplace } from "../lib/marketplace.js";
|
|
5
|
+
import { uninstallPlugin } from "../lib/plugin.js";
|
|
6
|
+
import { resolveTargets } from "../lib/paths.js";
|
|
7
|
+
|
|
8
|
+
export function uninstallHelp() {
|
|
9
|
+
return `Usage: meta-flow uninstall --scope repo|user [options]
|
|
10
|
+
|
|
11
|
+
Options:
|
|
12
|
+
--target <path> Target repo path for repo scope. Defaults to cwd.
|
|
13
|
+
--dry-run Print planned removals without deleting files.
|
|
14
|
+
--yes Confirm removal.
|
|
15
|
+
--keep-tasks Keep .meta-flow/tasks. This is the default.
|
|
16
|
+
--verbose Print detailed actions.`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function runUninstall(argv = []) {
|
|
20
|
+
if (helpRequested(argv)) {
|
|
21
|
+
console.log(uninstallHelp());
|
|
22
|
+
return 0;
|
|
23
|
+
}
|
|
24
|
+
const { options } = parseOptions(argv, {
|
|
25
|
+
scope: "string",
|
|
26
|
+
target: "string"
|
|
27
|
+
});
|
|
28
|
+
const scope = options.scope || "repo";
|
|
29
|
+
const targets = resolveTargets({ scope, target: options.target });
|
|
30
|
+
const dryRun = Boolean(options.dryRun);
|
|
31
|
+
const logger = createLogger({ verbose: options.verbose });
|
|
32
|
+
|
|
33
|
+
console.log("Meta Flow uninstall plan:");
|
|
34
|
+
console.log(`- scope: ${targets.scope}`);
|
|
35
|
+
console.log(`- target: ${targets.target}`);
|
|
36
|
+
console.log(`- remove plugin: ${targets.pluginTarget}`);
|
|
37
|
+
console.log(`- update marketplace: ${targets.marketplaceTarget}`);
|
|
38
|
+
console.log(`- remove marked agents: ${targets.agentsTarget}`);
|
|
39
|
+
console.log(`- keep tasks: ${targets.tasksTarget}`);
|
|
40
|
+
|
|
41
|
+
if (!dryRun && !options.yes) {
|
|
42
|
+
throw new Error("Refusing to delete files without --yes. Re-run with --dry-run first or add --yes.");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const pluginResult = await uninstallPlugin(targets, { dryRun, logger });
|
|
46
|
+
if (pluginResult.skipped) {
|
|
47
|
+
logger.warn(`plugin target is not a confirmed meta-flow plugin; skipped: ${targets.pluginTarget}`);
|
|
48
|
+
}
|
|
49
|
+
await uninstallMarketplace(targets, { dryRun, logger });
|
|
50
|
+
const agentResult = await uninstallAgents(targets, { dryRun, logger });
|
|
51
|
+
for (const skipped of agentResult.skipped) {
|
|
52
|
+
logger.warn(`agent file has no meta-flow marker; skipped: ${skipped}`);
|
|
53
|
+
}
|
|
54
|
+
console.log("Uninstall complete. User task data was not deleted.");
|
|
55
|
+
return 0;
|
|
56
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { installAgents, validateAgentTemplates } from "../lib/agents.js";
|
|
7
|
+
import { runDoctor } from "./doctor.js";
|
|
8
|
+
import { runInstall } from "./install.js";
|
|
9
|
+
import { runUninstall } from "./uninstall.js";
|
|
10
|
+
import { validatePlugin } from "../lib/plugin.js";
|
|
11
|
+
import { packageRoot, pluginSource, sampleTaskRoot } from "../lib/paths.js";
|
|
12
|
+
|
|
13
|
+
export async function runVerify(argv = []) {
|
|
14
|
+
const lintOnly = argv.includes("--lint-only");
|
|
15
|
+
const checks = [];
|
|
16
|
+
checks.push(await checkPackageJson());
|
|
17
|
+
checks.push(await checkNoForbiddenSourcePatterns());
|
|
18
|
+
if (lintOnly) {
|
|
19
|
+
report(checks);
|
|
20
|
+
return checks.some((check) => check.level === "FAIL") ? 1 : 0;
|
|
21
|
+
}
|
|
22
|
+
checks.push(await checkPlugin());
|
|
23
|
+
checks.push(await checkAgents());
|
|
24
|
+
checks.push(await checkPythonHelp());
|
|
25
|
+
checks.push(await checkSampleTask());
|
|
26
|
+
checks.push(await checkDryRun());
|
|
27
|
+
checks.push(await checkInstallUninstallSimulation());
|
|
28
|
+
report(checks);
|
|
29
|
+
return checks.some((check) => check.level === "FAIL") ? 1 : 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function checkPackageJson() {
|
|
33
|
+
const packageJson = JSON.parse(await fs.readFile(path.join(packageRoot, "package.json"), "utf8"));
|
|
34
|
+
const errors = [];
|
|
35
|
+
if (packageJson.name !== "@bx-h/meta-flow") errors.push("unexpected package name");
|
|
36
|
+
if (packageJson.scripts?.postinstall) errors.push("postinstall must not exist");
|
|
37
|
+
if (packageJson.bin?.["meta-flow"] !== "bin/meta-flow.js") errors.push("bin meta-flow is invalid");
|
|
38
|
+
if (packageJson.type !== "module") errors.push("type must be module");
|
|
39
|
+
return result("package.json", errors);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function checkNoForbiddenSourcePatterns() {
|
|
43
|
+
const files = await collectJsFiles(path.join(packageRoot, "src"));
|
|
44
|
+
files.push(path.join(packageRoot, "bin", "meta-flow.js"));
|
|
45
|
+
const errors = [];
|
|
46
|
+
for (const filePath of files) {
|
|
47
|
+
const text = await fs.readFile(filePath, "utf8");
|
|
48
|
+
if (/\beval\s*\(/.test(text)) errors.push(`dynamic eval found in ${filePath}`);
|
|
49
|
+
if (/https?:\/\/.+exec/i.test(text)) errors.push(`suspicious remote execution pattern in ${filePath}`);
|
|
50
|
+
}
|
|
51
|
+
return result("no eval or remote execution patterns", errors);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function checkPlugin() {
|
|
55
|
+
const plugin = await validatePlugin();
|
|
56
|
+
return result("plugin manifest and skill", plugin.errors);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function checkAgents() {
|
|
60
|
+
const results = await validateAgentTemplates();
|
|
61
|
+
const errors = results
|
|
62
|
+
.filter((item) => item.missing.length)
|
|
63
|
+
.map((item) => `${path.basename(item.filePath)} missing ${item.missing.join(", ")}`);
|
|
64
|
+
return result("agent TOML required fields", errors);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function checkPythonHelp() {
|
|
68
|
+
const scripts = [
|
|
69
|
+
"new_task.py",
|
|
70
|
+
"validate_goal_contract.py",
|
|
71
|
+
"aggregate_reviews.py",
|
|
72
|
+
"validate_adjudication.py",
|
|
73
|
+
"validate_milestone_plan.py",
|
|
74
|
+
"validate_task_list.py",
|
|
75
|
+
"validate_task_verification.py",
|
|
76
|
+
"status.py"
|
|
77
|
+
];
|
|
78
|
+
const python = resolvePython();
|
|
79
|
+
const errors = [];
|
|
80
|
+
for (const script of scripts) {
|
|
81
|
+
const run = spawnSync(python, [path.join(pluginSource, "scripts", script), "--help"], pythonOptions());
|
|
82
|
+
if (run.status !== 0) {
|
|
83
|
+
errors.push(`${script} --help failed: ${run.stderr || run.stdout}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return result("Python script --help", errors);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function checkSampleTask() {
|
|
90
|
+
const python = resolvePython();
|
|
91
|
+
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "meta-flow-verify-"));
|
|
92
|
+
const runs = [
|
|
93
|
+
[path.join(pluginSource, "scripts", "validate_goal_contract.py"), path.join(sampleTaskRoot, "goal-contract.json")],
|
|
94
|
+
[
|
|
95
|
+
path.join(pluginSource, "scripts", "aggregate_reviews.py"),
|
|
96
|
+
"--reviews-dir",
|
|
97
|
+
path.join(sampleTaskRoot, "reviews"),
|
|
98
|
+
"--output",
|
|
99
|
+
path.join(tmp, "review-aggregate.json"),
|
|
100
|
+
"--task-id",
|
|
101
|
+
"sample-health-check"
|
|
102
|
+
],
|
|
103
|
+
[path.join(pluginSource, "scripts", "validate_adjudication.py"), path.join(sampleTaskRoot, "adjudication-report.json")],
|
|
104
|
+
[path.join(pluginSource, "scripts", "validate_milestone_plan.py"), path.join(sampleTaskRoot, "milestone-plan.json")],
|
|
105
|
+
[path.join(pluginSource, "scripts", "validate_task_list.py"), path.join(sampleTaskRoot, "milestones", "M1", "task-list.json")],
|
|
106
|
+
[
|
|
107
|
+
path.join(pluginSource, "scripts", "validate_task_verification.py"),
|
|
108
|
+
path.join(sampleTaskRoot, "milestones", "M1", "tasks", "T1", "verification-report.json")
|
|
109
|
+
]
|
|
110
|
+
];
|
|
111
|
+
const errors = [];
|
|
112
|
+
for (const args of runs) {
|
|
113
|
+
const run = spawnSync(python, args, pythonOptions());
|
|
114
|
+
if (run.status !== 0) {
|
|
115
|
+
errors.push(`${path.basename(args[0])} failed: ${run.stderr || run.stdout}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return result("sample-task validation", errors);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function checkDryRun() {
|
|
122
|
+
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "meta-flow-dry-run-"));
|
|
123
|
+
await runInstall(["--scope", "repo", "--target", tmp, "--dry-run"]);
|
|
124
|
+
const entries = await fs.readdir(tmp);
|
|
125
|
+
return result("install --dry-run does not write", entries.length ? [`dry-run wrote files: ${entries.join(", ")}`] : []);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function checkInstallUninstallSimulation() {
|
|
129
|
+
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "meta-flow-install-"));
|
|
130
|
+
const errors = [];
|
|
131
|
+
const installCode = await runInstall(["--scope", "repo", "--target", tmp, "--yes"]);
|
|
132
|
+
if (installCode !== 0) errors.push("install returned non-zero");
|
|
133
|
+
const secondInstallCode = await runInstall(["--scope", "repo", "--target", tmp, "--yes"]);
|
|
134
|
+
if (secondInstallCode !== 0) errors.push("second install returned non-zero");
|
|
135
|
+
const marketplace = JSON.parse(await fs.readFile(path.join(tmp, ".agents", "plugins", "marketplace.json"), "utf8"));
|
|
136
|
+
const entries = marketplace.plugins.filter((plugin) => plugin.name === "meta-flow");
|
|
137
|
+
if (entries.length !== 1) errors.push(`marketplace has ${entries.length} meta-flow entries`);
|
|
138
|
+
const doctorCode = await runDoctor(["--scope", "repo", "--target", tmp]);
|
|
139
|
+
if (doctorCode !== 0) errors.push("doctor returned non-zero after install");
|
|
140
|
+
const uninstallCode = await runUninstall(["--scope", "repo", "--target", tmp, "--yes"]);
|
|
141
|
+
if (uninstallCode !== 0) errors.push("uninstall returned non-zero");
|
|
142
|
+
return result("repo install/doctor/uninstall simulation", errors);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function collectJsFiles(dir) {
|
|
146
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
147
|
+
const files = [];
|
|
148
|
+
for (const entry of entries) {
|
|
149
|
+
const full = path.join(dir, entry.name);
|
|
150
|
+
if (entry.isDirectory()) {
|
|
151
|
+
files.push(...await collectJsFiles(full));
|
|
152
|
+
} else if (entry.isFile() && entry.name.endsWith(".js")) {
|
|
153
|
+
files.push(full);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return files;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function resolvePython() {
|
|
160
|
+
const python3 = spawnSync("python3", ["--version"], pythonOptions());
|
|
161
|
+
return python3.status === 0 ? "python3" : "python";
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function pythonOptions() {
|
|
165
|
+
return {
|
|
166
|
+
encoding: "utf8",
|
|
167
|
+
env: {
|
|
168
|
+
...process.env,
|
|
169
|
+
PYTHONDONTWRITEBYTECODE: "1"
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function result(title, errors) {
|
|
175
|
+
return errors.length ? { level: "FAIL", title, errors } : { level: "PASS", title, errors: [] };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function report(checks) {
|
|
179
|
+
for (const check of checks) {
|
|
180
|
+
if (check.level === "PASS") {
|
|
181
|
+
console.log(`PASS: ${check.title}`);
|
|
182
|
+
} else {
|
|
183
|
+
console.error(`FAIL: ${check.title}`);
|
|
184
|
+
for (const error of check.errors) console.error(` - ${error}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const thisFile = fileURLToPath(import.meta.url);
|
|
190
|
+
if (process.argv[1] === thisFile) {
|
|
191
|
+
runVerify(process.argv.slice(2)).then((code) => {
|
|
192
|
+
process.exitCode = code;
|
|
193
|
+
}).catch((error) => {
|
|
194
|
+
console.error(error.message);
|
|
195
|
+
process.exitCode = 1;
|
|
196
|
+
});
|
|
197
|
+
}
|
package/src/cli/index.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { runDoctor, doctorHelp } from "./commands/doctor.js";
|
|
4
|
+
import { runInstall, installHelp } from "./commands/install.js";
|
|
5
|
+
import { runPrintPaths, printPathsHelp } from "./commands/print_paths.js";
|
|
6
|
+
import { runUninstall, uninstallHelp } from "./commands/uninstall.js";
|
|
7
|
+
import { runVerify } from "./commands/verify.js";
|
|
8
|
+
import { packageRoot } from "./lib/paths.js";
|
|
9
|
+
|
|
10
|
+
const COMMANDS = {
|
|
11
|
+
install: { run: runInstall, help: installHelp },
|
|
12
|
+
uninstall: { run: runUninstall, help: uninstallHelp },
|
|
13
|
+
doctor: { run: runDoctor, help: doctorHelp },
|
|
14
|
+
verify: { run: runVerify, help: () => "Usage: meta-flow verify" },
|
|
15
|
+
"print-paths": { run: runPrintPaths, help: printPathsHelp }
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export async function main(argv = []) {
|
|
19
|
+
if (argv.length === 0 || argv.includes("--help") || argv.includes("-h")) {
|
|
20
|
+
printHelp();
|
|
21
|
+
return 0;
|
|
22
|
+
}
|
|
23
|
+
if (argv.includes("--version")) {
|
|
24
|
+
const packageJson = JSON.parse(await fs.readFile(path.join(packageRoot, "package.json"), "utf8"));
|
|
25
|
+
console.log(packageJson.version);
|
|
26
|
+
return 0;
|
|
27
|
+
}
|
|
28
|
+
const [command, ...rest] = argv;
|
|
29
|
+
const entry = COMMANDS[command];
|
|
30
|
+
if (!entry) {
|
|
31
|
+
throw new Error(`Unknown command: ${command}\n\n${topHelp()}`);
|
|
32
|
+
}
|
|
33
|
+
return entry.run(rest);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function printHelp() {
|
|
37
|
+
console.log(topHelp());
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function topHelp() {
|
|
41
|
+
return `Usage: meta-flow <command> [options]
|
|
42
|
+
|
|
43
|
+
Commands:
|
|
44
|
+
install Install plugin, agents, marketplace entry, and config.
|
|
45
|
+
uninstall Remove managed plugin and marked agents.
|
|
46
|
+
doctor Check installed state.
|
|
47
|
+
verify Verify package structure and installer behavior.
|
|
48
|
+
print-paths Print repo/user scope target paths.
|
|
49
|
+
|
|
50
|
+
Global:
|
|
51
|
+
--help Show help.
|
|
52
|
+
--version Show package version.`;
|
|
53
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
AGENT_HEADER,
|
|
5
|
+
backupIfExists,
|
|
6
|
+
hasMetaFlowMarker,
|
|
7
|
+
pathExists,
|
|
8
|
+
removeFileSafe,
|
|
9
|
+
writeTextSafe
|
|
10
|
+
} from "./fs_safe.js";
|
|
11
|
+
import { agentTemplatesSource } from "./paths.js";
|
|
12
|
+
|
|
13
|
+
export const AGENT_FILES = [
|
|
14
|
+
"questioner.toml",
|
|
15
|
+
"researcher-proposer.toml",
|
|
16
|
+
"product-reviewer.toml",
|
|
17
|
+
"technical-reviewer.toml",
|
|
18
|
+
"risk-reviewer.toml",
|
|
19
|
+
"verification-reviewer.toml",
|
|
20
|
+
"adjudicator.toml",
|
|
21
|
+
"proposal-summarizer.toml",
|
|
22
|
+
"planner.toml",
|
|
23
|
+
"direction-evaluator.toml",
|
|
24
|
+
"task-decomposer.toml",
|
|
25
|
+
"executor.toml",
|
|
26
|
+
"result-verifier.toml",
|
|
27
|
+
"final-summarizer.toml"
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
export async function installAgents(targets, options = {}) {
|
|
31
|
+
const { dryRun = false, force = false, logger } = options;
|
|
32
|
+
const installed = [];
|
|
33
|
+
const conflicts = [];
|
|
34
|
+
|
|
35
|
+
for (const fileName of AGENT_FILES) {
|
|
36
|
+
const source = path.join(agentTemplatesSource, fileName);
|
|
37
|
+
const target = path.join(targets.agentsTarget, fileName);
|
|
38
|
+
const sourceText = await fs.readFile(source, "utf8");
|
|
39
|
+
const targetExists = await pathExists(target);
|
|
40
|
+
if (targetExists && !(await hasMetaFlowMarker(target))) {
|
|
41
|
+
if (!force) {
|
|
42
|
+
conflicts.push(target);
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
await backupIfExists(target, { dryRun, logger });
|
|
46
|
+
}
|
|
47
|
+
await writeTextSafe(target, `${AGENT_HEADER}${sourceText.replace(/^# Installed by meta-flow\.[\s\S]*?developer_instructions/m, "developer_instructions")}`, { dryRun, logger });
|
|
48
|
+
installed.push(target);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return { installed, conflicts };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function uninstallAgents(targets, options = {}) {
|
|
55
|
+
const removed = [];
|
|
56
|
+
const skipped = [];
|
|
57
|
+
for (const fileName of AGENT_FILES) {
|
|
58
|
+
const target = path.join(targets.agentsTarget, fileName);
|
|
59
|
+
if (!(await pathExists(target))) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (!(await hasMetaFlowMarker(target))) {
|
|
63
|
+
skipped.push(target);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
await removeFileSafe(targets.agentsTarget, target, options);
|
|
67
|
+
removed.push(target);
|
|
68
|
+
}
|
|
69
|
+
return { removed, skipped };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function validateAgentTemplate(filePath) {
|
|
73
|
+
const text = await fs.readFile(filePath, "utf8");
|
|
74
|
+
const missing = [];
|
|
75
|
+
for (const key of ["name", "description", "developer_instructions"]) {
|
|
76
|
+
if (!new RegExp(`^\\s*${key}\\s*=`, "m").test(text)) {
|
|
77
|
+
missing.push(key);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return { filePath, missing };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function validateAgentTemplates(root = agentTemplatesSource) {
|
|
84
|
+
const results = [];
|
|
85
|
+
for (const fileName of AGENT_FILES) {
|
|
86
|
+
results.push(await validateAgentTemplate(path.join(root, fileName)));
|
|
87
|
+
}
|
|
88
|
+
return results;
|
|
89
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export function parseOptions(argv, schema = {}) {
|
|
2
|
+
const options = {};
|
|
3
|
+
const rest = [];
|
|
4
|
+
|
|
5
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
6
|
+
const token = argv[index];
|
|
7
|
+
if (!token.startsWith("--")) {
|
|
8
|
+
rest.push(token);
|
|
9
|
+
continue;
|
|
10
|
+
}
|
|
11
|
+
if (token === "--") {
|
|
12
|
+
rest.push(...argv.slice(index + 1));
|
|
13
|
+
break;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const [rawName, inlineValue] = token.slice(2).split("=", 2);
|
|
17
|
+
const negated = rawName.startsWith("no-");
|
|
18
|
+
const name = toCamel(negated ? rawName.slice(3) : rawName);
|
|
19
|
+
const kind = schema[name] || "boolean";
|
|
20
|
+
|
|
21
|
+
if (negated) {
|
|
22
|
+
options[name] = false;
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (kind === "boolean") {
|
|
26
|
+
options[name] = inlineValue === undefined ? true : inlineValue !== "false";
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (inlineValue !== undefined) {
|
|
30
|
+
options[name] = inlineValue;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (index + 1 >= argv.length) {
|
|
34
|
+
throw new Error(`Missing value for --${rawName}`);
|
|
35
|
+
}
|
|
36
|
+
options[name] = argv[index + 1];
|
|
37
|
+
index += 1;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return { options, rest };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function toCamel(value) {
|
|
44
|
+
return value.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function helpRequested(argv) {
|
|
48
|
+
return argv.includes("--help") || argv.includes("-h");
|
|
49
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import { backupIfExists, pathExists, writeTextSafe } from "./fs_safe.js";
|
|
3
|
+
|
|
4
|
+
const DEFAULT_AGENTS_CONFIG = "[agents]\nmax_threads = 6\nmax_depth = 1\n";
|
|
5
|
+
|
|
6
|
+
function sectionBounds(lines, sectionName) {
|
|
7
|
+
const header = `[${sectionName}]`;
|
|
8
|
+
const start = lines.findIndex((line) => line.trim() === header);
|
|
9
|
+
if (start < 0) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
let end = lines.length;
|
|
13
|
+
for (let index = start + 1; index < lines.length; index += 1) {
|
|
14
|
+
if (/^\s*\[[^\]]+\]\s*$/.test(lines[index])) {
|
|
15
|
+
end = index;
|
|
16
|
+
break;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return { start, end };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function setOrInsert(lines, bounds, key, value) {
|
|
23
|
+
const pattern = new RegExp(`^\\s*${key}\\s*=`);
|
|
24
|
+
for (let index = bounds.start + 1; index < bounds.end; index += 1) {
|
|
25
|
+
if (pattern.test(lines[index])) {
|
|
26
|
+
lines[index] = `${key} = ${value}`;
|
|
27
|
+
return { changed: true, existed: true };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
lines.splice(bounds.end, 0, `${key} = ${value}`);
|
|
31
|
+
bounds.end += 1;
|
|
32
|
+
return { changed: true, existed: false };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function patchCodexConfig(filePath, { force = false, dryRun = false, backup = false, logger } = {}) {
|
|
36
|
+
if (!(await pathExists(filePath))) {
|
|
37
|
+
await writeTextSafe(filePath, DEFAULT_AGENTS_CONFIG, { dryRun, logger });
|
|
38
|
+
return { changed: true, warnings: [] };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const content = await fs.readFile(filePath, "utf8");
|
|
42
|
+
const lines = content.split(/\r?\n/);
|
|
43
|
+
const warnings = [];
|
|
44
|
+
let changed = false;
|
|
45
|
+
let bounds = sectionBounds(lines, "agents");
|
|
46
|
+
|
|
47
|
+
if (!bounds) {
|
|
48
|
+
if (lines.at(-1) !== "") {
|
|
49
|
+
lines.push("");
|
|
50
|
+
}
|
|
51
|
+
lines.push("[agents]", "max_threads = 6", "max_depth = 1");
|
|
52
|
+
changed = true;
|
|
53
|
+
} else {
|
|
54
|
+
const section = lines.slice(bounds.start + 1, bounds.end).join("\n");
|
|
55
|
+
if (/^\s*max_threads\s*=/m.test(section)) {
|
|
56
|
+
if (force) {
|
|
57
|
+
setOrInsert(lines, bounds, "max_threads", "6");
|
|
58
|
+
changed = true;
|
|
59
|
+
} else {
|
|
60
|
+
warnings.push("config already contains [agents].max_threads; leaving it unchanged");
|
|
61
|
+
}
|
|
62
|
+
} else {
|
|
63
|
+
setOrInsert(lines, bounds, "max_threads", "6");
|
|
64
|
+
changed = true;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
bounds = sectionBounds(lines, "agents");
|
|
68
|
+
const refreshed = lines.slice(bounds.start + 1, bounds.end).join("\n");
|
|
69
|
+
if (/^\s*max_depth\s*=/m.test(refreshed)) {
|
|
70
|
+
if (force) {
|
|
71
|
+
setOrInsert(lines, bounds, "max_depth", "1");
|
|
72
|
+
changed = true;
|
|
73
|
+
} else {
|
|
74
|
+
warnings.push("config already contains [agents].max_depth; leaving it unchanged");
|
|
75
|
+
}
|
|
76
|
+
} else {
|
|
77
|
+
setOrInsert(lines, bounds, "max_depth", "1");
|
|
78
|
+
changed = true;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (changed) {
|
|
83
|
+
if (force || backup) {
|
|
84
|
+
await backupIfExists(filePath, { dryRun, logger });
|
|
85
|
+
}
|
|
86
|
+
await writeTextSafe(filePath, `${lines.join("\n").replace(/\n*$/, "")}\n`, { dryRun, logger });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return { changed, warnings };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function inspectCodexConfig(filePath) {
|
|
93
|
+
if (!(await pathExists(filePath))) {
|
|
94
|
+
return { exists: false, hasAgents: false, hasMaxThreads: false, hasMaxDepth: false };
|
|
95
|
+
}
|
|
96
|
+
const content = await fs.readFile(filePath, "utf8");
|
|
97
|
+
const lines = content.split(/\r?\n/);
|
|
98
|
+
const bounds = sectionBounds(lines, "agents");
|
|
99
|
+
const section = bounds ? lines.slice(bounds.start + 1, bounds.end).join("\n") : "";
|
|
100
|
+
return {
|
|
101
|
+
exists: true,
|
|
102
|
+
hasAgents: Boolean(bounds),
|
|
103
|
+
hasMaxThreads: /^\s*max_threads\s*=/m.test(section),
|
|
104
|
+
hasMaxDepth: /^\s*max_depth\s*=/m.test(section)
|
|
105
|
+
};
|
|
106
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export const META_FLOW_MARKER = "# Installed by meta-flow.";
|
|
5
|
+
export const AGENT_HEADER = `${META_FLOW_MARKER}
|
|
6
|
+
# Source: https://github.com/bx-h/meta-flow
|
|
7
|
+
# Do not edit unless you know what you are doing.
|
|
8
|
+
`;
|
|
9
|
+
|
|
10
|
+
export async function pathExists(filePath) {
|
|
11
|
+
try {
|
|
12
|
+
await fs.access(filePath);
|
|
13
|
+
return true;
|
|
14
|
+
} catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function ensureInside(parent, child) {
|
|
20
|
+
const resolvedParent = path.resolve(parent);
|
|
21
|
+
const resolvedChild = path.resolve(child);
|
|
22
|
+
const relative = path.relative(resolvedParent, resolvedChild);
|
|
23
|
+
if (relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative))) {
|
|
24
|
+
return resolvedChild;
|
|
25
|
+
}
|
|
26
|
+
throw new Error(`Unsafe path outside ${resolvedParent}: ${resolvedChild}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function readJsonOrDefault(filePath, defaultValue) {
|
|
30
|
+
if (!(await pathExists(filePath))) {
|
|
31
|
+
return structuredClone(defaultValue);
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
return JSON.parse(await fs.readFile(filePath, "utf8"));
|
|
35
|
+
} catch (error) {
|
|
36
|
+
throw new Error(`Invalid JSON at ${filePath}: ${error.message}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function writeJsonPretty(filePath, value, { dryRun = false, logger } = {}) {
|
|
41
|
+
if (dryRun) {
|
|
42
|
+
logger?.info(`DRY-RUN write JSON ${filePath}`);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
46
|
+
await fs.writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function backupPath(filePath, timestamp = timestampForBackup()) {
|
|
50
|
+
return `${filePath}.bak.${timestamp}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function timestampForBackup(date = new Date()) {
|
|
54
|
+
return date.toISOString().replace(/[-:]/g, "").replace(/\..+$/, "Z");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function backupIfExists(filePath, { dryRun = false, logger } = {}) {
|
|
58
|
+
if (!(await pathExists(filePath))) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
const backup = backupPath(filePath);
|
|
62
|
+
if (dryRun) {
|
|
63
|
+
logger?.info(`DRY-RUN backup ${filePath} -> ${backup}`);
|
|
64
|
+
return backup;
|
|
65
|
+
}
|
|
66
|
+
await copyPath(filePath, backup);
|
|
67
|
+
return backup;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function hasMetaFlowMarker(filePath) {
|
|
71
|
+
if (!(await pathExists(filePath))) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
const head = await fs.readFile(filePath, "utf8");
|
|
75
|
+
return head.slice(0, 512).includes(META_FLOW_MARKER);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function copyDirSafe(src, dest, { dryRun = false, logger } = {}) {
|
|
79
|
+
if (dryRun) {
|
|
80
|
+
logger?.info(`DRY-RUN copy directory ${src} -> ${dest}`);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
await fs.mkdir(dest, { recursive: true });
|
|
84
|
+
const entries = await fs.readdir(src, { withFileTypes: true });
|
|
85
|
+
for (const entry of entries) {
|
|
86
|
+
const from = path.join(src, entry.name);
|
|
87
|
+
const to = path.join(dest, entry.name);
|
|
88
|
+
if (entry.isDirectory()) {
|
|
89
|
+
await copyDirSafe(from, to, { dryRun, logger });
|
|
90
|
+
} else if (entry.isFile()) {
|
|
91
|
+
await fs.mkdir(path.dirname(to), { recursive: true });
|
|
92
|
+
await fs.copyFile(from, to);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function copyPath(src, dest) {
|
|
98
|
+
const stat = await fs.stat(src);
|
|
99
|
+
if (stat.isDirectory()) {
|
|
100
|
+
await fs.mkdir(dest, { recursive: true });
|
|
101
|
+
const entries = await fs.readdir(src, { withFileTypes: true });
|
|
102
|
+
for (const entry of entries) {
|
|
103
|
+
await copyPath(path.join(src, entry.name), path.join(dest, entry.name));
|
|
104
|
+
}
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
await fs.mkdir(path.dirname(dest), { recursive: true });
|
|
108
|
+
await fs.copyFile(src, dest);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function removeDirSafe(parent, target, { dryRun = false, logger } = {}) {
|
|
112
|
+
const safeTarget = ensureInside(parent, target);
|
|
113
|
+
if (!(await pathExists(safeTarget))) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (dryRun) {
|
|
117
|
+
logger?.info(`DRY-RUN remove directory ${safeTarget}`);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
await fs.rm(safeTarget, { recursive: true, force: true });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export async function removeFileSafe(parent, target, { dryRun = false, logger } = {}) {
|
|
124
|
+
const safeTarget = ensureInside(parent, target);
|
|
125
|
+
if (!(await pathExists(safeTarget))) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (dryRun) {
|
|
129
|
+
logger?.info(`DRY-RUN remove file ${safeTarget}`);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
await fs.rm(safeTarget, { force: true });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export async function writeTextSafe(filePath, content, { dryRun = false, logger } = {}) {
|
|
136
|
+
if (dryRun) {
|
|
137
|
+
logger?.info(`DRY-RUN write ${filePath}`);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
141
|
+
await fs.writeFile(filePath, content, "utf8");
|
|
142
|
+
}
|