@0dai-dev/cli 4.3.6 → 4.3.8
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/README.md +12 -11
- package/bin/0dai.js +133 -33
- package/lib/ai/manifest/mcp-exposure-contract.json +121 -0
- package/lib/ai/meta/manifest/mcp-tool-tiers.json +435 -0
- package/lib/ai/registry/mcp-catalog.json +98 -0
- package/lib/commands/auth.js +2 -1
- package/lib/commands/compliance.js +1 -1
- package/lib/commands/doctor.js +707 -12
- package/lib/commands/experience.js +40 -5
- package/lib/commands/feedback.js +157 -15
- package/lib/commands/gh.js +26 -0
- package/lib/commands/graph.js +9 -4
- package/lib/commands/heatmap.js +1 -1
- package/lib/commands/init.js +298 -27
- package/lib/commands/mcp.js +111 -33
- package/lib/commands/models.js +138 -41
- package/lib/commands/play.js +20 -4
- package/lib/commands/provider.js +30 -59
- package/lib/commands/quota.js +1 -1
- package/lib/commands/receipt.js +1 -1
- package/lib/commands/run.js +14 -6
- package/lib/commands/runner.js +31 -1
- package/lib/commands/status.js +176 -11
- package/lib/commands/swarm.js +130 -12
- package/lib/commands/trust.js +1 -1
- package/lib/commands/update.js +184 -38
- package/lib/commands/usage.js +1 -1
- package/lib/commands/validate.js +32 -3
- package/lib/commands/vault.js +43 -8
- package/lib/python/__init__.py +0 -0
- package/lib/python/agent_quotas.py +525 -0
- package/lib/python/anomaly_alert.py +397 -0
- package/lib/python/anti_pattern_detector.py +799 -0
- package/lib/python/auth.py +443 -0
- package/lib/python/capi_profile_guard.py +477 -0
- package/lib/python/compliance_report.py +581 -0
- package/lib/python/drift_detector.py +388 -0
- package/lib/python/experience_pipeline.py +1130 -0
- package/lib/python/graph.py +19 -0
- package/lib/python/graph_core.py +293 -0
- package/lib/python/graph_io.py +179 -0
- package/lib/python/graph_legacy.py +2052 -0
- package/lib/python/graph_legacy_helpers.py +221 -0
- package/lib/python/graph_outcomes_core.py +85 -0
- package/lib/python/graph_queries.py +171 -0
- package/lib/python/graph_slice.py +198 -0
- package/lib/python/graph_slicer.py +576 -0
- package/lib/python/graph_slicer_cli.py +60 -0
- package/lib/python/graph_validation.py +64 -0
- package/lib/python/heatmap.py +943 -0
- package/lib/python/json_utils.py +193 -0
- package/lib/python/mcp_exposure_check.py +247 -0
- package/lib/python/model_router.py +1434 -0
- package/lib/python/project_manager.py +621 -0
- package/lib/python/provider_profiles.py +1618 -0
- package/lib/python/provider_registry.py +1211 -0
- package/lib/python/provider_registry_cli.py +125 -0
- package/lib/python/receipt_png.py +727 -0
- package/lib/python/structural_memory.py +325 -0
- package/lib/python/swarm_cost.py +177 -0
- package/lib/python/usage_ledger.py +569 -0
- package/lib/scripts/mcp_tier_config.py +240 -0
- package/lib/shared.js +96 -12
- package/lib/tui/index.mjs +35174 -0
- package/lib/utils/activation_telemetry.js +1 -4
- package/lib/utils/constants.js +7 -1
- package/lib/utils/identity.js +184 -0
- package/lib/utils/mcp-auth.js +81 -15
- package/lib/utils/plan.js +1 -1
- package/lib/vault/index.js +19 -3
- package/lib/vault/storage.js +21 -2
- package/lib/wizard.js +5 -2
- package/package.json +9 -3
- package/scripts/build-python-bundle.js +106 -0
- package/scripts/build-tui.js +14 -1
- package/scripts/harvest_experience.py +523 -0
- package/scripts/postinstall.js +15 -9
package/lib/commands/receipt.js
CHANGED
|
@@ -18,7 +18,7 @@ function _findArg(args, flag) {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
function cmdReceipt(target, args) {
|
|
21
|
-
const script =
|
|
21
|
+
const script = shared.resolvePythonScript(target, "receipt_png.py");
|
|
22
22
|
if (!script) {
|
|
23
23
|
log("receipt generator unavailable — missing scripts/receipt_png.py");
|
|
24
24
|
console.log(` ${D}Run from a 0dai-initialized project root.${R}`);
|
package/lib/commands/run.js
CHANGED
|
@@ -9,12 +9,20 @@ const {
|
|
|
9
9
|
} = require("../utils/run_cost");
|
|
10
10
|
const { recordActivationFirstTask } = require("../utils/activation_telemetry");
|
|
11
11
|
|
|
12
|
+
function printRunHelp() {
|
|
13
|
+
console.log(`Usage: 0dai run <goal> [--dry-run] [--dry-cost] [--max-cost N] [--agent claude|codex|gemini] [--provider deepseek|gemini-direct|codex|claude-opus]`);
|
|
14
|
+
console.log(` Example: 0dai run "add dark mode to settings page"`);
|
|
15
|
+
console.log(` Example: 0dai run "fix login bug" --dry-cost`);
|
|
16
|
+
console.log(` Example: 0dai run "refactor auth" --max-cost 0.50`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function isRunHelpRequest(goal, args = []) {
|
|
20
|
+
return goal === "--help" || goal === "-h" || args.includes("--help") || args.includes("-h");
|
|
21
|
+
}
|
|
22
|
+
|
|
12
23
|
async function cmdRun(goal, target, args = []) {
|
|
13
|
-
if (!goal) {
|
|
14
|
-
|
|
15
|
-
console.log(` Example: 0dai run "add dark mode to settings page"`);
|
|
16
|
-
console.log(` Example: 0dai run "fix login bug" --dry-cost`);
|
|
17
|
-
console.log(` Example: 0dai run "refactor auth" --max-cost 0.50`);
|
|
24
|
+
if (!goal || isRunHelpRequest(goal, args)) {
|
|
25
|
+
printRunHelp();
|
|
18
26
|
return;
|
|
19
27
|
}
|
|
20
28
|
|
|
@@ -174,4 +182,4 @@ function providerAgent(provider) {
|
|
|
174
182
|
return null;
|
|
175
183
|
}
|
|
176
184
|
|
|
177
|
-
module.exports = { cmdRun, printCostPreview };
|
|
185
|
+
module.exports = { cmdRun, printCostPreview, printRunHelp };
|
package/lib/commands/runner.js
CHANGED
|
@@ -376,6 +376,31 @@ function runRouteDryRunController(target, configPath, forwarded) {
|
|
|
376
376
|
}
|
|
377
377
|
}
|
|
378
378
|
|
|
379
|
+
function runHostPoolPlanController(target, forwarded) {
|
|
380
|
+
const script = findRepoScript(target, "runner_host_pool_autosize.py");
|
|
381
|
+
if (!script) {
|
|
382
|
+
log("runner host-pool plan controller not found");
|
|
383
|
+
console.log(` ${D}expected: scripts/runner_host_pool_autosize.py${R}`);
|
|
384
|
+
process.exitCode = 1;
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
const result = spawnSync("python3", [script, "host-pool-plan", ...forwarded], {
|
|
388
|
+
encoding: "utf8",
|
|
389
|
+
timeout: 30000,
|
|
390
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
391
|
+
});
|
|
392
|
+
if (result.error) {
|
|
393
|
+
log(`runner host-pool plan controller failed: ${result.error.message}`);
|
|
394
|
+
process.exitCode = 1;
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
398
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
399
|
+
if (typeof result.status === "number" && result.status !== 0) {
|
|
400
|
+
process.exitCode = result.status;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
379
404
|
function runLabelAuditController(target, configPath, forwarded) {
|
|
380
405
|
const script = findRepoScript(target, "runner_label_audit.py");
|
|
381
406
|
if (!script) {
|
|
@@ -468,6 +493,11 @@ function cmdRunner(target, sub, args) {
|
|
|
468
493
|
return;
|
|
469
494
|
}
|
|
470
495
|
|
|
496
|
+
if (command === "host-pool-plan" || command === "host-pool-autosize") {
|
|
497
|
+
runHostPoolPlanController(target, forwarded);
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
|
|
471
501
|
if (command === "label-audit") {
|
|
472
502
|
runLabelAuditController(target, configPath, forwarded);
|
|
473
503
|
return;
|
|
@@ -485,7 +515,7 @@ function cmdRunner(target, sub, args) {
|
|
|
485
515
|
}
|
|
486
516
|
|
|
487
517
|
if (command !== "status" && command !== "plan") {
|
|
488
|
-
console.log("Usage: 0dai runner [status|plan|queue-status|label-audit|burst-plan|route-dry-run|burst-apply|burst-preflight|burst-reap|burst-reap-plan] [--json] [--offline]");
|
|
518
|
+
console.log("Usage: 0dai runner [status|plan|queue-status|label-audit|burst-plan|route-dry-run|host-pool-plan|burst-apply|burst-preflight|burst-reap|burst-reap-plan] [--json] [--offline]");
|
|
489
519
|
process.exitCode = 1;
|
|
490
520
|
return;
|
|
491
521
|
}
|
package/lib/commands/status.js
CHANGED
|
@@ -9,6 +9,10 @@ const {
|
|
|
9
9
|
getActivationMergedPrStats,
|
|
10
10
|
printActivationStats,
|
|
11
11
|
} = require("../utils/activation_telemetry");
|
|
12
|
+
const {
|
|
13
|
+
collectMcpExposurePayload,
|
|
14
|
+
formatMcpExposureLine,
|
|
15
|
+
} = require("./doctor");
|
|
12
16
|
|
|
13
17
|
function countJson(dir) {
|
|
14
18
|
try { return fs.readdirSync(dir).filter(f => f.endsWith(".json")).length; } catch { return 0; }
|
|
@@ -19,17 +23,133 @@ function loadJson(file) {
|
|
|
19
23
|
}
|
|
20
24
|
|
|
21
25
|
function loadProjectBinding(target) {
|
|
22
|
-
return
|
|
26
|
+
return shared.loadProjectBinding(target);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Open sprint:current issues are the authoritative work queue (SPRINT.md
|
|
30
|
+
// retired as SoT). Probe gh; degrade quietly when gh is absent or the token
|
|
31
|
+
// is not authenticated — never throw out of status.
|
|
32
|
+
function collectSprintPayload(target, options = {}) {
|
|
33
|
+
const out = { available: false, authenticated: false, issues: [], reason: "" };
|
|
34
|
+
try {
|
|
35
|
+
const r = spawnSync(
|
|
36
|
+
"gh",
|
|
37
|
+
[
|
|
38
|
+
"issue", "list",
|
|
39
|
+
"--label", "sprint:current",
|
|
40
|
+
"--state", "open",
|
|
41
|
+
"--json", "number,title",
|
|
42
|
+
"--limit", "20",
|
|
43
|
+
],
|
|
44
|
+
{
|
|
45
|
+
cwd: target,
|
|
46
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
47
|
+
encoding: "utf8",
|
|
48
|
+
timeout: options.ghTimeout || 8000,
|
|
49
|
+
},
|
|
50
|
+
);
|
|
51
|
+
if (r.error && r.error.code === "ENOENT") {
|
|
52
|
+
out.reason = "gh CLI not installed";
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
out.available = true;
|
|
56
|
+
if (r.status !== 0) {
|
|
57
|
+
const err = String(r.stderr || "").trim();
|
|
58
|
+
out.reason = /auth|logged in|token/i.test(err)
|
|
59
|
+
? "gh not authenticated — run: gh auth login"
|
|
60
|
+
: (err.split("\n").slice(-1)[0] || "gh issue list failed");
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
out.authenticated = true;
|
|
64
|
+
const parsed = JSON.parse(String(r.stdout || "[]").trim() || "[]");
|
|
65
|
+
out.issues = Array.isArray(parsed)
|
|
66
|
+
? parsed.map((i) => ({ number: i.number, title: i.title || "" }))
|
|
67
|
+
: [];
|
|
68
|
+
} catch (e) {
|
|
69
|
+
out.reason = String((e && e.message) || e).split("\n").slice(-1)[0] || "sprint probe failed";
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Active flight = newest flight plan under ai/meta/reports/flight-F*.md, ranked
|
|
75
|
+
// by flight number (F17 > F6) then by the trailing date in the filename.
|
|
76
|
+
function findActiveFlight(target) {
|
|
77
|
+
const dir = path.join(target, "ai", "meta", "reports");
|
|
78
|
+
let names;
|
|
79
|
+
try {
|
|
80
|
+
names = fs.readdirSync(dir).filter((f) => /^flight-F\d+.*\.md$/.test(f));
|
|
81
|
+
} catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
if (!names.length) return null;
|
|
85
|
+
const parse = (name) => {
|
|
86
|
+
const m = name.match(/^flight-F(\d+)/);
|
|
87
|
+
const dateM = name.match(/(\d{4}-\d{2}-\d{2})/);
|
|
88
|
+
return { name, num: m ? Number(m[1]) : -1, date: dateM ? dateM[1] : "" };
|
|
89
|
+
};
|
|
90
|
+
const newest = names
|
|
91
|
+
.map(parse)
|
|
92
|
+
.sort((a, b) => (b.num - a.num) || b.date.localeCompare(a.date))[0];
|
|
93
|
+
const file = path.join(dir, newest.name);
|
|
94
|
+
let id = `F${newest.num}`, title = "", flightStatus = "";
|
|
95
|
+
try {
|
|
96
|
+
const text = fs.readFileSync(file, "utf8");
|
|
97
|
+
const fm = text.match(/^---\n([\s\S]*?)\n---/);
|
|
98
|
+
const block = fm ? fm[1] : text;
|
|
99
|
+
const idM = block.match(/^id:\s*(.+)$/m);
|
|
100
|
+
const titleM = block.match(/^title:\s*(.+)$/m);
|
|
101
|
+
const statusM = block.match(/^status:\s*(.+)$/m);
|
|
102
|
+
if (idM) id = idM[1].trim().replace(/^["']|["']$/g, "");
|
|
103
|
+
if (titleM) title = titleM[1].trim().replace(/^["']|["']$/g, "");
|
|
104
|
+
if (statusM) flightStatus = statusM[1].trim().replace(/^["']|["']$/g, "");
|
|
105
|
+
} catch {}
|
|
106
|
+
return { id, title, status: flightStatus, file: path.relative(target, file) };
|
|
23
107
|
}
|
|
24
108
|
|
|
25
|
-
|
|
109
|
+
// Drift signal: roadmap.json is generated from ROADMAP.md. If the source is
|
|
110
|
+
// newer than the generated artifact, the JSON is stale and should be regenerated.
|
|
111
|
+
function collectRoadmapDrift(target) {
|
|
112
|
+
const out = { detected: false, reason: "" };
|
|
113
|
+
const jsonPath = path.join(target, "ai", "meta", "manifest", "roadmap.json");
|
|
114
|
+
const mdPath = path.join(target, "ROADMAP.md");
|
|
115
|
+
let jsonMtime, mdMtime;
|
|
116
|
+
try { jsonMtime = fs.statSync(jsonPath).mtimeMs; } catch { return out; }
|
|
117
|
+
try { mdMtime = fs.statSync(mdPath).mtimeMs; } catch { return out; }
|
|
118
|
+
// Prefer an explicit updated_at/generated_at in the JSON over file mtime when present.
|
|
119
|
+
const j = loadJson(jsonPath);
|
|
120
|
+
const stamp = j && (j.updated_at || j.generated_at);
|
|
121
|
+
const jsonTime = stamp ? Date.parse(stamp) : NaN;
|
|
122
|
+
const jsonEffective = Number.isFinite(jsonTime) ? jsonTime : jsonMtime;
|
|
123
|
+
if (mdMtime > jsonEffective) {
|
|
124
|
+
out.detected = true;
|
|
125
|
+
out.reason = "ROADMAP.md is newer than roadmap.json — run: python3 scripts/roadmap_to_json.py";
|
|
126
|
+
}
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function collectStatusPayload(target, options = {}) {
|
|
26
131
|
const ai = path.join(target, "ai");
|
|
27
|
-
const
|
|
132
|
+
const baseIdentity = shared.buildProjectIdentity(target, shared.collectMetadata(target));
|
|
28
133
|
const binding = loadProjectBinding(target);
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
134
|
+
const identity = shared.applyBoundProjectIdentity(target, baseIdentity, binding);
|
|
135
|
+
// A binding whose stored target no longer matches this path (the project was
|
|
136
|
+
// moved/copied) must not report "bound" — the identity already falls back to
|
|
137
|
+
// the path-derived one, so the status must say "moved" and prompt a re-bind,
|
|
138
|
+
// not silently claim a stale project_id/name (#4363).
|
|
139
|
+
const bindingHasTarget =
|
|
140
|
+
!!binding &&
|
|
141
|
+
!!(binding.target || binding.current_target || (Array.isArray(binding.target_aliases) && binding.target_aliases.length));
|
|
142
|
+
const boundButMoved =
|
|
143
|
+
!!binding &&
|
|
144
|
+
binding.binding_status === "bound" &&
|
|
145
|
+
bindingHasTarget &&
|
|
146
|
+
!shared.bindingTargetMatches(target, binding);
|
|
147
|
+
const bindingStatus = boundButMoved ? "moved" : ((binding && binding.binding_status) || "local-only");
|
|
148
|
+
const bindingReason = boundButMoved
|
|
149
|
+
? "binding target does not match this path — project moved or copied; re-bind to refresh identity"
|
|
150
|
+
: binding
|
|
151
|
+
? (binding.binding_status ? "" : "project-binding.json has no binding_status")
|
|
152
|
+
: "no .0dai/project-binding.json";
|
|
33
153
|
const bindingNextAction = bindingStatus === "bound" ? "" : "run 0dai project bind";
|
|
34
154
|
let v = "?", stack = "?";
|
|
35
155
|
try { v = fs.readFileSync(path.join(ai, "VERSION"), "utf8").trim(); } catch {}
|
|
@@ -64,7 +184,7 @@ function collectStatusPayload(target) {
|
|
|
64
184
|
} catch {}
|
|
65
185
|
|
|
66
186
|
try {
|
|
67
|
-
const ds =
|
|
187
|
+
const ds = shared.resolvePythonScript(target, "drift_detector.py");
|
|
68
188
|
if (ds) {
|
|
69
189
|
const dr = spawnSync("python3", [ds, "report", "--target", target],
|
|
70
190
|
{ stdio: ["ignore", "pipe", "ignore"], encoding: "utf8", timeout: 5000 });
|
|
@@ -93,7 +213,7 @@ function collectStatusPayload(target) {
|
|
|
93
213
|
queued: q,
|
|
94
214
|
active: a,
|
|
95
215
|
done: d,
|
|
96
|
-
quota_state: quota.
|
|
216
|
+
quota_state: Number(quota.daily_limit || 0) === 0 ? "locked" : "available",
|
|
97
217
|
quota_plan: quota.plan,
|
|
98
218
|
used_today: Number(quota.used_today || 0),
|
|
99
219
|
daily_limit: Number(quota.daily_limit || 0),
|
|
@@ -120,6 +240,10 @@ function collectStatusPayload(target) {
|
|
|
120
240
|
drift: {
|
|
121
241
|
detected: driftDetected,
|
|
122
242
|
},
|
|
243
|
+
sprint: collectSprintPayload(target, options),
|
|
244
|
+
flight: findActiveFlight(target),
|
|
245
|
+
roadmap_drift: collectRoadmapDrift(target),
|
|
246
|
+
mcp_exposure: collectMcpExposurePayload(target, options.mcpExposureOptions || {}),
|
|
123
247
|
warnings: warningCount,
|
|
124
248
|
activation_ttfv: getActivationDurationStats(target),
|
|
125
249
|
activation_first_merged_pr: getActivationMergedPrStats(target),
|
|
@@ -127,7 +251,7 @@ function collectStatusPayload(target) {
|
|
|
127
251
|
}
|
|
128
252
|
|
|
129
253
|
function cmdStatus(target, options = {}) {
|
|
130
|
-
const payload = collectStatusPayload(target);
|
|
254
|
+
const payload = collectStatusPayload(target, options);
|
|
131
255
|
if (options.json) {
|
|
132
256
|
console.log(JSON.stringify(payload, null, 2));
|
|
133
257
|
return payload;
|
|
@@ -139,6 +263,33 @@ function cmdStatus(target, options = {}) {
|
|
|
139
263
|
console.log(` swarm: ${payload.swarm.queued} queued, ${payload.swarm.active} active, ${payload.swarm.done} done`);
|
|
140
264
|
}
|
|
141
265
|
|
|
266
|
+
const sprint = payload.sprint || {};
|
|
267
|
+
if (sprint.authenticated) {
|
|
268
|
+
if (sprint.issues.length) {
|
|
269
|
+
console.log(` sprint:current — ${sprint.issues.length} open:`);
|
|
270
|
+
for (const i of sprint.issues.slice(0, 8)) {
|
|
271
|
+
console.log(` #${i.number} ${i.title}`);
|
|
272
|
+
}
|
|
273
|
+
if (sprint.issues.length > 8) {
|
|
274
|
+
console.log(` …and ${sprint.issues.length - 8} more`);
|
|
275
|
+
}
|
|
276
|
+
} else {
|
|
277
|
+
console.log(` sprint:current — none open`);
|
|
278
|
+
}
|
|
279
|
+
} else if (sprint.reason) {
|
|
280
|
+
console.log(` sprint:current — ${D}unavailable (${sprint.reason})${R}`);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (payload.flight) {
|
|
284
|
+
const ftitle = payload.flight.title ? ` ${payload.flight.title}` : "";
|
|
285
|
+
const fstatus = payload.flight.status ? ` [${payload.flight.status}]` : "";
|
|
286
|
+
console.log(` flight: ${payload.flight.id}${ftitle}${fstatus}`);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (payload.roadmap_drift && payload.roadmap_drift.detected) {
|
|
290
|
+
console.log(` roadmap: ${D}${payload.roadmap_drift.reason}${R}`);
|
|
291
|
+
}
|
|
292
|
+
|
|
142
293
|
if (payload.swarm.quota_state === "locked") {
|
|
143
294
|
console.log(` swarm quota: ${D}locked (Free) — upgrade for ${payload.swarm.daily_limit} tasks/day${R}`);
|
|
144
295
|
} else {
|
|
@@ -151,6 +302,14 @@ function cmdStatus(target, options = {}) {
|
|
|
151
302
|
console.log(` session roaming: ${T}available (${payload.session.roaming_plan})${R}`);
|
|
152
303
|
}
|
|
153
304
|
|
|
305
|
+
const mcpExposure = payload.mcp_exposure || {};
|
|
306
|
+
if (mcpExposure.status && mcpExposure.status !== "green") {
|
|
307
|
+
console.log(` mcp exposure: ${formatMcpExposureLine(mcpExposure)}`);
|
|
308
|
+
if (mcpExposure.next_step) console.log(` → ${mcpExposure.next_step}`);
|
|
309
|
+
} else if (mcpExposure.status === "green") {
|
|
310
|
+
console.log(` mcp exposure: ${formatMcpExposureLine(mcpExposure)}`);
|
|
311
|
+
}
|
|
312
|
+
|
|
154
313
|
if (payload.session.name) {
|
|
155
314
|
console.log(` session: ${payload.session.name} (agent: ${payload.session.agent || "?"})`);
|
|
156
315
|
}
|
|
@@ -170,4 +329,10 @@ function cmdStatus(target, options = {}) {
|
|
|
170
329
|
return payload;
|
|
171
330
|
}
|
|
172
331
|
|
|
173
|
-
module.exports = {
|
|
332
|
+
module.exports = {
|
|
333
|
+
cmdStatus,
|
|
334
|
+
collectStatusPayload,
|
|
335
|
+
collectSprintPayload,
|
|
336
|
+
findActiveFlight,
|
|
337
|
+
collectRoadmapDrift,
|
|
338
|
+
};
|
package/lib/commands/swarm.js
CHANGED
|
@@ -2,32 +2,121 @@
|
|
|
2
2
|
const shared = require("../shared");
|
|
3
3
|
const { log, T, R, D, fs, path, https, requirePlan, spawnSync, findRepoScript } = shared;
|
|
4
4
|
|
|
5
|
+
// The npm package ships the lightweight swarm client; the Python orchestration
|
|
6
|
+
// behind pick/run/quality/sessions runs from a 0dai repo checkout or dev install
|
|
7
|
+
// (it needs the full toolchain + heavier deps). Emit an honest, actionable line
|
|
8
|
+
// instead of dev-internal "probed: <path>" noise that npm users can't act on.
|
|
9
|
+
function logSwarmOrchestrationUnavailable(capability, envVar) {
|
|
10
|
+
log(`${capability} is not available in the npm package`);
|
|
11
|
+
console.log(` ${D}It runs from a 0dai repo checkout with the full Python toolchain.${R}`);
|
|
12
|
+
console.log(` ${D}You need: the repo checkout + python3 + an installed agent CLI (claude/codex/...) + the gh CLI.${R}`);
|
|
13
|
+
console.log(` ${D}Run 0dai doctor to see which prerequisites are missing.${R}`);
|
|
14
|
+
const power = envVar ? ` · power users: point ${envVar} at a local helper` : "";
|
|
15
|
+
console.log(` ${D}Docs: https://0dai.dev/docs${power}${R}`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function printSwarmHelp() {
|
|
19
|
+
console.log("Usage: 0dai swarm [status|pick|run|budget|estimate|quality|sessions|swarm-run]");
|
|
20
|
+
console.log(" 0dai swarm add --task '...' [--for agent]");
|
|
21
|
+
console.log(" 0dai swarm delegate --task '...' [--to agent]");
|
|
22
|
+
console.log(" pick Pick one queued task for this agent: 0dai swarm pick --agent codex");
|
|
23
|
+
console.log(" run One-shot: 0dai swarm run \"<goal>\" creates a task and dispatches it (queue-drains if no goal)");
|
|
24
|
+
console.log(" sessions Show live session registry table (idle/busy/stale per session) [--json]");
|
|
25
|
+
console.log(" swarm-run Repo-checkout helper: add, dispatch, and wait for one swarm task as JSON");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function printSwarmRunHelp() {
|
|
29
|
+
console.log("Usage: 0dai swarm swarm-run --task '...' [--agent codex] [--files path ...] [--json]");
|
|
30
|
+
console.log("");
|
|
31
|
+
console.log("Adds, dispatches, and waits for one swarm task through scripts/swarm_run.py.");
|
|
32
|
+
console.log("Requires a full repo checkout helper or ODAI_SWARM_RUN_SCRIPT; otherwise use queue-only swarm commands.");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function swarmRunAvailability(target) {
|
|
36
|
+
const override = process.env.ODAI_SWARM_RUN_SCRIPT || "";
|
|
37
|
+
if (override) {
|
|
38
|
+
if (fs.existsSync(override)) return { available: true, source: "ODAI_SWARM_RUN_SCRIPT", path: override };
|
|
39
|
+
return {
|
|
40
|
+
available: false,
|
|
41
|
+
source: "ODAI_SWARM_RUN_SCRIPT",
|
|
42
|
+
path: override,
|
|
43
|
+
reason: "override path does not exist",
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const script = findRepoScript(target, "swarm_run.py");
|
|
47
|
+
if (script) return { available: true, source: "auto", path: script };
|
|
48
|
+
return {
|
|
49
|
+
available: false,
|
|
50
|
+
source: "auto",
|
|
51
|
+
reason: "swarm_run.py not found",
|
|
52
|
+
probed: [
|
|
53
|
+
path.join(target, "scripts", "swarm_run.py"),
|
|
54
|
+
path.join(process.cwd(), "scripts", "swarm_run.py"),
|
|
55
|
+
],
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function printSwarmRunnerHint(target) {
|
|
60
|
+
const runner = swarmRunAvailability(target);
|
|
61
|
+
if (runner.available) {
|
|
62
|
+
console.log(` ${D}runner: available (${runner.path})${R}`);
|
|
63
|
+
return runner;
|
|
64
|
+
}
|
|
65
|
+
console.log(` ${D}runner: queue-only (${runner.reason}; set ODAI_SWARM_RUN_SCRIPT or sync scripts/swarm_run.py)${R}`);
|
|
66
|
+
return runner;
|
|
67
|
+
}
|
|
68
|
+
|
|
5
69
|
function cmdSwarm(target, sub, args) {
|
|
6
70
|
const swarmDir = path.join(target, "ai", "swarm");
|
|
7
71
|
const queueDir = path.join(swarmDir, "queue");
|
|
8
72
|
|
|
73
|
+
if (!sub || sub === "help" || sub === "--help" || sub === "-h") {
|
|
74
|
+
printSwarmHelp();
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (sub === "pick") {
|
|
79
|
+
cmdPythonSwarm(target, "pick", args.slice(2));
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
9
82
|
if (sub === "swarm-run") {
|
|
10
83
|
cmdSwarmRun(target, args.slice(2));
|
|
11
84
|
return;
|
|
12
85
|
}
|
|
86
|
+
if (sub === "run") {
|
|
87
|
+
cmdRun(target, args.slice(2));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
13
90
|
if (sub === "status") {
|
|
14
91
|
const count = (d) => { try { return fs.readdirSync(d).filter(f => f.endsWith(".json")).length; } catch { return 0; } };
|
|
15
92
|
const q = count(path.join(swarmDir, "queue"));
|
|
16
93
|
const a = count(path.join(swarmDir, "active"));
|
|
17
94
|
const d = count(path.join(swarmDir, "done"));
|
|
18
95
|
log(`swarm: ${q} queued, ${a} active, ${d} done`);
|
|
96
|
+
printSwarmRunnerHint(target);
|
|
19
97
|
return;
|
|
20
98
|
}
|
|
21
99
|
if (sub === "add" || sub === "delegate") {
|
|
100
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
101
|
+
printSwarmHelp();
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const taskIndex = args.indexOf("--task");
|
|
105
|
+
const task = taskIndex >= 0 ? args[taskIndex + 1] : "";
|
|
106
|
+
if (!task || task.startsWith("--")) {
|
|
107
|
+
log(`Usage: 0dai swarm ${sub} --task '...' [--for agent]`);
|
|
108
|
+
process.exitCode = 1;
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
22
111
|
const gate = requirePlan("pro", "Swarm", target);
|
|
23
112
|
if (gate) { log(gate.error); log(gate.hint); return; }
|
|
24
113
|
fs.mkdirSync(queueDir, { recursive: true });
|
|
25
|
-
const task = args.find((_, i) => args[i - 1] === "--task") || "untitled";
|
|
26
114
|
const forAgent = args.find((_, i) => ["--for", "--to"].includes(args[i - 1])) || "any";
|
|
27
115
|
const id = `swarm-${Date.now()}`;
|
|
28
116
|
const t = { id, title: task, assigned_to: forAgent, status: "pending", created_at: new Date().toISOString(), created_by: "cli" };
|
|
29
117
|
fs.writeFileSync(path.join(queueDir, `${id}.json`), JSON.stringify(t, null, 2));
|
|
30
118
|
log(`task created: ${id} → ${forAgent}`);
|
|
119
|
+
printSwarmRunnerHint(target);
|
|
31
120
|
return;
|
|
32
121
|
}
|
|
33
122
|
if (sub === "webhook") {
|
|
@@ -185,7 +274,7 @@ function cmdSwarm(target, sub, args) {
|
|
|
185
274
|
}
|
|
186
275
|
if (sub === "quality") {
|
|
187
276
|
const scorerScript = findRepoScript(target, "quality_scorer.py");
|
|
188
|
-
if (!scorerScript) {
|
|
277
|
+
if (!scorerScript) { logSwarmOrchestrationUnavailable("swarm quality scoring"); return; }
|
|
189
278
|
const fwd = [scorerScript, "--target", target];
|
|
190
279
|
if (args.includes("--json")) fwd.push("--json");
|
|
191
280
|
for (let i = 2; i < args.length; i++) {
|
|
@@ -198,7 +287,7 @@ function cmdSwarm(target, sub, args) {
|
|
|
198
287
|
}
|
|
199
288
|
if (sub === "sessions") {
|
|
200
289
|
const script = findRepoScript(target, "swarm_session_registry.py");
|
|
201
|
-
if (!script) {
|
|
290
|
+
if (!script) { logSwarmOrchestrationUnavailable("the swarm session registry"); return; }
|
|
202
291
|
const fwd = [script, "rebuild", "--stale-after", "600"];
|
|
203
292
|
const asJson = args.includes("--json");
|
|
204
293
|
if (!asJson) fwd.push("--write", path.join(swarmDir, "session_registry.json"));
|
|
@@ -250,12 +339,44 @@ function cmdSwarm(target, sub, args) {
|
|
|
250
339
|
console.log(` ${G2}idle${reset}: ${idle} ${W2}busy${reset}: ${busy} ${M2}stale${reset}: ${stale} total: ${rows.length}\n`);
|
|
251
340
|
return;
|
|
252
341
|
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
342
|
+
printSwarmHelp();
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function cmdPythonSwarm(target, subcommand, args) {
|
|
346
|
+
// Forward Python-backed swarm operations through scripts/swarm.py so quota,
|
|
347
|
+
// dispatch gates, pick semantics, and preflight messaging have one source of
|
|
348
|
+
// truth. `swarm-run` (hyphenated add+wait helper) stays on swarm_run.py.
|
|
349
|
+
let script = process.env.ODAI_SWARM_SCRIPT || "";
|
|
350
|
+
if (script && !fs.existsSync(script)) {
|
|
351
|
+
log(`swarm helper unavailable: ODAI_SWARM_SCRIPT=${script} does not exist`);
|
|
352
|
+
process.exit(1);
|
|
353
|
+
}
|
|
354
|
+
if (!script) script = findRepoScript(target, "swarm.py");
|
|
355
|
+
if (!script) {
|
|
356
|
+
logSwarmOrchestrationUnavailable("swarm orchestration", "ODAI_SWARM_SCRIPT");
|
|
357
|
+
process.exit(1);
|
|
358
|
+
}
|
|
359
|
+
const python = process.env.ODAI_SWARM_RUN_PYTHON || "python3";
|
|
360
|
+
const result = spawnSync(python, [script, "--target", target, subcommand, ...args], { stdio: "inherit" });
|
|
361
|
+
if (result.error) {
|
|
362
|
+
log(`swarm ${subcommand} failed: ${result.error.message}`);
|
|
363
|
+
process.exit(1);
|
|
364
|
+
}
|
|
365
|
+
if (typeof result.status === "number" && result.status !== 0) process.exit(result.status);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function cmdRun(target, args) {
|
|
369
|
+
// `0dai swarm run "<goal>"` — one-shot: forward to the python swarm.py `run`
|
|
370
|
+
// subcommand so the per-tier daily quota (check_swarm_quota / record_swarm_task)
|
|
371
|
+
// governs identically for the goal path and the queue-drain.
|
|
372
|
+
cmdPythonSwarm(target, "run", args);
|
|
256
373
|
}
|
|
257
374
|
|
|
258
375
|
function cmdSwarmRun(target, args) {
|
|
376
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
377
|
+
printSwarmRunHelp();
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
259
380
|
// #2143 hardening:
|
|
260
381
|
// 1. `ODAI_SWARM_RUN_SCRIPT` env override lets tests inject a deterministic
|
|
261
382
|
// script path, bypassing the `findRepoScript` heuristic that walks up
|
|
@@ -269,15 +390,12 @@ function cmdSwarmRun(target, args) {
|
|
|
269
390
|
let script = process.env.ODAI_SWARM_RUN_SCRIPT || "";
|
|
270
391
|
if (script && !fs.existsSync(script)) {
|
|
271
392
|
log(`swarm-run helper unavailable: ODAI_SWARM_RUN_SCRIPT=${script} does not exist`);
|
|
393
|
+
log(` hint: set ODAI_SWARM_RUN_SCRIPT to a valid helper, unset it, or create the helper at that exact path`);
|
|
272
394
|
process.exit(1);
|
|
273
395
|
}
|
|
274
396
|
if (!script) script = findRepoScript(target, "swarm_run.py");
|
|
275
397
|
if (!script) {
|
|
276
|
-
|
|
277
|
-
log("swarm-run helper unavailable");
|
|
278
|
-
log(` probed: ${path.join(target, "scripts", "swarm_run.py")}`);
|
|
279
|
-
log(` probed: ${path.join(cwd, "scripts", "swarm_run.py")}`);
|
|
280
|
-
log(` hint: set ODAI_SWARM_RUN_SCRIPT to override the lookup`);
|
|
398
|
+
logSwarmOrchestrationUnavailable("the swarm-run helper", "ODAI_SWARM_RUN_SCRIPT");
|
|
281
399
|
process.exit(1);
|
|
282
400
|
}
|
|
283
401
|
const python = process.env.ODAI_SWARM_RUN_PYTHON || "python3";
|
|
@@ -289,4 +407,4 @@ function cmdSwarmRun(target, args) {
|
|
|
289
407
|
if (typeof result.status === "number" && result.status !== 0) process.exit(result.status);
|
|
290
408
|
}
|
|
291
409
|
|
|
292
|
-
module.exports = { cmdSwarm, cmdSwarmRun };
|
|
410
|
+
module.exports = { cmdSwarm, cmdSwarmRun, cmdRun, cmdPythonSwarm, swarmRunAvailability };
|
package/lib/commands/trust.js
CHANGED