speexor 0.2.0 → 0.2.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/BENCHMARKS.md +21 -0
- package/CHANGELOG.md +24 -0
- package/CONTRIBUTING.md +5 -10
- package/FAQ.md +28 -1
- package/GLOSSARY.md +1 -0
- package/PUBLISH.md +7 -2
- package/README.md +2 -2
- package/ROADMAP.md +29 -62
- package/SECURITY.md +2 -1
- package/SUMMARY.md +1 -1
- package/dist/{agent-D4BRWEOZ.js → agent-C64T66XT.js} +4 -4
- package/dist/{agent-D4BRWEOZ.js.map → agent-C64T66XT.js.map} +1 -1
- package/dist/{chunk-7VZHDGRQ.js → chunk-5OD5UWB5.js} +322 -121
- package/dist/chunk-5OD5UWB5.js.map +1 -0
- package/dist/chunk-GOGI3JQD.js +1637 -0
- package/dist/chunk-GOGI3JQD.js.map +1 -0
- package/dist/{chunk-2DX54KIM.js → chunk-VEZQT5SX.js} +80 -8
- package/dist/chunk-VEZQT5SX.js.map +1 -0
- package/dist/cli/index.js +2058 -18
- package/dist/cli/index.js.map +1 -1
- package/dist/core/index.d.ts +682 -3
- package/dist/core/index.js +1 -1
- package/dist/index.d.ts +102 -14
- package/dist/index.js +55 -29
- package/dist/index.js.map +1 -1
- package/dist/plugins/index.d.ts +1 -1
- package/dist/plugins/index.js +1 -1
- package/dist/types-BOMap-tI.d.ts +389 -0
- package/docs/PRD03.md +119 -0
- package/docs/PRD06.md +125 -0
- package/package.json +3 -3
- package/dist/chunk-2DX54KIM.js.map +0 -1
- package/dist/chunk-7VZHDGRQ.js.map +0 -1
- package/dist/chunk-AOFWQZWY.js +0 -345
- package/dist/chunk-AOFWQZWY.js.map +0 -1
- package/dist/types-0q_okI2g.d.ts +0 -205
package/dist/cli/index.js
CHANGED
|
@@ -1,20 +1,29 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { createDashboardServer } from '../chunk-
|
|
3
|
-
import { generateDefaultConfig, loadConfig, SpeexorLifecycle } from '../chunk-
|
|
4
|
-
import { loadAllPlugins } from '../chunk-
|
|
2
|
+
import { createDashboardServer } from '../chunk-GOGI3JQD.js';
|
|
3
|
+
import { generateDefaultConfig, loadConfig, SpeexorLifecycle, validateConfig } from '../chunk-VEZQT5SX.js';
|
|
4
|
+
import { loadAllPlugins } from '../chunk-5OD5UWB5.js';
|
|
5
5
|
import { Command } from 'commander';
|
|
6
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
7
|
-
import { dirname, join } from 'path';
|
|
8
|
-
import {
|
|
6
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync, rmSync, appendFileSync } from 'fs';
|
|
7
|
+
import { dirname, join, resolve } from 'path';
|
|
8
|
+
import { execSync } from 'child_process';
|
|
9
|
+
import { stringify, parse } from 'yaml';
|
|
9
10
|
import Debug from 'debug';
|
|
10
11
|
import ora from 'ora';
|
|
11
12
|
import chalk4 from 'chalk';
|
|
13
|
+
import { execa } from 'execa';
|
|
14
|
+
import { homedir } from 'os';
|
|
15
|
+
import { createHash } from 'crypto';
|
|
12
16
|
import { fileURLToPath } from 'url';
|
|
13
17
|
|
|
14
18
|
var debug = Debug("speexor:start");
|
|
15
19
|
async function startCommand(repo, options = {}) {
|
|
16
20
|
const cwd = process.cwd();
|
|
17
|
-
const port = parseInt(options.port ?? "
|
|
21
|
+
const port = parseInt(options.port ?? "7777", 10);
|
|
22
|
+
const ensureDir = (dir) => {
|
|
23
|
+
if (!existsSync(dir)) {
|
|
24
|
+
mkdirSync(dir, { recursive: true });
|
|
25
|
+
}
|
|
26
|
+
};
|
|
18
27
|
if (repo) {
|
|
19
28
|
const spinner = ora("Initializing Speexor project...").start();
|
|
20
29
|
const config = generateDefaultConfig(repo, options.name);
|
|
@@ -26,15 +35,27 @@ async function startCommand(repo, options = {}) {
|
|
|
26
35
|
writeFileSync(configPath, yaml, "utf-8");
|
|
27
36
|
spinner.succeed(`Created speexor.config.yaml at ${configPath}`);
|
|
28
37
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
38
|
+
ensureDir(join(cwd, ".speexor", "worktrees"));
|
|
39
|
+
ensureDir(join(cwd, ".speexor", "logs"));
|
|
40
|
+
spinner.succeed("Project initialized");
|
|
41
|
+
} else {
|
|
42
|
+
let repoUrl;
|
|
43
|
+
try {
|
|
44
|
+
repoUrl = execSync("git remote get-url origin", { encoding: "utf-8", cwd }).trim();
|
|
45
|
+
} catch {
|
|
32
46
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
47
|
+
if (repoUrl) {
|
|
48
|
+
const configPath = join(cwd, "speexor.config.yaml");
|
|
49
|
+
if (!existsSync(configPath)) {
|
|
50
|
+
const spinner = ora("Detected git repo \u2014 auto-generating config...").start();
|
|
51
|
+
const config = generateDefaultConfig(repoUrl, options.name);
|
|
52
|
+
const yaml = stringify(config, { indent: 2 });
|
|
53
|
+
writeFileSync(configPath, yaml, "utf-8");
|
|
54
|
+
ensureDir(join(cwd, ".speexor", "worktrees"));
|
|
55
|
+
ensureDir(join(cwd, ".speexor", "logs"));
|
|
56
|
+
spinner.succeed(`Created speexor.config.yaml at ${configPath}`);
|
|
57
|
+
}
|
|
36
58
|
}
|
|
37
|
-
spinner.succeed("Project initialized");
|
|
38
59
|
}
|
|
39
60
|
try {
|
|
40
61
|
const config = loadConfig(cwd);
|
|
@@ -70,7 +91,7 @@ async function startCommand(repo, options = {}) {
|
|
|
70
91
|
\u2716 Error: ${error.message}
|
|
71
92
|
`);
|
|
72
93
|
if (!repo) {
|
|
73
|
-
console.log(" Tip: Run `speexor start <repo-url>`
|
|
94
|
+
console.log(" Tip: Run `speexor start <repo-url>` to initialize a project, or run from a git repository.\n");
|
|
74
95
|
}
|
|
75
96
|
}
|
|
76
97
|
process.exit(1);
|
|
@@ -239,7 +260,7 @@ projects:
|
|
|
239
260
|
auto: false
|
|
240
261
|
action: notify
|
|
241
262
|
retries: 0
|
|
242
|
-
escalateAfter:
|
|
263
|
+
escalateAfter: 1
|
|
243
264
|
`)}
|
|
244
265
|
|
|
245
266
|
${chalk4.bold("Commands:")}
|
|
@@ -257,6 +278,2009 @@ ${chalk4.bold("Commands:")}
|
|
|
257
278
|
function configHelpCommand() {
|
|
258
279
|
console.log(CONFIG_HELP);
|
|
259
280
|
}
|
|
281
|
+
var debug2 = Debug("speexor:task-graph:store");
|
|
282
|
+
var TaskGraphStore = class _TaskGraphStore {
|
|
283
|
+
db;
|
|
284
|
+
dbPath;
|
|
285
|
+
constructor(db, dbPath) {
|
|
286
|
+
this.db = db;
|
|
287
|
+
this.dbPath = dbPath;
|
|
288
|
+
}
|
|
289
|
+
static async create(dbPath) {
|
|
290
|
+
const { default: initSqlJs } = await import('sql.js');
|
|
291
|
+
const SQL = await initSqlJs();
|
|
292
|
+
const dir = dbPath ?? join(process.cwd(), ".speexor");
|
|
293
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
294
|
+
const fullPath = join(dir, "task-graph.sqlite");
|
|
295
|
+
let db;
|
|
296
|
+
if (existsSync(fullPath)) {
|
|
297
|
+
const buffer = readFileSync(fullPath);
|
|
298
|
+
db = new SQL.Database(buffer);
|
|
299
|
+
} else {
|
|
300
|
+
db = new SQL.Database();
|
|
301
|
+
}
|
|
302
|
+
const store = new _TaskGraphStore(db, fullPath);
|
|
303
|
+
store.initialize();
|
|
304
|
+
return store;
|
|
305
|
+
}
|
|
306
|
+
initialize() {
|
|
307
|
+
this.db.exec(`
|
|
308
|
+
CREATE TABLE IF NOT EXISTS task_graphs (
|
|
309
|
+
id TEXT PRIMARY KEY,
|
|
310
|
+
root_task_id TEXT NOT NULL,
|
|
311
|
+
project_name TEXT NOT NULL,
|
|
312
|
+
status TEXT NOT NULL DEFAULT 'decomposing',
|
|
313
|
+
created_at TEXT NOT NULL,
|
|
314
|
+
updated_at TEXT NOT NULL
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
CREATE TABLE IF NOT EXISTS task_nodes (
|
|
318
|
+
id TEXT PRIMARY KEY,
|
|
319
|
+
graph_id TEXT NOT NULL,
|
|
320
|
+
title TEXT NOT NULL,
|
|
321
|
+
description TEXT,
|
|
322
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
323
|
+
parent_id TEXT,
|
|
324
|
+
depth INTEGER NOT NULL DEFAULT 0,
|
|
325
|
+
depends_on TEXT DEFAULT '[]',
|
|
326
|
+
created_by TEXT NOT NULL DEFAULT 'user',
|
|
327
|
+
created_by_agent_id TEXT,
|
|
328
|
+
assigned_agent_id TEXT,
|
|
329
|
+
worktree_path TEXT,
|
|
330
|
+
skills_used TEXT DEFAULT '[]',
|
|
331
|
+
commands_executed TEXT DEFAULT '[]',
|
|
332
|
+
result_json TEXT,
|
|
333
|
+
proposed_by TEXT,
|
|
334
|
+
proposed_reason TEXT,
|
|
335
|
+
created_at TEXT NOT NULL,
|
|
336
|
+
updated_at TEXT NOT NULL,
|
|
337
|
+
FOREIGN KEY (graph_id) REFERENCES task_graphs(id)
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
CREATE INDEX IF NOT EXISTS idx_nodes_graph ON task_nodes(graph_id);
|
|
341
|
+
CREATE INDEX IF NOT EXISTS idx_nodes_status ON task_nodes(status);
|
|
342
|
+
CREATE INDEX IF NOT EXISTS idx_nodes_parent ON task_nodes(parent_id);
|
|
343
|
+
`);
|
|
344
|
+
debug2("Task Graph SQLite store initialized");
|
|
345
|
+
}
|
|
346
|
+
save() {
|
|
347
|
+
const data = this.db.export();
|
|
348
|
+
writeFileSync(this.dbPath, Buffer.from(data));
|
|
349
|
+
}
|
|
350
|
+
queryOne(sql, params) {
|
|
351
|
+
const stmt = this.db.prepare(sql);
|
|
352
|
+
if (params) stmt.bind(params);
|
|
353
|
+
try {
|
|
354
|
+
if (stmt.step()) {
|
|
355
|
+
return stmt.getAsObject();
|
|
356
|
+
}
|
|
357
|
+
return void 0;
|
|
358
|
+
} finally {
|
|
359
|
+
stmt.free();
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
queryAll(sql, params) {
|
|
363
|
+
const stmt = this.db.prepare(sql);
|
|
364
|
+
if (params) stmt.bind(params);
|
|
365
|
+
const rows = [];
|
|
366
|
+
try {
|
|
367
|
+
while (stmt.step()) {
|
|
368
|
+
rows.push(stmt.getAsObject());
|
|
369
|
+
}
|
|
370
|
+
return rows;
|
|
371
|
+
} finally {
|
|
372
|
+
stmt.free();
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
createGraph(projectName, rootTask) {
|
|
376
|
+
const graph = {
|
|
377
|
+
id: `tg-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
|
|
378
|
+
rootTaskId: rootTask.id,
|
|
379
|
+
projectName,
|
|
380
|
+
nodes: /* @__PURE__ */ new Map([[rootTask.id, rootTask]]),
|
|
381
|
+
status: "decomposing",
|
|
382
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
383
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
384
|
+
};
|
|
385
|
+
this.db.run("BEGIN");
|
|
386
|
+
try {
|
|
387
|
+
this.db.run(`INSERT INTO task_graphs (id, root_task_id, project_name, status, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)`, [
|
|
388
|
+
graph.id,
|
|
389
|
+
graph.rootTaskId,
|
|
390
|
+
graph.projectName,
|
|
391
|
+
graph.status,
|
|
392
|
+
graph.createdAt.toISOString(),
|
|
393
|
+
graph.updatedAt.toISOString()
|
|
394
|
+
]);
|
|
395
|
+
this.insertNode(graph.id, rootTask);
|
|
396
|
+
this.db.run("COMMIT");
|
|
397
|
+
this.save();
|
|
398
|
+
} catch (e) {
|
|
399
|
+
this.db.run("ROLLBACK");
|
|
400
|
+
throw e;
|
|
401
|
+
}
|
|
402
|
+
return graph;
|
|
403
|
+
}
|
|
404
|
+
insertNode(graphId, node) {
|
|
405
|
+
this.db.run(
|
|
406
|
+
`INSERT OR REPLACE INTO task_nodes (id, graph_id, title, description, status, parent_id, depth, depends_on, created_by, created_by_agent_id, assigned_agent_id, worktree_path, skills_used, commands_executed, result_json, proposed_by, proposed_reason, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
407
|
+
[
|
|
408
|
+
node.id,
|
|
409
|
+
graphId,
|
|
410
|
+
node.title,
|
|
411
|
+
node.description,
|
|
412
|
+
node.status,
|
|
413
|
+
node.parentId,
|
|
414
|
+
node.depth,
|
|
415
|
+
JSON.stringify(node.dependsOn),
|
|
416
|
+
node.createdBy,
|
|
417
|
+
node.createdByAgentId ?? null,
|
|
418
|
+
node.assignedAgentId ?? null,
|
|
419
|
+
node.worktreePath ?? null,
|
|
420
|
+
JSON.stringify(node.skillsUsed),
|
|
421
|
+
JSON.stringify(node.commandsExecuted),
|
|
422
|
+
node.result ? JSON.stringify(node.result) : null,
|
|
423
|
+
node.proposedBy ?? null,
|
|
424
|
+
node.proposedReason ?? null,
|
|
425
|
+
node.createdAt.toISOString(),
|
|
426
|
+
node.updatedAt.toISOString()
|
|
427
|
+
]
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
getGraph(graphId) {
|
|
431
|
+
const graphRow = this.queryOne("SELECT * FROM task_graphs WHERE id = ?", [graphId]);
|
|
432
|
+
if (!graphRow) return null;
|
|
433
|
+
const nodeRows = this.queryAll("SELECT * FROM task_nodes WHERE graph_id = ?", [graphId]);
|
|
434
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
435
|
+
for (const row of nodeRows) {
|
|
436
|
+
nodes.set(row.id, this.rowToNode(row));
|
|
437
|
+
}
|
|
438
|
+
return {
|
|
439
|
+
id: graphRow.id,
|
|
440
|
+
rootTaskId: graphRow.root_task_id,
|
|
441
|
+
projectName: graphRow.project_name,
|
|
442
|
+
nodes,
|
|
443
|
+
status: graphRow.status,
|
|
444
|
+
createdAt: new Date(graphRow.created_at),
|
|
445
|
+
updatedAt: new Date(graphRow.updated_at)
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
rowToNode(row) {
|
|
449
|
+
return {
|
|
450
|
+
id: row.id,
|
|
451
|
+
title: row.title,
|
|
452
|
+
description: row.description ?? "",
|
|
453
|
+
status: row.status,
|
|
454
|
+
parentId: row.parent_id ?? null,
|
|
455
|
+
depth: Number.parseInt(row.depth),
|
|
456
|
+
dependsOn: JSON.parse(row.depends_on ?? "[]"),
|
|
457
|
+
createdBy: row.created_by,
|
|
458
|
+
createdByAgentId: row.created_by_agent_id ?? void 0,
|
|
459
|
+
assignedAgentId: row.assigned_agent_id ?? null,
|
|
460
|
+
worktreePath: row.worktree_path ?? null,
|
|
461
|
+
skillsUsed: JSON.parse(row.skills_used ?? "[]"),
|
|
462
|
+
commandsExecuted: JSON.parse(row.commands_executed ?? "[]"),
|
|
463
|
+
result: row.result_json ? JSON.parse(row.result_json) : void 0,
|
|
464
|
+
proposedBy: row.proposed_by ?? void 0,
|
|
465
|
+
proposedReason: row.proposed_reason ?? void 0,
|
|
466
|
+
createdAt: new Date(row.created_at),
|
|
467
|
+
updatedAt: new Date(row.updated_at)
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
updateNodeStatus(nodeId, status) {
|
|
471
|
+
this.db.run("UPDATE task_nodes SET status = ?, updated_at = ? WHERE id = ?", [status, (/* @__PURE__ */ new Date()).toISOString(), nodeId]);
|
|
472
|
+
this.save();
|
|
473
|
+
}
|
|
474
|
+
updateNodeResult(nodeId, result) {
|
|
475
|
+
this.db.run("UPDATE task_nodes SET result_json = ?, updated_at = ? WHERE id = ?", [
|
|
476
|
+
JSON.stringify(result),
|
|
477
|
+
(/* @__PURE__ */ new Date()).toISOString(),
|
|
478
|
+
nodeId
|
|
479
|
+
]);
|
|
480
|
+
this.save();
|
|
481
|
+
}
|
|
482
|
+
addNode(graphId, node) {
|
|
483
|
+
this.insertNode(graphId, node);
|
|
484
|
+
this.save();
|
|
485
|
+
}
|
|
486
|
+
getReadyNodes(graphId) {
|
|
487
|
+
const rows = this.queryAll("SELECT * FROM task_nodes WHERE graph_id = ? AND status = ?", [graphId, "ready"]);
|
|
488
|
+
return rows.map((r) => this.rowToNode(r));
|
|
489
|
+
}
|
|
490
|
+
getNodesByStatus(graphId, status) {
|
|
491
|
+
const rows = this.queryAll("SELECT * FROM task_nodes WHERE graph_id = ? AND status = ?", [graphId, status]);
|
|
492
|
+
return rows.map((r) => this.rowToNode(r));
|
|
493
|
+
}
|
|
494
|
+
updateGraphStatus(graphId, status) {
|
|
495
|
+
this.db.run("UPDATE task_graphs SET status = ?, updated_at = ? WHERE id = ?", [status, (/* @__PURE__ */ new Date()).toISOString(), graphId]);
|
|
496
|
+
this.save();
|
|
497
|
+
}
|
|
498
|
+
assignAgent(nodeId, agentId) {
|
|
499
|
+
this.db.run("UPDATE task_nodes SET assigned_agent_id = ?, updated_at = ? WHERE id = ?", [agentId, (/* @__PURE__ */ new Date()).toISOString(), nodeId]);
|
|
500
|
+
this.save();
|
|
501
|
+
}
|
|
502
|
+
close() {
|
|
503
|
+
this.save();
|
|
504
|
+
this.db.close();
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
// src/task-graph/graph.ts
|
|
509
|
+
var TaskGraphManager = class {
|
|
510
|
+
graph;
|
|
511
|
+
constructor(graph) {
|
|
512
|
+
this.graph = graph;
|
|
513
|
+
}
|
|
514
|
+
getGraph() {
|
|
515
|
+
return this.graph;
|
|
516
|
+
}
|
|
517
|
+
getNode(nodeId) {
|
|
518
|
+
return this.graph.nodes.get(nodeId);
|
|
519
|
+
}
|
|
520
|
+
addNode(node) {
|
|
521
|
+
this.graph.nodes.set(node.id, node);
|
|
522
|
+
this.graph.updatedAt = /* @__PURE__ */ new Date();
|
|
523
|
+
}
|
|
524
|
+
updateNodeStatus(nodeId, status) {
|
|
525
|
+
const node = this.graph.nodes.get(nodeId);
|
|
526
|
+
if (node) {
|
|
527
|
+
node.status = status;
|
|
528
|
+
node.updatedAt = /* @__PURE__ */ new Date();
|
|
529
|
+
this.graph.updatedAt = /* @__PURE__ */ new Date();
|
|
530
|
+
if (status === "failed") {
|
|
531
|
+
this.blockDependents(nodeId);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
blockDependents(nodeId) {
|
|
536
|
+
for (const [, node] of this.graph.nodes) {
|
|
537
|
+
if (node.dependsOn.includes(nodeId) && node.status === "pending") {
|
|
538
|
+
node.status = "blocked";
|
|
539
|
+
node.updatedAt = /* @__PURE__ */ new Date();
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
/** Get topologically sorted list of nodes */
|
|
544
|
+
topologicalSort() {
|
|
545
|
+
const visited = /* @__PURE__ */ new Set();
|
|
546
|
+
const result = [];
|
|
547
|
+
const visit = (nodeId) => {
|
|
548
|
+
if (visited.has(nodeId)) return;
|
|
549
|
+
visited.add(nodeId);
|
|
550
|
+
const node = this.graph.nodes.get(nodeId);
|
|
551
|
+
if (!node) return;
|
|
552
|
+
for (const depId of node.dependsOn) {
|
|
553
|
+
visit(depId);
|
|
554
|
+
}
|
|
555
|
+
result.push(node);
|
|
556
|
+
};
|
|
557
|
+
visit(this.graph.rootTaskId);
|
|
558
|
+
for (const [id] of this.graph.nodes) {
|
|
559
|
+
if (!visited.has(id)) visit(id);
|
|
560
|
+
}
|
|
561
|
+
return result;
|
|
562
|
+
}
|
|
563
|
+
/** Get nodes that are ready to execute (all dependencies done) */
|
|
564
|
+
getReadyNodes() {
|
|
565
|
+
const ready = [];
|
|
566
|
+
for (const [, node] of this.graph.nodes) {
|
|
567
|
+
if (node.status !== "pending") continue;
|
|
568
|
+
const allDepsDone = node.dependsOn.every((depId) => {
|
|
569
|
+
const dep = this.graph.nodes.get(depId);
|
|
570
|
+
return dep?.status === "done";
|
|
571
|
+
});
|
|
572
|
+
if (allDepsDone) {
|
|
573
|
+
ready.push(node);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
return ready;
|
|
577
|
+
}
|
|
578
|
+
/** Validate the DAG has no cycles */
|
|
579
|
+
validateDag() {
|
|
580
|
+
const visited = /* @__PURE__ */ new Set();
|
|
581
|
+
const recStack = /* @__PURE__ */ new Set();
|
|
582
|
+
const dfs = (nodeId, path) => {
|
|
583
|
+
visited.add(nodeId);
|
|
584
|
+
recStack.add(nodeId);
|
|
585
|
+
path.push(nodeId);
|
|
586
|
+
const node = this.graph.nodes.get(nodeId);
|
|
587
|
+
if (node) {
|
|
588
|
+
for (const depId of node.dependsOn) {
|
|
589
|
+
if (!visited.has(depId)) {
|
|
590
|
+
const result = dfs(depId, [...path]);
|
|
591
|
+
if (result) return result;
|
|
592
|
+
} else if (recStack.has(depId)) {
|
|
593
|
+
const cycleStart = path.indexOf(depId);
|
|
594
|
+
return path.slice(cycleStart);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
recStack.delete(nodeId);
|
|
599
|
+
return null;
|
|
600
|
+
};
|
|
601
|
+
for (const [id] of this.graph.nodes) {
|
|
602
|
+
if (!visited.has(id)) {
|
|
603
|
+
const cycle = dfs(id, []);
|
|
604
|
+
if (cycle) return { valid: false, cycle };
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
return { valid: true };
|
|
608
|
+
}
|
|
609
|
+
/** Get all leaf nodes (no dependents) */
|
|
610
|
+
getLeafNodes() {
|
|
611
|
+
const hasDependents = /* @__PURE__ */ new Set();
|
|
612
|
+
for (const [, node] of this.graph.nodes) {
|
|
613
|
+
for (const depId of node.dependsOn) {
|
|
614
|
+
hasDependents.add(depId);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
return Array.from(this.graph.nodes.values()).filter((n) => !hasDependents.has(n.id));
|
|
618
|
+
}
|
|
619
|
+
/** Get depth of a node from root */
|
|
620
|
+
getNodeDepth(nodeId) {
|
|
621
|
+
let depth = 0;
|
|
622
|
+
let current = this.graph.nodes.get(nodeId);
|
|
623
|
+
while (current?.parentId) {
|
|
624
|
+
depth++;
|
|
625
|
+
current = this.graph.nodes.get(current.parentId);
|
|
626
|
+
}
|
|
627
|
+
return depth;
|
|
628
|
+
}
|
|
629
|
+
/** Check if the graph is complete */
|
|
630
|
+
isComplete() {
|
|
631
|
+
for (const [, node] of this.graph.nodes) {
|
|
632
|
+
if (node.status !== "done" && node.status !== "failed") return false;
|
|
633
|
+
}
|
|
634
|
+
return true;
|
|
635
|
+
}
|
|
636
|
+
/** Check if all nodes are terminal (done or failed) */
|
|
637
|
+
allTerminal() {
|
|
638
|
+
for (const [, node] of this.graph.nodes) {
|
|
639
|
+
if (node.status !== "done" && node.status !== "failed") return false;
|
|
640
|
+
}
|
|
641
|
+
return true;
|
|
642
|
+
}
|
|
643
|
+
};
|
|
644
|
+
|
|
645
|
+
// src/planner/decomposer.ts
|
|
646
|
+
var TaskDecomposer = class {
|
|
647
|
+
config;
|
|
648
|
+
constructor(config) {
|
|
649
|
+
this.config = config;
|
|
650
|
+
}
|
|
651
|
+
createDecompositionPrompt(task) {
|
|
652
|
+
return `You are a task decomposition engine. Break down the following software engineering task into smaller, executable subtasks.
|
|
653
|
+
|
|
654
|
+
ROOT TASK: ${task.title}
|
|
655
|
+
DESCRIPTION: ${task.description}
|
|
656
|
+
|
|
657
|
+
RULES:
|
|
658
|
+
- Each subtask must be scoped to a single concern (e.g., "Create database schema", not "Build the feature")
|
|
659
|
+
- Maximum depth: ${this.config.maxTaskGraphDepth}
|
|
660
|
+
- Maximum nodes per graph: ${this.config.maxNodesPerGraph}
|
|
661
|
+
- Identify dependencies between subtasks (e.g., "schema" must be done before "API endpoints")
|
|
662
|
+
- Each subtask should be completable independently
|
|
663
|
+
|
|
664
|
+
OUTPUT FORMAT (JSON only):
|
|
665
|
+
{
|
|
666
|
+
"subtasks": [
|
|
667
|
+
{
|
|
668
|
+
"title": "Short task title",
|
|
669
|
+
"description": "What to do, acceptance criteria, files likely involved",
|
|
670
|
+
"dependsOn": ["title of task this depends on"]
|
|
671
|
+
}
|
|
672
|
+
]
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
Output ONLY valid JSON, no explanation.`;
|
|
676
|
+
}
|
|
677
|
+
parseDecompositionResponse(llmResponse) {
|
|
678
|
+
try {
|
|
679
|
+
let jsonStr = llmResponse.trim();
|
|
680
|
+
if (jsonStr.startsWith("```")) {
|
|
681
|
+
jsonStr = jsonStr.replace(/```(?:json)?\n?/g, "").trim();
|
|
682
|
+
}
|
|
683
|
+
const parsed = JSON.parse(jsonStr);
|
|
684
|
+
const subtasks = parsed.subtasks;
|
|
685
|
+
if (!Array.isArray(subtasks) || subtasks.length === 0) {
|
|
686
|
+
return {
|
|
687
|
+
subtasks: [],
|
|
688
|
+
isValid: false,
|
|
689
|
+
warnings: ["No subtasks found in response"]
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
if (subtasks.length > this.config.maxNodesPerGraph) {
|
|
693
|
+
return {
|
|
694
|
+
subtasks: subtasks.slice(0, this.config.maxNodesPerGraph),
|
|
695
|
+
isValid: false,
|
|
696
|
+
warnings: [
|
|
697
|
+
`Truncated to ${this.config.maxNodesPerGraph} subtasks (max)`
|
|
698
|
+
]
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
const warnings = [];
|
|
702
|
+
const titles = new Set(subtasks.map((s) => s.title));
|
|
703
|
+
for (const sub of subtasks) {
|
|
704
|
+
for (const dep of sub.dependsOn) {
|
|
705
|
+
if (!titles.has(dep)) {
|
|
706
|
+
warnings.push(
|
|
707
|
+
`Subtask "${sub.title}" depends on "${dep}" which doesn't exist`
|
|
708
|
+
);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
const graph = /* @__PURE__ */ new Map();
|
|
713
|
+
for (const sub of subtasks) {
|
|
714
|
+
graph.set(sub.title, sub.dependsOn);
|
|
715
|
+
}
|
|
716
|
+
const visited = /* @__PURE__ */ new Set();
|
|
717
|
+
const recStack = /* @__PURE__ */ new Set();
|
|
718
|
+
const hasCycle = (title) => {
|
|
719
|
+
if (recStack.has(title)) return true;
|
|
720
|
+
if (visited.has(title)) return false;
|
|
721
|
+
visited.add(title);
|
|
722
|
+
recStack.add(title);
|
|
723
|
+
const deps = graph.get(title) ?? [];
|
|
724
|
+
for (const dep of deps) {
|
|
725
|
+
if (hasCycle(dep)) return true;
|
|
726
|
+
}
|
|
727
|
+
recStack.delete(title);
|
|
728
|
+
return false;
|
|
729
|
+
};
|
|
730
|
+
for (const title of titles) {
|
|
731
|
+
if (hasCycle(title)) {
|
|
732
|
+
warnings.push("Circular dependency detected in decomposition");
|
|
733
|
+
break;
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
return { subtasks, isValid: warnings.length === 0, warnings };
|
|
737
|
+
} catch (error) {
|
|
738
|
+
return {
|
|
739
|
+
subtasks: [],
|
|
740
|
+
isValid: false,
|
|
741
|
+
warnings: [
|
|
742
|
+
`Failed to parse LLM response: ${error instanceof Error ? error.message : String(error)}`
|
|
743
|
+
]
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
needsFurtherDecomposition(subtask, currentDepth) {
|
|
748
|
+
if (currentDepth >= this.config.maxTaskGraphDepth) return false;
|
|
749
|
+
const scopeIndicators = [
|
|
750
|
+
"implement",
|
|
751
|
+
"build",
|
|
752
|
+
"create",
|
|
753
|
+
"develop",
|
|
754
|
+
"setup"
|
|
755
|
+
];
|
|
756
|
+
const hasMultipleConcepts = subtask.description.includes(" and ") || subtask.description.includes(",");
|
|
757
|
+
const hasBroadVerbs = scopeIndicators.some(
|
|
758
|
+
(v) => subtask.description.toLowerCase().startsWith(v)
|
|
759
|
+
);
|
|
760
|
+
const isLong = subtask.description.length > 500;
|
|
761
|
+
return hasMultipleConcepts || hasBroadVerbs && isLong;
|
|
762
|
+
}
|
|
763
|
+
heuristicDecomposition(task) {
|
|
764
|
+
const text = `${task.title} ${task.description}`.toLowerCase();
|
|
765
|
+
const subtasks = [];
|
|
766
|
+
const warnings = [];
|
|
767
|
+
const patterns = [
|
|
768
|
+
{
|
|
769
|
+
keyword: "research|analyze|investigate|understand",
|
|
770
|
+
label: "Research & Analysis",
|
|
771
|
+
phase: 0
|
|
772
|
+
},
|
|
773
|
+
{
|
|
774
|
+
keyword: "design|architect|plan|outline|schema",
|
|
775
|
+
label: "Design & Architecture",
|
|
776
|
+
phase: 1
|
|
777
|
+
},
|
|
778
|
+
{
|
|
779
|
+
keyword: "implement|build|create|develop|write|code|add",
|
|
780
|
+
label: "Implementation",
|
|
781
|
+
phase: 2
|
|
782
|
+
},
|
|
783
|
+
{
|
|
784
|
+
keyword: "test|verify|validate|check|qa",
|
|
785
|
+
label: "Testing & Verification",
|
|
786
|
+
phase: 3
|
|
787
|
+
},
|
|
788
|
+
{
|
|
789
|
+
keyword: "document|documentation|readme|comment",
|
|
790
|
+
label: "Documentation",
|
|
791
|
+
phase: 4
|
|
792
|
+
},
|
|
793
|
+
{
|
|
794
|
+
keyword: "deploy|release|ship|publish|ci|cd",
|
|
795
|
+
label: "Deployment & Release",
|
|
796
|
+
phase: 5
|
|
797
|
+
},
|
|
798
|
+
{
|
|
799
|
+
keyword: "refactor|clean|optimize|improve|migrate",
|
|
800
|
+
label: "Refactoring & Optimization",
|
|
801
|
+
phase: 2
|
|
802
|
+
}
|
|
803
|
+
];
|
|
804
|
+
const mentionedPhases = patterns.filter(
|
|
805
|
+
(p) => new RegExp(p.keyword, "i").test(text)
|
|
806
|
+
);
|
|
807
|
+
if (mentionedPhases.length > 0) {
|
|
808
|
+
const unique = /* @__PURE__ */ new Map();
|
|
809
|
+
for (const p of mentionedPhases) unique.set(p.phase, p);
|
|
810
|
+
const sorted = [...unique.values()].sort((a, b) => a.phase - b.phase);
|
|
811
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
812
|
+
const current = sorted[i];
|
|
813
|
+
const prev = i > 0 ? sorted[i - 1] : void 0;
|
|
814
|
+
const deps = prev ? [prev.label] : [];
|
|
815
|
+
subtasks.push({
|
|
816
|
+
title: current.label,
|
|
817
|
+
description: `Perform ${current.label.toLowerCase()} for: ${task.description}`,
|
|
818
|
+
dependsOn: deps
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
} else {
|
|
822
|
+
const parts = task.description.split(/\band\b|[,;]\s+/).map((s) => s.trim()).filter((s) => s.length > 10);
|
|
823
|
+
if (parts.length >= 2) {
|
|
824
|
+
for (let i = 0; i < parts.length; i++) {
|
|
825
|
+
const part = parts[i];
|
|
826
|
+
const title = part.length > 60 ? `${part.slice(0, 57)}...` : part;
|
|
827
|
+
const prev = i > 0 ? subtasks[i - 1] : void 0;
|
|
828
|
+
subtasks.push({
|
|
829
|
+
title,
|
|
830
|
+
description: part,
|
|
831
|
+
dependsOn: prev ? [prev.title] : []
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
} else {
|
|
835
|
+
subtasks.push(
|
|
836
|
+
{
|
|
837
|
+
title: "Research & Approach",
|
|
838
|
+
description: `Research and determine approach for: ${task.description}`,
|
|
839
|
+
dependsOn: []
|
|
840
|
+
},
|
|
841
|
+
{
|
|
842
|
+
title: "Implementation",
|
|
843
|
+
description: `Implement the solution for: ${task.description}`,
|
|
844
|
+
dependsOn: ["Research & Approach"]
|
|
845
|
+
},
|
|
846
|
+
{
|
|
847
|
+
title: "Testing",
|
|
848
|
+
description: `Write and run tests for: ${task.description}`,
|
|
849
|
+
dependsOn: ["Implementation"]
|
|
850
|
+
}
|
|
851
|
+
);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
if (subtasks.length > this.config.maxNodesPerGraph) {
|
|
855
|
+
warnings.push(
|
|
856
|
+
`Truncated to ${this.config.maxNodesPerGraph} subtasks (max)`
|
|
857
|
+
);
|
|
858
|
+
}
|
|
859
|
+
return {
|
|
860
|
+
subtasks: subtasks.slice(0, this.config.maxNodesPerGraph),
|
|
861
|
+
isValid: warnings.length === 0,
|
|
862
|
+
warnings
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
subtasksToNodes(subtasks, parentId, parentTask, depth) {
|
|
866
|
+
const titleToId = /* @__PURE__ */ new Map();
|
|
867
|
+
const nodes = [];
|
|
868
|
+
for (const sub of subtasks) {
|
|
869
|
+
const id = `task-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
870
|
+
titleToId.set(sub.title, id);
|
|
871
|
+
}
|
|
872
|
+
for (const sub of subtasks) {
|
|
873
|
+
const nodeId = titleToId.get(sub.title);
|
|
874
|
+
nodes.push({
|
|
875
|
+
id: nodeId,
|
|
876
|
+
title: sub.title,
|
|
877
|
+
description: sub.description,
|
|
878
|
+
status: "pending",
|
|
879
|
+
parentId,
|
|
880
|
+
depth,
|
|
881
|
+
dependsOn: sub.dependsOn.map((d) => titleToId.get(d) ?? d).filter(Boolean),
|
|
882
|
+
createdBy: parentTask.createdBy === "user" ? "agent" : "subagent",
|
|
883
|
+
createdByAgentId: parentTask.createdByAgentId,
|
|
884
|
+
assignedAgentId: null,
|
|
885
|
+
worktreePath: null,
|
|
886
|
+
skillsUsed: [],
|
|
887
|
+
commandsExecuted: [],
|
|
888
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
889
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
return nodes;
|
|
893
|
+
}
|
|
894
|
+
};
|
|
895
|
+
var debug3 = Debug("speexor:planner:engine");
|
|
896
|
+
var PlannerEngine = class {
|
|
897
|
+
decomposer;
|
|
898
|
+
config;
|
|
899
|
+
constructor(config) {
|
|
900
|
+
this.config = config;
|
|
901
|
+
this.decomposer = new TaskDecomposer(config);
|
|
902
|
+
}
|
|
903
|
+
async planTask(task, graph, store) {
|
|
904
|
+
const manager = new TaskGraphManager(graph);
|
|
905
|
+
const newNodes = [];
|
|
906
|
+
if (task.depth >= this.config.maxTaskGraphDepth) {
|
|
907
|
+
debug3(`Max depth reached for ${task.id}, marking as ready`);
|
|
908
|
+
manager.updateNodeStatus(task.id, "ready");
|
|
909
|
+
store.updateNodeStatus(task.id, "ready");
|
|
910
|
+
return { graph, newNodes: [] };
|
|
911
|
+
}
|
|
912
|
+
const prompt = this.decomposer.createDecompositionPrompt(task);
|
|
913
|
+
let result = null;
|
|
914
|
+
const provider = this.config.plannerProvider || "opencode";
|
|
915
|
+
result = await this.callLLM(prompt, provider);
|
|
916
|
+
if (!result || !result.isValid) {
|
|
917
|
+
debug3(`LLM decomposition failed, falling back to heuristic`);
|
|
918
|
+
result = this.heuristicDecomposition(task);
|
|
919
|
+
}
|
|
920
|
+
if (!result || !result.subtasks.length) {
|
|
921
|
+
debug3(`Heuristic returned no subtasks, marking as atomic task`);
|
|
922
|
+
manager.updateNodeStatus(task.id, "ready");
|
|
923
|
+
store.updateNodeStatus(task.id, "ready");
|
|
924
|
+
return { graph, newNodes: [] };
|
|
925
|
+
}
|
|
926
|
+
const childNodes = this.decomposer.subtasksToNodes(
|
|
927
|
+
result.subtasks,
|
|
928
|
+
task.id,
|
|
929
|
+
task,
|
|
930
|
+
task.depth + 1
|
|
931
|
+
);
|
|
932
|
+
for (const node of childNodes) {
|
|
933
|
+
manager.addNode(node);
|
|
934
|
+
store.addNode(graph.id, node);
|
|
935
|
+
newNodes.push(node);
|
|
936
|
+
}
|
|
937
|
+
if (newNodes.length === 0) {
|
|
938
|
+
manager.updateNodeStatus(task.id, "ready");
|
|
939
|
+
store.updateNodeStatus(task.id, "ready");
|
|
940
|
+
}
|
|
941
|
+
return { graph, newNodes };
|
|
942
|
+
}
|
|
943
|
+
async callLLM(prompt, provider) {
|
|
944
|
+
const fallbackChain = [provider, "opencode", "claude-code"];
|
|
945
|
+
for (const p of fallbackChain) {
|
|
946
|
+
try {
|
|
947
|
+
const response = await this.callProvider(prompt, p);
|
|
948
|
+
if (!response) continue;
|
|
949
|
+
const result = this.decomposer.parseDecompositionResponse(response);
|
|
950
|
+
if (result.isValid && result.subtasks.length > 0) {
|
|
951
|
+
return result;
|
|
952
|
+
}
|
|
953
|
+
debug3(
|
|
954
|
+
`Provider "${p}" returned invalid result: ${result.warnings.join(", ")}`
|
|
955
|
+
);
|
|
956
|
+
} catch (error) {
|
|
957
|
+
debug3(
|
|
958
|
+
`Provider "${p}" threw: ${error instanceof Error ? error.message : String(error)}`
|
|
959
|
+
);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
return null;
|
|
963
|
+
}
|
|
964
|
+
async callProvider(prompt, provider) {
|
|
965
|
+
switch (provider) {
|
|
966
|
+
case "opencode":
|
|
967
|
+
return this.callOpenCode(prompt);
|
|
968
|
+
case "claude-code":
|
|
969
|
+
return this.callClaudeCode(prompt);
|
|
970
|
+
default:
|
|
971
|
+
debug3(`Unknown provider "${provider}", trying opencode CLI`);
|
|
972
|
+
return this.callOpenCode(prompt);
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
async callOpenCode(prompt) {
|
|
976
|
+
try {
|
|
977
|
+
const { stdout } = await execa(
|
|
978
|
+
"opencode",
|
|
979
|
+
["prompt", prompt, "--quiet"],
|
|
980
|
+
{
|
|
981
|
+
timeout: 3e4,
|
|
982
|
+
reject: false
|
|
983
|
+
}
|
|
984
|
+
);
|
|
985
|
+
return stdout || null;
|
|
986
|
+
} catch (error) {
|
|
987
|
+
debug3(
|
|
988
|
+
`opencode CLI failed: ${error instanceof Error ? error.message : String(error)}`
|
|
989
|
+
);
|
|
990
|
+
return null;
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
async callClaudeCode(prompt) {
|
|
994
|
+
try {
|
|
995
|
+
const { stdout } = await execa("claude", ["-p", prompt, "--print"], {
|
|
996
|
+
timeout: 3e4,
|
|
997
|
+
reject: false
|
|
998
|
+
});
|
|
999
|
+
return stdout || null;
|
|
1000
|
+
} catch (error) {
|
|
1001
|
+
debug3(
|
|
1002
|
+
`claude CLI failed: ${error instanceof Error ? error.message : String(error)}`
|
|
1003
|
+
);
|
|
1004
|
+
return null;
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
heuristicDecomposition(task) {
|
|
1008
|
+
return this.decomposer.heuristicDecomposition(task);
|
|
1009
|
+
}
|
|
1010
|
+
generateDefaultPlan(task) {
|
|
1011
|
+
return [
|
|
1012
|
+
{
|
|
1013
|
+
title: `Analyze: ${task.title}`,
|
|
1014
|
+
description: `Research and analyze requirements for: ${task.description}`,
|
|
1015
|
+
dependsOn: []
|
|
1016
|
+
},
|
|
1017
|
+
{
|
|
1018
|
+
title: `Implement: ${task.title}`,
|
|
1019
|
+
description: `Implement the changes needed for: ${task.description}`,
|
|
1020
|
+
dependsOn: [`Analyze: ${task.title}`]
|
|
1021
|
+
},
|
|
1022
|
+
{
|
|
1023
|
+
title: `Test: ${task.title}`,
|
|
1024
|
+
description: `Write and run tests for: ${task.description}`,
|
|
1025
|
+
dependsOn: [`Implement: ${task.title}`]
|
|
1026
|
+
}
|
|
1027
|
+
];
|
|
1028
|
+
}
|
|
1029
|
+
async decomposeGraph(graph, store) {
|
|
1030
|
+
const manager = new TaskGraphManager(graph);
|
|
1031
|
+
let changed = true;
|
|
1032
|
+
while (changed) {
|
|
1033
|
+
changed = false;
|
|
1034
|
+
const pendingNodes = Array.from(graph.nodes.values()).filter(
|
|
1035
|
+
(n) => n.status === "pending"
|
|
1036
|
+
);
|
|
1037
|
+
for (const node of pendingNodes) {
|
|
1038
|
+
if (node.depth >= this.config.maxTaskGraphDepth) {
|
|
1039
|
+
manager.updateNodeStatus(node.id, "ready");
|
|
1040
|
+
store.updateNodeStatus(node.id, "ready");
|
|
1041
|
+
continue;
|
|
1042
|
+
}
|
|
1043
|
+
const { graph: updatedGraph } = await this.planTask(node, graph, store);
|
|
1044
|
+
graph = updatedGraph;
|
|
1045
|
+
changed = true;
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
store.updateGraphStatus(graph.id, "executing");
|
|
1049
|
+
return graph;
|
|
1050
|
+
}
|
|
1051
|
+
};
|
|
1052
|
+
async function taskSubmitCommand(description) {
|
|
1053
|
+
try {
|
|
1054
|
+
const config = loadConfig();
|
|
1055
|
+
const depConfig = config.decomposition ?? {
|
|
1056
|
+
maxTaskGraphDepth: 5,
|
|
1057
|
+
maxAgentSpawnDepth: 3,
|
|
1058
|
+
maxNodesPerGraph: 50,
|
|
1059
|
+
plannerProvider: "opencode"
|
|
1060
|
+
};
|
|
1061
|
+
const projectName = config.projects[0]?.name ?? "default";
|
|
1062
|
+
const store = await TaskGraphStore.create();
|
|
1063
|
+
const rootNode = {
|
|
1064
|
+
id: `task-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
1065
|
+
title: description.length > 80 ? `${description.slice(0, 77)}...` : description,
|
|
1066
|
+
description,
|
|
1067
|
+
status: "pending",
|
|
1068
|
+
parentId: null,
|
|
1069
|
+
depth: 0,
|
|
1070
|
+
dependsOn: [],
|
|
1071
|
+
createdBy: "user",
|
|
1072
|
+
assignedAgentId: null,
|
|
1073
|
+
worktreePath: null,
|
|
1074
|
+
skillsUsed: [],
|
|
1075
|
+
commandsExecuted: [],
|
|
1076
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
1077
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
1078
|
+
};
|
|
1079
|
+
const graph = store.createGraph(projectName, rootNode);
|
|
1080
|
+
const planner = new PlannerEngine(depConfig);
|
|
1081
|
+
const finalGraph = await planner.decomposeGraph(graph, store);
|
|
1082
|
+
const manager = new TaskGraphManager(finalGraph);
|
|
1083
|
+
const sorted = manager.topologicalSort();
|
|
1084
|
+
console.log(`
|
|
1085
|
+
${chalk4.bold("\u{1F4CB} Task Graph Generated")}`);
|
|
1086
|
+
console.log(` ${chalk4.dim(`Graph ID: ${graph.id}`)}`);
|
|
1087
|
+
console.log(` ${chalk4.dim(`Project: ${projectName}`)}`);
|
|
1088
|
+
console.log(` ${chalk4.dim(`Nodes: ${sorted.length}`)}
|
|
1089
|
+
`);
|
|
1090
|
+
const depthColors = [chalk4.cyan, chalk4.green, chalk4.yellow, chalk4.magenta, chalk4.blue];
|
|
1091
|
+
for (const node of sorted) {
|
|
1092
|
+
const color = depthColors[node.depth] ?? chalk4.white;
|
|
1093
|
+
const indent = " ".repeat(node.depth);
|
|
1094
|
+
const statusIcon = node.status === "done" ? "\u2713" : node.status === "failed" ? "\u2716" : node.status === "blocked" ? "\u2298" : node.status === "ready" ? "\u2192" : node.status === "in_progress" ? "\u25CC" : "\u25CB";
|
|
1095
|
+
const statusColor = node.status === "done" ? chalk4.green : node.status === "failed" ? chalk4.red : node.status === "blocked" ? chalk4.red : node.status === "ready" ? chalk4.cyan : chalk4.dim;
|
|
1096
|
+
const deps = node.dependsOn.length > 0 ? chalk4.dim(
|
|
1097
|
+
` (after: ${sorted.filter((n) => node.dependsOn.includes(n.id)).map((n) => n.title.substring(0, 30)).join(", ")})`
|
|
1098
|
+
) : "";
|
|
1099
|
+
console.log(` ${indent}${statusColor(statusIcon)} ${color(node.title)}${deps}`);
|
|
1100
|
+
}
|
|
1101
|
+
console.log(`
|
|
1102
|
+
${chalk4.green("\u2713")} Task graph persisted to ${chalk4.bold(".speexor/task-graph.sqlite")}
|
|
1103
|
+
`);
|
|
1104
|
+
} catch (error) {
|
|
1105
|
+
if (error instanceof Error) {
|
|
1106
|
+
console.error(`
|
|
1107
|
+
${chalk4.red("\u2716")} ${error.message}
|
|
1108
|
+
`);
|
|
1109
|
+
}
|
|
1110
|
+
process.exit(1);
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
var debug4 = Debug("speexor:marketplace");
|
|
1114
|
+
var MarketplaceClient = class {
|
|
1115
|
+
sources;
|
|
1116
|
+
constructor(sources) {
|
|
1117
|
+
this.sources = sources;
|
|
1118
|
+
}
|
|
1119
|
+
async search(query) {
|
|
1120
|
+
const results = [];
|
|
1121
|
+
const lowerQuery = query.toLowerCase();
|
|
1122
|
+
for (const source of this.sources) {
|
|
1123
|
+
try {
|
|
1124
|
+
const packages = await this.fetchSourceIndex(source);
|
|
1125
|
+
const filtered = packages.filter(
|
|
1126
|
+
(pkg) => pkg.name.toLowerCase().includes(lowerQuery) || pkg.description.toLowerCase().includes(lowerQuery) || pkg.author.toLowerCase().includes(lowerQuery)
|
|
1127
|
+
);
|
|
1128
|
+
results.push(...filtered);
|
|
1129
|
+
} catch (err) {
|
|
1130
|
+
debug4(
|
|
1131
|
+
`Failed to search source ${source.url}: ${err.message}`
|
|
1132
|
+
);
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
return results;
|
|
1136
|
+
}
|
|
1137
|
+
async getDetails(packageName) {
|
|
1138
|
+
for (const source of this.sources) {
|
|
1139
|
+
try {
|
|
1140
|
+
const packages = await this.fetchSourceIndex(source);
|
|
1141
|
+
const found = packages.find((p) => p.name === packageName);
|
|
1142
|
+
if (found) return found;
|
|
1143
|
+
} catch (err) {
|
|
1144
|
+
debug4(
|
|
1145
|
+
`Failed to fetch details from ${source.url}: ${err.message}`
|
|
1146
|
+
);
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
throw new Error(
|
|
1150
|
+
`Package "${packageName}" not found in any marketplace source`
|
|
1151
|
+
);
|
|
1152
|
+
}
|
|
1153
|
+
resolveDownloadUrl(packageName) {
|
|
1154
|
+
for (const source of this.sources) {
|
|
1155
|
+
if (source.type === "github-index") {
|
|
1156
|
+
const base = source.url.replace(/\/index\.json$/, "").replace(/\/$/, "");
|
|
1157
|
+
return `${base}/${packageName}/speexor.extension.json`;
|
|
1158
|
+
}
|
|
1159
|
+
if (source.type === "npm") {
|
|
1160
|
+
return `npm:${packageName}`;
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
throw new Error(`No download URL could be resolved for "${packageName}"`);
|
|
1164
|
+
}
|
|
1165
|
+
async fetchSourceIndex(source) {
|
|
1166
|
+
if (source.type === "github-index") {
|
|
1167
|
+
const response = await fetch(source.url);
|
|
1168
|
+
if (!response.ok) {
|
|
1169
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
1170
|
+
}
|
|
1171
|
+
const data = await response.json();
|
|
1172
|
+
return data.packages ?? [];
|
|
1173
|
+
}
|
|
1174
|
+
if (source.type === "npm") {
|
|
1175
|
+
const encodedName = source.url.startsWith("@") ? `@${encodeURIComponent(source.url.slice(1).split("/")[0])}/${encodeURIComponent(source.url.split("/")[1])}` : encodeURIComponent(source.url);
|
|
1176
|
+
const response = await fetch(`https://registry.npmjs.org/${encodedName}`);
|
|
1177
|
+
if (!response.ok) {
|
|
1178
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
1179
|
+
}
|
|
1180
|
+
const data = await response.json();
|
|
1181
|
+
return [
|
|
1182
|
+
{
|
|
1183
|
+
name: data.name,
|
|
1184
|
+
manifestUrl: `https://www.npmjs.com/package/${source.url}`,
|
|
1185
|
+
checksum: "",
|
|
1186
|
+
author: data.author?.name ?? data._npmUser?.name ?? "unknown",
|
|
1187
|
+
description: data.description ?? "",
|
|
1188
|
+
version: data["dist-tags"]?.latest ?? "0.0.0"
|
|
1189
|
+
}
|
|
1190
|
+
];
|
|
1191
|
+
}
|
|
1192
|
+
return [];
|
|
1193
|
+
}
|
|
1194
|
+
};
|
|
1195
|
+
var ExtensionManifestLoader = class {
|
|
1196
|
+
loadFromFile(path) {
|
|
1197
|
+
if (!existsSync(path)) {
|
|
1198
|
+
throw new Error(`Manifest file not found: ${path}`);
|
|
1199
|
+
}
|
|
1200
|
+
let raw;
|
|
1201
|
+
try {
|
|
1202
|
+
raw = readFileSync(path, "utf-8");
|
|
1203
|
+
} catch (err) {
|
|
1204
|
+
throw new Error(
|
|
1205
|
+
`Failed to read manifest file: ${err.message}`
|
|
1206
|
+
);
|
|
1207
|
+
}
|
|
1208
|
+
let parsed;
|
|
1209
|
+
try {
|
|
1210
|
+
parsed = JSON.parse(raw);
|
|
1211
|
+
} catch (err) {
|
|
1212
|
+
throw new Error(
|
|
1213
|
+
`Invalid JSON in manifest file: ${err.message}`
|
|
1214
|
+
);
|
|
1215
|
+
}
|
|
1216
|
+
const manifest = parsed;
|
|
1217
|
+
const validation = this.validate(manifest);
|
|
1218
|
+
if (!validation.valid) {
|
|
1219
|
+
throw new Error(
|
|
1220
|
+
`Manifest validation failed:
|
|
1221
|
+
${validation.errors.join("\n ")}`
|
|
1222
|
+
);
|
|
1223
|
+
}
|
|
1224
|
+
return manifest;
|
|
1225
|
+
}
|
|
1226
|
+
validate(manifest) {
|
|
1227
|
+
const errors = [];
|
|
1228
|
+
if (!manifest.name || typeof manifest.name !== "string") {
|
|
1229
|
+
errors.push('Missing or invalid "name" (must be a non-empty string)');
|
|
1230
|
+
}
|
|
1231
|
+
if (!manifest.version || typeof manifest.version !== "string") {
|
|
1232
|
+
errors.push('Missing or invalid "version" (must be a string)');
|
|
1233
|
+
} else if (!/^\d+\.\d+\.\d+/.test(manifest.version)) {
|
|
1234
|
+
errors.push('Invalid "version" format (must be semver like x.y.z)');
|
|
1235
|
+
}
|
|
1236
|
+
const validTypes = [
|
|
1237
|
+
"agent-backend",
|
|
1238
|
+
"skill",
|
|
1239
|
+
"plugin",
|
|
1240
|
+
"action-adapter",
|
|
1241
|
+
"library",
|
|
1242
|
+
"mcp-server"
|
|
1243
|
+
];
|
|
1244
|
+
if (!manifest.type || !validTypes.includes(manifest.type)) {
|
|
1245
|
+
errors.push(
|
|
1246
|
+
`Missing or invalid "type" (must be one of: ${validTypes.join(", ")})`
|
|
1247
|
+
);
|
|
1248
|
+
}
|
|
1249
|
+
if (!manifest.speexorApiVersion || typeof manifest.speexorApiVersion !== "string") {
|
|
1250
|
+
errors.push('Missing or invalid "speexorApiVersion" (must be a string)');
|
|
1251
|
+
}
|
|
1252
|
+
if (!manifest.entry || typeof manifest.entry !== "string") {
|
|
1253
|
+
errors.push('Missing or invalid "entry" (must be a string)');
|
|
1254
|
+
}
|
|
1255
|
+
if (!manifest.description || typeof manifest.description !== "string") {
|
|
1256
|
+
errors.push(
|
|
1257
|
+
'Missing or invalid "description" (must be a non-empty string)'
|
|
1258
|
+
);
|
|
1259
|
+
}
|
|
1260
|
+
if (!manifest.author || typeof manifest.author !== "string") {
|
|
1261
|
+
errors.push('Missing or invalid "author" (must be a non-empty string)');
|
|
1262
|
+
}
|
|
1263
|
+
if (manifest.permissions) {
|
|
1264
|
+
const permErrors = this.validatePermissions(manifest.permissions);
|
|
1265
|
+
errors.push(...permErrors);
|
|
1266
|
+
} else {
|
|
1267
|
+
errors.push('Missing "permissions" object');
|
|
1268
|
+
}
|
|
1269
|
+
if (manifest.signature !== void 0 && typeof manifest.signature !== "string") {
|
|
1270
|
+
errors.push('Invalid "signature" (must be a string if present)');
|
|
1271
|
+
}
|
|
1272
|
+
return { valid: errors.length === 0, errors };
|
|
1273
|
+
}
|
|
1274
|
+
checkApiCompatibility(manifest, currentApiVersion) {
|
|
1275
|
+
const reqParts = manifest.speexorApiVersion.split(".").map(Number);
|
|
1276
|
+
const curParts = currentApiVersion.split(".").map(Number);
|
|
1277
|
+
if (reqParts.length < 2 || curParts.length < 2) return false;
|
|
1278
|
+
return reqParts[0] === curParts[0] && reqParts[1] === curParts[1];
|
|
1279
|
+
}
|
|
1280
|
+
validatePermissions(permissions) {
|
|
1281
|
+
const errors = [];
|
|
1282
|
+
const validLevels = [
|
|
1283
|
+
"none",
|
|
1284
|
+
"read-only",
|
|
1285
|
+
"read-write",
|
|
1286
|
+
"scoped",
|
|
1287
|
+
"full"
|
|
1288
|
+
];
|
|
1289
|
+
const axes = [
|
|
1290
|
+
"fileSystem",
|
|
1291
|
+
"network",
|
|
1292
|
+
"shell"
|
|
1293
|
+
];
|
|
1294
|
+
for (const axis of axes) {
|
|
1295
|
+
if (!validLevels.includes(permissions[axis])) {
|
|
1296
|
+
errors.push(
|
|
1297
|
+
`Invalid permission level for "${axis}" (must be one of: ${validLevels.join(", ")})`
|
|
1298
|
+
);
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
if (!Array.isArray(permissions.secrets)) {
|
|
1302
|
+
errors.push('"permissions.secrets" must be an array of strings');
|
|
1303
|
+
} else {
|
|
1304
|
+
for (const s of permissions.secrets) {
|
|
1305
|
+
if (typeof s !== "string") {
|
|
1306
|
+
errors.push('Each entry in "permissions.secrets" must be a string');
|
|
1307
|
+
break;
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
return errors;
|
|
1312
|
+
}
|
|
1313
|
+
};
|
|
1314
|
+
|
|
1315
|
+
// src/extension-manager/permission-view.ts
|
|
1316
|
+
var axisLabels = {
|
|
1317
|
+
fileSystem: "File system access",
|
|
1318
|
+
network: "Network access",
|
|
1319
|
+
shell: "Shell/command execution",
|
|
1320
|
+
secrets: "Secret/key access"
|
|
1321
|
+
};
|
|
1322
|
+
var levelDescriptions = {
|
|
1323
|
+
none: "No access",
|
|
1324
|
+
"read-only": "Read-only access",
|
|
1325
|
+
"read-write": "Read and write access",
|
|
1326
|
+
scoped: "Scoped access (limited to specific paths/origins)",
|
|
1327
|
+
full: "Full unrestricted access"
|
|
1328
|
+
};
|
|
1329
|
+
function formatPermissions(permissions) {
|
|
1330
|
+
const lines = ["Permissions:"];
|
|
1331
|
+
const axes = [
|
|
1332
|
+
"fileSystem",
|
|
1333
|
+
"network",
|
|
1334
|
+
"shell"
|
|
1335
|
+
];
|
|
1336
|
+
for (const axis of axes) {
|
|
1337
|
+
lines.push(
|
|
1338
|
+
` ${axisLabels[axis]}: ${levelDescriptions[permissions[axis]]}`
|
|
1339
|
+
);
|
|
1340
|
+
}
|
|
1341
|
+
if (permissions.secrets.length > 0) {
|
|
1342
|
+
lines.push(` Secret scopes: ${permissions.secrets.join(", ")}`);
|
|
1343
|
+
} else {
|
|
1344
|
+
lines.push(" Secret scopes: none");
|
|
1345
|
+
}
|
|
1346
|
+
return lines.join("\n");
|
|
1347
|
+
}
|
|
1348
|
+
function getPermissionRiskLevel(permissions) {
|
|
1349
|
+
let riskScore = 0;
|
|
1350
|
+
if (permissions.fileSystem === "full" || permissions.fileSystem === "read-write")
|
|
1351
|
+
riskScore += 2;
|
|
1352
|
+
else if (permissions.fileSystem === "scoped") riskScore += 1;
|
|
1353
|
+
if (permissions.network === "full" || permissions.network === "read-write")
|
|
1354
|
+
riskScore += 2;
|
|
1355
|
+
else if (permissions.network === "scoped") riskScore += 1;
|
|
1356
|
+
if (permissions.shell === "full" || permissions.shell === "read-write")
|
|
1357
|
+
riskScore += 3;
|
|
1358
|
+
else if (permissions.shell === "scoped") riskScore += 1;
|
|
1359
|
+
if (permissions.secrets.length > 0)
|
|
1360
|
+
riskScore += Math.min(permissions.secrets.length, 3);
|
|
1361
|
+
if (riskScore >= 6) return "high";
|
|
1362
|
+
if (riskScore >= 3) return "medium";
|
|
1363
|
+
return "low";
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
// src/extension-manager/registry.ts
|
|
1367
|
+
var debug5 = Debug("speexor:extension-registry");
|
|
1368
|
+
var ExtensionRegistry = class {
|
|
1369
|
+
baseDir;
|
|
1370
|
+
registryPath;
|
|
1371
|
+
extensionsDir;
|
|
1372
|
+
data;
|
|
1373
|
+
manifestLoader;
|
|
1374
|
+
constructor(baseDir) {
|
|
1375
|
+
const defaultDir = join(homedir(), ".speexor", "extensions");
|
|
1376
|
+
this.baseDir = baseDir ?? defaultDir;
|
|
1377
|
+
this.registryPath = join(this.baseDir, "registry.json");
|
|
1378
|
+
this.extensionsDir = this.baseDir;
|
|
1379
|
+
this.manifestLoader = new ExtensionManifestLoader();
|
|
1380
|
+
this.data = this.loadRegistry();
|
|
1381
|
+
}
|
|
1382
|
+
async install(manifestPath) {
|
|
1383
|
+
const manifest = this.manifestLoader.loadFromFile(manifestPath);
|
|
1384
|
+
if (this.get(manifest.name)) {
|
|
1385
|
+
throw new Error(`Extension "${manifest.name}" is already installed`);
|
|
1386
|
+
}
|
|
1387
|
+
const extDir = join(this.extensionsDir, manifest.name);
|
|
1388
|
+
if (!existsSync(extDir)) {
|
|
1389
|
+
mkdirSync(extDir, { recursive: true });
|
|
1390
|
+
}
|
|
1391
|
+
const destManifest = join(extDir, "speexor.extension.json");
|
|
1392
|
+
copyFileSync(manifestPath, destManifest);
|
|
1393
|
+
const sourceDir = dirname(manifestPath);
|
|
1394
|
+
const entryPath = join(sourceDir, manifest.entry);
|
|
1395
|
+
if (existsSync(entryPath)) {
|
|
1396
|
+
const destEntry = join(extDir, manifest.entry);
|
|
1397
|
+
const destEntryDir = dirname(destEntry);
|
|
1398
|
+
if (!existsSync(destEntryDir)) {
|
|
1399
|
+
mkdirSync(destEntryDir, { recursive: true });
|
|
1400
|
+
}
|
|
1401
|
+
copyFileSync(entryPath, destEntry);
|
|
1402
|
+
}
|
|
1403
|
+
const checksum = createHash("sha256").update(JSON.stringify(manifest)).digest("hex");
|
|
1404
|
+
const installed = {
|
|
1405
|
+
name: manifest.name,
|
|
1406
|
+
manifest,
|
|
1407
|
+
enabled: true,
|
|
1408
|
+
installPath: extDir,
|
|
1409
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1410
|
+
checksum
|
|
1411
|
+
};
|
|
1412
|
+
this.data.extensions.push(installed);
|
|
1413
|
+
this.saveRegistry();
|
|
1414
|
+
debug5(`Installed extension "${manifest.name}" v${manifest.version}`);
|
|
1415
|
+
return installed;
|
|
1416
|
+
}
|
|
1417
|
+
remove(name) {
|
|
1418
|
+
const idx = this.data.extensions.findIndex((e) => e.name === name);
|
|
1419
|
+
if (idx === -1) {
|
|
1420
|
+
throw new Error(`Extension "${name}" is not installed`);
|
|
1421
|
+
}
|
|
1422
|
+
const ext = this.data.extensions[idx];
|
|
1423
|
+
const extDir = ext.installPath;
|
|
1424
|
+
if (existsSync(extDir)) {
|
|
1425
|
+
rmSync(extDir, { recursive: true, force: true });
|
|
1426
|
+
}
|
|
1427
|
+
this.data.extensions.splice(idx, 1);
|
|
1428
|
+
for (const projectName of Object.keys(this.data.projectOverrides)) {
|
|
1429
|
+
const overrides = this.data.projectOverrides[projectName];
|
|
1430
|
+
if (overrides) {
|
|
1431
|
+
delete overrides[name];
|
|
1432
|
+
if (Object.keys(overrides).length === 0) {
|
|
1433
|
+
delete this.data.projectOverrides[projectName];
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
this.saveRegistry();
|
|
1438
|
+
debug5(`Removed extension "${name}"`);
|
|
1439
|
+
}
|
|
1440
|
+
enable(name, projectName) {
|
|
1441
|
+
const ext = this.get(name);
|
|
1442
|
+
if (!ext) {
|
|
1443
|
+
throw new Error(`Extension "${name}" is not installed`);
|
|
1444
|
+
}
|
|
1445
|
+
if (projectName) {
|
|
1446
|
+
if (!this.data.projectOverrides[projectName]) {
|
|
1447
|
+
this.data.projectOverrides[projectName] = {};
|
|
1448
|
+
}
|
|
1449
|
+
this.data.projectOverrides[projectName][name] = true;
|
|
1450
|
+
} else {
|
|
1451
|
+
ext.enabled = true;
|
|
1452
|
+
}
|
|
1453
|
+
this.saveRegistry();
|
|
1454
|
+
debug5(`Enabled extension "${name}"${projectName ? ` for project "${projectName}"` : ""}`);
|
|
1455
|
+
}
|
|
1456
|
+
disable(name, projectName) {
|
|
1457
|
+
const ext = this.get(name);
|
|
1458
|
+
if (!ext) {
|
|
1459
|
+
throw new Error(`Extension "${name}" is not installed`);
|
|
1460
|
+
}
|
|
1461
|
+
if (projectName) {
|
|
1462
|
+
if (!this.data.projectOverrides[projectName]) {
|
|
1463
|
+
this.data.projectOverrides[projectName] = {};
|
|
1464
|
+
}
|
|
1465
|
+
this.data.projectOverrides[projectName][name] = false;
|
|
1466
|
+
} else {
|
|
1467
|
+
ext.enabled = false;
|
|
1468
|
+
}
|
|
1469
|
+
this.saveRegistry();
|
|
1470
|
+
debug5(`Disabled extension "${name}"${projectName ? ` for project "${projectName}"` : ""}`);
|
|
1471
|
+
}
|
|
1472
|
+
list() {
|
|
1473
|
+
return [...this.data.extensions];
|
|
1474
|
+
}
|
|
1475
|
+
get(name) {
|
|
1476
|
+
return this.data.extensions.find((e) => e.name === name);
|
|
1477
|
+
}
|
|
1478
|
+
isEnabled(name, projectName) {
|
|
1479
|
+
const ext = this.get(name);
|
|
1480
|
+
if (!ext) return false;
|
|
1481
|
+
if (projectName) {
|
|
1482
|
+
const overrides = this.data.projectOverrides[projectName];
|
|
1483
|
+
if (overrides && name in overrides) {
|
|
1484
|
+
return overrides[name] === true;
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
return ext.enabled;
|
|
1488
|
+
}
|
|
1489
|
+
verifyInstall(name) {
|
|
1490
|
+
const ext = this.get(name);
|
|
1491
|
+
if (!ext) {
|
|
1492
|
+
throw new Error(`Extension "${name}" is not installed`);
|
|
1493
|
+
}
|
|
1494
|
+
const actual = createHash("sha256").update(JSON.stringify(ext.manifest)).digest("hex");
|
|
1495
|
+
return {
|
|
1496
|
+
valid: ext.checksum === actual,
|
|
1497
|
+
expectedChecksum: ext.checksum ?? "",
|
|
1498
|
+
actualChecksum: actual
|
|
1499
|
+
};
|
|
1500
|
+
}
|
|
1501
|
+
displayPermissions(name) {
|
|
1502
|
+
const ext = this.get(name);
|
|
1503
|
+
if (!ext) {
|
|
1504
|
+
throw new Error(`Extension "${name}" is not installed`);
|
|
1505
|
+
}
|
|
1506
|
+
const formatted = formatPermissions(ext.manifest.permissions);
|
|
1507
|
+
const risk = getPermissionRiskLevel(ext.manifest.permissions);
|
|
1508
|
+
return `${formatted}
|
|
1509
|
+
Risk level: ${risk}`;
|
|
1510
|
+
}
|
|
1511
|
+
loadRegistry() {
|
|
1512
|
+
try {
|
|
1513
|
+
if (existsSync(this.registryPath)) {
|
|
1514
|
+
const raw = readFileSync(this.registryPath, "utf-8");
|
|
1515
|
+
const parsed = JSON.parse(raw);
|
|
1516
|
+
return {
|
|
1517
|
+
version: parsed.version ?? 1,
|
|
1518
|
+
extensions: parsed.extensions ?? [],
|
|
1519
|
+
projectOverrides: parsed.projectOverrides ?? {}
|
|
1520
|
+
};
|
|
1521
|
+
}
|
|
1522
|
+
} catch (error) {
|
|
1523
|
+
debug5(`Failed to load registry: ${error}`);
|
|
1524
|
+
}
|
|
1525
|
+
return { version: 1, extensions: [], projectOverrides: {} };
|
|
1526
|
+
}
|
|
1527
|
+
saveRegistry() {
|
|
1528
|
+
try {
|
|
1529
|
+
if (!existsSync(this.baseDir)) {
|
|
1530
|
+
mkdirSync(this.baseDir, { recursive: true });
|
|
1531
|
+
}
|
|
1532
|
+
writeFileSync(this.registryPath, JSON.stringify(this.data, null, 2), "utf-8");
|
|
1533
|
+
} catch (error) {
|
|
1534
|
+
debug5(`Failed to save registry: ${error}`);
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
};
|
|
1538
|
+
async function extSearchCommand(query) {
|
|
1539
|
+
try {
|
|
1540
|
+
const config = loadConfig();
|
|
1541
|
+
const sources = [
|
|
1542
|
+
{ type: "github-index", url: config.extensions?.marketplaceIndex ?? "https://marketplace.speexor.dev/index.json" }
|
|
1543
|
+
];
|
|
1544
|
+
const client = new MarketplaceClient(sources);
|
|
1545
|
+
const results = await client.search(query);
|
|
1546
|
+
if (results.length === 0) {
|
|
1547
|
+
console.log(`
|
|
1548
|
+
${chalk4.dim("No extensions found matching")} "${query}"
|
|
1549
|
+
`);
|
|
1550
|
+
return;
|
|
1551
|
+
}
|
|
1552
|
+
console.log(`
|
|
1553
|
+
${chalk4.bold(`\u{1F50D} Search results for "${query}"`)} (${results.length})
|
|
1554
|
+
`);
|
|
1555
|
+
for (const pkg of results) {
|
|
1556
|
+
console.log(` ${chalk4.cyan(pkg.name)} ${chalk4.dim(`v${pkg.version}`)}`);
|
|
1557
|
+
console.log(` ${pkg.description}`);
|
|
1558
|
+
console.log(` ${chalk4.dim(`by ${pkg.author}`)}`);
|
|
1559
|
+
console.log();
|
|
1560
|
+
}
|
|
1561
|
+
} catch (error) {
|
|
1562
|
+
if (error instanceof Error) {
|
|
1563
|
+
console.error(`
|
|
1564
|
+
${chalk4.red("\u2716")} ${error.message}
|
|
1565
|
+
`);
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
async function extInstallCommand(name) {
|
|
1570
|
+
try {
|
|
1571
|
+
const config = loadConfig();
|
|
1572
|
+
const sources = [
|
|
1573
|
+
{ type: "github-index", url: config.extensions?.marketplaceIndex ?? "https://marketplace.speexor.dev/index.json" }
|
|
1574
|
+
];
|
|
1575
|
+
const client = new MarketplaceClient(sources);
|
|
1576
|
+
await client.getDetails(name);
|
|
1577
|
+
const downloadUrl = client.resolveDownloadUrl(name);
|
|
1578
|
+
const response = await fetch(downloadUrl);
|
|
1579
|
+
if (!response.ok) {
|
|
1580
|
+
throw new Error(`Failed to download extension manifest: HTTP ${response.status}`);
|
|
1581
|
+
}
|
|
1582
|
+
const manifestJson = await response.text();
|
|
1583
|
+
const extDir = join(process.cwd(), ".speexor", "extensions", name);
|
|
1584
|
+
if (!existsSync(extDir)) {
|
|
1585
|
+
mkdirSync(extDir, { recursive: true });
|
|
1586
|
+
}
|
|
1587
|
+
const manifestPath = join(extDir, "speexor.extension.json");
|
|
1588
|
+
writeFileSync(manifestPath, manifestJson, "utf-8");
|
|
1589
|
+
const registry = new ExtensionRegistry();
|
|
1590
|
+
const installed = await registry.install(manifestPath);
|
|
1591
|
+
console.log(`
|
|
1592
|
+
${chalk4.green("\u2713")} Installed ${chalk4.bold(installed.name)} v${installed.manifest.version}
|
|
1593
|
+
`);
|
|
1594
|
+
} catch (error) {
|
|
1595
|
+
if (error instanceof Error) {
|
|
1596
|
+
console.error(`
|
|
1597
|
+
${chalk4.red("\u2716")} ${error.message}
|
|
1598
|
+
`);
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
async function extListCommand() {
|
|
1603
|
+
try {
|
|
1604
|
+
const registry = new ExtensionRegistry();
|
|
1605
|
+
const extensions = registry.list();
|
|
1606
|
+
if (extensions.length === 0) {
|
|
1607
|
+
console.log(`
|
|
1608
|
+
${chalk4.dim("No extensions installed.")}
|
|
1609
|
+
`);
|
|
1610
|
+
return;
|
|
1611
|
+
}
|
|
1612
|
+
console.log(`
|
|
1613
|
+
${chalk4.bold("\u{1F4E6} Installed Extensions")} (${extensions.length})
|
|
1614
|
+
`);
|
|
1615
|
+
for (const ext of extensions) {
|
|
1616
|
+
const status = ext.enabled ? chalk4.green("enabled") : chalk4.red("disabled");
|
|
1617
|
+
console.log(` ${chalk4.cyan(ext.name)} ${chalk4.dim(`v${ext.manifest.version}`)} [${status}]`);
|
|
1618
|
+
console.log(` ${ext.manifest.description}`);
|
|
1619
|
+
console.log(` ${chalk4.dim(`type: ${ext.manifest.type} path: ${ext.installPath}`)}`);
|
|
1620
|
+
console.log();
|
|
1621
|
+
}
|
|
1622
|
+
} catch (error) {
|
|
1623
|
+
if (error instanceof Error) {
|
|
1624
|
+
console.error(`
|
|
1625
|
+
${chalk4.red("\u2716")} ${error.message}
|
|
1626
|
+
`);
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
async function extUpdateCommand(name) {
|
|
1631
|
+
try {
|
|
1632
|
+
const registry = new ExtensionRegistry();
|
|
1633
|
+
const config = loadConfig();
|
|
1634
|
+
const sources = [
|
|
1635
|
+
{ type: "github-index", url: config.extensions?.marketplaceIndex ?? "https://marketplace.speexor.dev/index.json" }
|
|
1636
|
+
];
|
|
1637
|
+
const client = new MarketplaceClient(sources);
|
|
1638
|
+
const toUpdate = name ? registry.list().filter((e) => e.name === name) : registry.list();
|
|
1639
|
+
if (toUpdate.length === 0) {
|
|
1640
|
+
console.log(`
|
|
1641
|
+
${chalk4.dim("No extensions to update.")}
|
|
1642
|
+
`);
|
|
1643
|
+
return;
|
|
1644
|
+
}
|
|
1645
|
+
let updated = 0;
|
|
1646
|
+
for (const ext of toUpdate) {
|
|
1647
|
+
try {
|
|
1648
|
+
const pkg = await client.getDetails(ext.name);
|
|
1649
|
+
if (pkg.version !== ext.manifest.version) {
|
|
1650
|
+
registry.remove(ext.name);
|
|
1651
|
+
const downloadUrl = client.resolveDownloadUrl(ext.name);
|
|
1652
|
+
const response = await fetch(downloadUrl);
|
|
1653
|
+
if (response.ok) {
|
|
1654
|
+
const manifestJson = await response.text();
|
|
1655
|
+
const extDir = join(process.cwd(), ".speexor", "extensions", ext.name);
|
|
1656
|
+
if (!existsSync(extDir)) mkdirSync(extDir, { recursive: true });
|
|
1657
|
+
const manifestPath = join(extDir, "speexor.extension.json");
|
|
1658
|
+
writeFileSync(manifestPath, manifestJson, "utf-8");
|
|
1659
|
+
const installed = await registry.install(manifestPath);
|
|
1660
|
+
console.log(
|
|
1661
|
+
` ${chalk4.green("\u2713")} Updated ${chalk4.bold(installed.name)} ${chalk4.dim(`${ext.manifest.version} \u2192 ${installed.manifest.version}`)}`
|
|
1662
|
+
);
|
|
1663
|
+
updated++;
|
|
1664
|
+
}
|
|
1665
|
+
} else {
|
|
1666
|
+
console.log(` ${chalk4.dim(`${ext.name} is already at latest version (${pkg.version})`)}`);
|
|
1667
|
+
}
|
|
1668
|
+
} catch (err) {
|
|
1669
|
+
console.error(` ${chalk4.red("\u2716")} Failed to update ${ext.name}: ${err.message}`);
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
if (updated > 0) {
|
|
1673
|
+
console.log(`
|
|
1674
|
+
${chalk4.green("\u2713")} ${updated} extension(s) updated
|
|
1675
|
+
`);
|
|
1676
|
+
} else {
|
|
1677
|
+
console.log(`
|
|
1678
|
+
${chalk4.dim("All extensions up to date.")}
|
|
1679
|
+
`);
|
|
1680
|
+
}
|
|
1681
|
+
} catch (error) {
|
|
1682
|
+
if (error instanceof Error) {
|
|
1683
|
+
console.error(`
|
|
1684
|
+
${chalk4.red("\u2716")} ${error.message}
|
|
1685
|
+
`);
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
async function extRemoveCommand(name) {
|
|
1690
|
+
try {
|
|
1691
|
+
const registry = new ExtensionRegistry();
|
|
1692
|
+
registry.remove(name);
|
|
1693
|
+
console.log(`
|
|
1694
|
+
${chalk4.green("\u2713")} Removed extension ${chalk4.bold(name)}
|
|
1695
|
+
`);
|
|
1696
|
+
} catch (error) {
|
|
1697
|
+
if (error instanceof Error) {
|
|
1698
|
+
console.error(`
|
|
1699
|
+
${chalk4.red("\u2716")} ${error.message}
|
|
1700
|
+
`);
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
var debug6 = Debug("speexor:config-migration:registry");
|
|
1705
|
+
var MigrationRegistry = class {
|
|
1706
|
+
migrations = [];
|
|
1707
|
+
register(migration) {
|
|
1708
|
+
const existing = this.migrations.find(
|
|
1709
|
+
(m) => m.fromVersion === migration.fromVersion && m.toVersion === migration.toVersion
|
|
1710
|
+
);
|
|
1711
|
+
if (existing) {
|
|
1712
|
+
debug6(
|
|
1713
|
+
`Overwriting migration ${migration.fromVersion} \u2192 ${migration.toVersion}`
|
|
1714
|
+
);
|
|
1715
|
+
const idx = this.migrations.indexOf(existing);
|
|
1716
|
+
this.migrations[idx] = migration;
|
|
1717
|
+
return;
|
|
1718
|
+
}
|
|
1719
|
+
this.migrations.push(migration);
|
|
1720
|
+
debug6(
|
|
1721
|
+
`Registered migration ${migration.fromVersion} \u2192 ${migration.toVersion}`
|
|
1722
|
+
);
|
|
1723
|
+
}
|
|
1724
|
+
getMigration(from, to) {
|
|
1725
|
+
return this.migrations.find(
|
|
1726
|
+
(m) => m.fromVersion === from && m.toVersion === to
|
|
1727
|
+
);
|
|
1728
|
+
}
|
|
1729
|
+
getAvailableMigrations() {
|
|
1730
|
+
return this.migrations.map((m) => ({
|
|
1731
|
+
from: m.fromVersion,
|
|
1732
|
+
to: m.toVersion
|
|
1733
|
+
}));
|
|
1734
|
+
}
|
|
1735
|
+
resolveChain(from, to) {
|
|
1736
|
+
const chain = [];
|
|
1737
|
+
let current = from;
|
|
1738
|
+
while (current !== to) {
|
|
1739
|
+
const next = this.findNextStep(current);
|
|
1740
|
+
if (!next) {
|
|
1741
|
+
throw new Error(
|
|
1742
|
+
`No migration path from ${from} to ${to}. Available: ${JSON.stringify(this.getAvailableMigrations())}`
|
|
1743
|
+
);
|
|
1744
|
+
}
|
|
1745
|
+
chain.push(next);
|
|
1746
|
+
current = next.toVersion;
|
|
1747
|
+
}
|
|
1748
|
+
return chain;
|
|
1749
|
+
}
|
|
1750
|
+
clear() {
|
|
1751
|
+
this.migrations = [];
|
|
1752
|
+
debug6("Migration registry cleared");
|
|
1753
|
+
}
|
|
1754
|
+
findNextStep(from) {
|
|
1755
|
+
return this.migrations.find((m) => m.fromVersion === from);
|
|
1756
|
+
}
|
|
1757
|
+
};
|
|
1758
|
+
var debug7 = Debug("speexor:config-migration:migrations");
|
|
1759
|
+
function migration1to2(config, configPath) {
|
|
1760
|
+
const migrated = { ...config };
|
|
1761
|
+
migrated.version = "2";
|
|
1762
|
+
if (!migrated.decomposition) {
|
|
1763
|
+
migrated.decomposition = {
|
|
1764
|
+
maxTaskGraphDepth: 5,
|
|
1765
|
+
maxAgentSpawnDepth: 3,
|
|
1766
|
+
maxNodesPerGraph: 50
|
|
1767
|
+
};
|
|
1768
|
+
}
|
|
1769
|
+
if (!migrated.scheduler) {
|
|
1770
|
+
migrated.scheduler = {
|
|
1771
|
+
maxConcurrentAgents: "auto",
|
|
1772
|
+
retryOnFailure: 3,
|
|
1773
|
+
providerFallbackChain: []
|
|
1774
|
+
};
|
|
1775
|
+
}
|
|
1776
|
+
if (!migrated.governance) {
|
|
1777
|
+
migrated.governance = {
|
|
1778
|
+
autoApproveProposedTasks: false,
|
|
1779
|
+
autoApproveCategories: [],
|
|
1780
|
+
duplicateSimilarityThreshold: 0.8
|
|
1781
|
+
};
|
|
1782
|
+
}
|
|
1783
|
+
debug7(
|
|
1784
|
+
"Migrated v1 \u2192 v2: added decomposition, scheduler, governance defaults"
|
|
1785
|
+
);
|
|
1786
|
+
return migrated;
|
|
1787
|
+
}
|
|
1788
|
+
function migration2to3(config, configPath) {
|
|
1789
|
+
const migrated = { ...config };
|
|
1790
|
+
migrated.version = "3";
|
|
1791
|
+
if (!migrated.riskPolicy) {
|
|
1792
|
+
migrated.riskPolicy = {
|
|
1793
|
+
autoApprove: [],
|
|
1794
|
+
requireApproval: [],
|
|
1795
|
+
approvalTimeout: "30m",
|
|
1796
|
+
approvalDefaultAction: "escalate",
|
|
1797
|
+
defaultRiskTierForUnknownActions: "medium"
|
|
1798
|
+
};
|
|
1799
|
+
}
|
|
1800
|
+
if (!migrated.scheduler) {
|
|
1801
|
+
migrated.scheduler = {
|
|
1802
|
+
maxConcurrentAgents: "auto",
|
|
1803
|
+
retryOnFailure: 3,
|
|
1804
|
+
providerFallbackChain: []
|
|
1805
|
+
};
|
|
1806
|
+
}
|
|
1807
|
+
debug7("Migrated v2 \u2192 v3: added riskPolicy, session config");
|
|
1808
|
+
return migrated;
|
|
1809
|
+
}
|
|
1810
|
+
function migration3to4(config, configPath) {
|
|
1811
|
+
const migrated = { ...config };
|
|
1812
|
+
migrated.version = "4";
|
|
1813
|
+
if (!migrated.extensions) {
|
|
1814
|
+
migrated.extensions = {
|
|
1815
|
+
enabled: [],
|
|
1816
|
+
permissionsMode: "strict"
|
|
1817
|
+
};
|
|
1818
|
+
}
|
|
1819
|
+
if (!migrated.security) {
|
|
1820
|
+
migrated.security = {
|
|
1821
|
+
secretsBackend: "file",
|
|
1822
|
+
encryptAtRest: false,
|
|
1823
|
+
sandboxModel: "process"
|
|
1824
|
+
};
|
|
1825
|
+
}
|
|
1826
|
+
if (!migrated.performance) {
|
|
1827
|
+
migrated.performance = {
|
|
1828
|
+
maxConcurrentAgents: "auto",
|
|
1829
|
+
workerThreadPoolSize: 4
|
|
1830
|
+
};
|
|
1831
|
+
}
|
|
1832
|
+
debug7("Migrated v3 \u2192 v4: added extensions, security, performance config");
|
|
1833
|
+
return migrated;
|
|
1834
|
+
}
|
|
1835
|
+
function migration4to5(config, configPath) {
|
|
1836
|
+
const migrated = { ...config };
|
|
1837
|
+
migrated.version = "5";
|
|
1838
|
+
if (!migrated.worktreeHierarchy) {
|
|
1839
|
+
migrated.worktreeHierarchy = {
|
|
1840
|
+
pinToCommitHash: true,
|
|
1841
|
+
serializeMerges: true,
|
|
1842
|
+
conflictEscalatesToApproval: true
|
|
1843
|
+
};
|
|
1844
|
+
}
|
|
1845
|
+
if (!migrated.costGuard) {
|
|
1846
|
+
migrated.costGuard = {
|
|
1847
|
+
trackByProvider: true,
|
|
1848
|
+
trackByProject: true,
|
|
1849
|
+
trackByTaskNode: false
|
|
1850
|
+
};
|
|
1851
|
+
}
|
|
1852
|
+
if (!migrated.logging) {
|
|
1853
|
+
migrated.logging = {
|
|
1854
|
+
level: "info"
|
|
1855
|
+
};
|
|
1856
|
+
}
|
|
1857
|
+
if (migrated.extensions && typeof migrated.extensions === "object") {
|
|
1858
|
+
const ext = migrated.extensions;
|
|
1859
|
+
if (ext.marketplaceIndex === void 0) {
|
|
1860
|
+
ext.marketplaceIndex = "https://marketplace.speexor.dev";
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
debug7("Migrated v4 \u2192 v5: added worktreeHierarchy, costGuard, logging");
|
|
1864
|
+
return migrated;
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
// src/config-migration/engine.ts
|
|
1868
|
+
var debug8 = Debug("speexor:config-migration:engine");
|
|
1869
|
+
var MigrationEngine = class {
|
|
1870
|
+
registry;
|
|
1871
|
+
constructor(registry) {
|
|
1872
|
+
this.registry = registry ?? this.buildDefaultRegistry();
|
|
1873
|
+
}
|
|
1874
|
+
detectVersion(configPath) {
|
|
1875
|
+
const absPath = resolve(configPath);
|
|
1876
|
+
if (!existsSync(absPath)) {
|
|
1877
|
+
throw new Error(`Config file not found: ${absPath}`);
|
|
1878
|
+
}
|
|
1879
|
+
const raw = readFileSync(absPath, "utf-8");
|
|
1880
|
+
const parsed = parse(raw);
|
|
1881
|
+
const version = parsed?.version;
|
|
1882
|
+
if (!version || typeof version !== "string") {
|
|
1883
|
+
throw new Error(
|
|
1884
|
+
`Config file ${absPath} is missing a valid 'version' field`
|
|
1885
|
+
);
|
|
1886
|
+
}
|
|
1887
|
+
debug8(`Detected version ${version} in ${absPath}`);
|
|
1888
|
+
return version;
|
|
1889
|
+
}
|
|
1890
|
+
needsMigration(configPath) {
|
|
1891
|
+
const from = this.detectVersion(configPath);
|
|
1892
|
+
if (from === "5") {
|
|
1893
|
+
return { needs: false, from, to: "5" };
|
|
1894
|
+
}
|
|
1895
|
+
return { needs: true, from, to: "5" };
|
|
1896
|
+
}
|
|
1897
|
+
previewMigration(configPath) {
|
|
1898
|
+
const absPath = resolve(configPath);
|
|
1899
|
+
const from = this.detectVersion(absPath);
|
|
1900
|
+
if (from === "5") {
|
|
1901
|
+
return `Config at ${absPath} is already at latest version (5). No migration needed.`;
|
|
1902
|
+
}
|
|
1903
|
+
const chain = this.registry.resolveChain(from, "5");
|
|
1904
|
+
const raw = readFileSync(absPath, "utf-8");
|
|
1905
|
+
const parsed = parse(raw);
|
|
1906
|
+
const lines = [];
|
|
1907
|
+
lines.push(`Migration preview for ${absPath}`);
|
|
1908
|
+
lines.push(` From: v${from}`);
|
|
1909
|
+
lines.push(` To: v5`);
|
|
1910
|
+
lines.push(
|
|
1911
|
+
` Steps: ${chain.map((m) => `${m.fromVersion}\u2192${m.toVersion}`).join(" \u2192 ")}`
|
|
1912
|
+
);
|
|
1913
|
+
lines.push("");
|
|
1914
|
+
let current = { ...parsed };
|
|
1915
|
+
for (const step of chain) {
|
|
1916
|
+
const beforeKeys = Object.keys(current).sort();
|
|
1917
|
+
const after = step.migrate({ ...current });
|
|
1918
|
+
const afterKeys = Object.keys(after).sort();
|
|
1919
|
+
const added = afterKeys.filter((k) => !beforeKeys.includes(k));
|
|
1920
|
+
const removed = beforeKeys.filter((k) => !afterKeys.includes(k));
|
|
1921
|
+
if (removed.length > 0) {
|
|
1922
|
+
lines.push(` v${step.fromVersion} \u2192 v${step.toVersion}:`);
|
|
1923
|
+
lines.push(` Removed keys: ${removed.join(", ")}`);
|
|
1924
|
+
}
|
|
1925
|
+
if (added.length > 0) {
|
|
1926
|
+
lines.push(` v${step.fromVersion} \u2192 v${step.toVersion}:`);
|
|
1927
|
+
for (const key of added) {
|
|
1928
|
+
lines.push(` + ${key}: ${JSON.stringify(after[key])}`);
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
if (added.length === 0 && removed.length === 0) {
|
|
1932
|
+
lines.push(
|
|
1933
|
+
` v${step.fromVersion} \u2192 v${step.toVersion}: (internal changes only)`
|
|
1934
|
+
);
|
|
1935
|
+
}
|
|
1936
|
+
current = after;
|
|
1937
|
+
}
|
|
1938
|
+
lines.push("");
|
|
1939
|
+
lines.push("Run applyMigration to execute this migration.");
|
|
1940
|
+
return lines.join("\n");
|
|
1941
|
+
}
|
|
1942
|
+
applyMigration(configPath) {
|
|
1943
|
+
const absPath = resolve(configPath);
|
|
1944
|
+
const from = this.detectVersion(absPath);
|
|
1945
|
+
if (from === "5") {
|
|
1946
|
+
return { success: true, backupPath: "" };
|
|
1947
|
+
}
|
|
1948
|
+
const chain = this.registry.resolveChain(from, "5");
|
|
1949
|
+
const raw = readFileSync(absPath, "utf-8");
|
|
1950
|
+
const parsed = parse(raw);
|
|
1951
|
+
let current = { ...parsed };
|
|
1952
|
+
let lastBackupPath = "";
|
|
1953
|
+
for (const step of chain) {
|
|
1954
|
+
const stepBackupPath = `${absPath}.v${step.fromVersion}-to-v${step.toVersion}.${Date.now()}.bak`;
|
|
1955
|
+
copyFileSync(absPath, stepBackupPath);
|
|
1956
|
+
lastBackupPath = stepBackupPath;
|
|
1957
|
+
const migrated = step.migrate(current);
|
|
1958
|
+
current = migrated;
|
|
1959
|
+
debug8(
|
|
1960
|
+
`Applied migration ${step.fromVersion} \u2192 ${step.toVersion}, backup at ${stepBackupPath}`
|
|
1961
|
+
);
|
|
1962
|
+
}
|
|
1963
|
+
const output = stringify(current, { indent: 2 });
|
|
1964
|
+
writeFileSync(absPath, output, "utf-8");
|
|
1965
|
+
debug8(`Written migrated config to ${absPath}`);
|
|
1966
|
+
return { success: true, backupPath: lastBackupPath };
|
|
1967
|
+
}
|
|
1968
|
+
rollbackMigration(backupPath) {
|
|
1969
|
+
const absBakPath = resolve(backupPath);
|
|
1970
|
+
if (!existsSync(absBakPath)) {
|
|
1971
|
+
throw new Error(`Backup file not found: ${absBakPath}`);
|
|
1972
|
+
}
|
|
1973
|
+
const configPath = absBakPath.replace(/\.v\d+-to-v\d+\.\d+\.bak$/, "").replace(/\.\d+\.bak$/, "");
|
|
1974
|
+
if (!existsSync(configPath)) {
|
|
1975
|
+
throw new Error(
|
|
1976
|
+
`Cannot determine config path from backup: ${absBakPath}`
|
|
1977
|
+
);
|
|
1978
|
+
}
|
|
1979
|
+
copyFileSync(absBakPath, configPath);
|
|
1980
|
+
debug8(`Restored ${configPath} from backup ${absBakPath}`);
|
|
1981
|
+
}
|
|
1982
|
+
buildDefaultRegistry() {
|
|
1983
|
+
const registry = new MigrationRegistry();
|
|
1984
|
+
registry.register({
|
|
1985
|
+
fromVersion: "1",
|
|
1986
|
+
toVersion: "2",
|
|
1987
|
+
migrate: (config) => migration1to2(config)
|
|
1988
|
+
});
|
|
1989
|
+
registry.register({
|
|
1990
|
+
fromVersion: "2",
|
|
1991
|
+
toVersion: "3",
|
|
1992
|
+
migrate: (config) => migration2to3(config)
|
|
1993
|
+
});
|
|
1994
|
+
registry.register({
|
|
1995
|
+
fromVersion: "3",
|
|
1996
|
+
toVersion: "4",
|
|
1997
|
+
migrate: (config) => migration3to4(config)
|
|
1998
|
+
});
|
|
1999
|
+
registry.register({
|
|
2000
|
+
fromVersion: "4",
|
|
2001
|
+
toVersion: "5",
|
|
2002
|
+
migrate: (config) => migration4to5(config)
|
|
2003
|
+
});
|
|
2004
|
+
return registry;
|
|
2005
|
+
}
|
|
2006
|
+
};
|
|
2007
|
+
async function configValidateCommand() {
|
|
2008
|
+
try {
|
|
2009
|
+
const cwd = process.cwd();
|
|
2010
|
+
const candidates = ["speexor.config.yaml", "speexor.config.yml", ".speexor.yaml", ".speexor.yml", "speexor.yaml"];
|
|
2011
|
+
let configPath;
|
|
2012
|
+
for (const c of candidates) {
|
|
2013
|
+
const full = join(cwd, c);
|
|
2014
|
+
if (existsSync(full)) {
|
|
2015
|
+
configPath = full;
|
|
2016
|
+
break;
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
if (!configPath) {
|
|
2020
|
+
console.error(`
|
|
2021
|
+
${chalk4.red("\u2716")} No speexor config found in ${cwd}
|
|
2022
|
+
`);
|
|
2023
|
+
return;
|
|
2024
|
+
}
|
|
2025
|
+
console.log(`
|
|
2026
|
+
${chalk4.bold("\u{1F50D} Config Validation")}`);
|
|
2027
|
+
console.log(` ${chalk4.dim(`Path: ${resolve(configPath)}`)}
|
|
2028
|
+
`);
|
|
2029
|
+
const raw = readFileSync(configPath, "utf-8");
|
|
2030
|
+
const parsed = parse(raw);
|
|
2031
|
+
const currentVersion = parsed?.version ?? "unknown";
|
|
2032
|
+
console.log(` Schema version: ${chalk4.cyan(currentVersion)}`);
|
|
2033
|
+
console.log(` Latest version: ${chalk4.green("5")}`);
|
|
2034
|
+
const engine = new MigrationEngine();
|
|
2035
|
+
const detected = engine.detectVersion(configPath);
|
|
2036
|
+
const migrationStatus = engine.needsMigration(configPath);
|
|
2037
|
+
if (migrationStatus.needs) {
|
|
2038
|
+
console.log(`
|
|
2039
|
+
${chalk4.yellow("\u26A0")} Version mismatch: v${detected} \u2192 v5`);
|
|
2040
|
+
console.log(` ${chalk4.dim("Run migration to update config to latest schema.")}`);
|
|
2041
|
+
console.log();
|
|
2042
|
+
console.log(` ${chalk4.cyan("Preview:")}`);
|
|
2043
|
+
console.log(` ${engine.previewMigration(configPath).replace(/\n/g, "\n ")}`);
|
|
2044
|
+
console.log();
|
|
2045
|
+
console.log(` Run ${chalk4.bold("speexor config migrate")} to apply.
|
|
2046
|
+
`);
|
|
2047
|
+
} else {
|
|
2048
|
+
console.log(` ${chalk4.green("\u2713")} Config is at latest version
|
|
2049
|
+
`);
|
|
2050
|
+
}
|
|
2051
|
+
try {
|
|
2052
|
+
validateConfig(parsed);
|
|
2053
|
+
console.log(` ${chalk4.green("\u2713")} Schema validation passed
|
|
2054
|
+
`);
|
|
2055
|
+
} catch (schemaError) {
|
|
2056
|
+
console.log(` ${chalk4.red("\u2716")} Schema validation failed:
|
|
2057
|
+
`);
|
|
2058
|
+
const msg = schemaError.message;
|
|
2059
|
+
for (const line of msg.split("\n")) {
|
|
2060
|
+
console.log(` ${chalk4.red(line)}`);
|
|
2061
|
+
}
|
|
2062
|
+
console.log();
|
|
2063
|
+
}
|
|
2064
|
+
} catch (error) {
|
|
2065
|
+
if (error instanceof Error) {
|
|
2066
|
+
console.error(`
|
|
2067
|
+
${chalk4.red("\u2716")} ${error.message}
|
|
2068
|
+
`);
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
var debug9 = Debug("speexor:eval:decision-log");
|
|
2073
|
+
var DecisionLog = class {
|
|
2074
|
+
entries = [];
|
|
2075
|
+
logPath;
|
|
2076
|
+
constructor(baseDir) {
|
|
2077
|
+
const dir = baseDir ?? join(process.cwd(), ".speexor");
|
|
2078
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
2079
|
+
this.logPath = join(dir, "decision-log.jsonl");
|
|
2080
|
+
this.load();
|
|
2081
|
+
}
|
|
2082
|
+
load() {
|
|
2083
|
+
try {
|
|
2084
|
+
if (existsSync(this.logPath)) {
|
|
2085
|
+
const lines = readFileSync(this.logPath, "utf-8").split("\n").filter(Boolean);
|
|
2086
|
+
for (const line of lines) {
|
|
2087
|
+
try {
|
|
2088
|
+
const entry = JSON.parse(line);
|
|
2089
|
+
entry.createdAt = new Date(entry.createdAt);
|
|
2090
|
+
this.entries.push(entry);
|
|
2091
|
+
} catch {
|
|
2092
|
+
debug9("Skipping malformed log line");
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
} catch {
|
|
2097
|
+
debug9("Failed to load decision log, starting fresh");
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
record(entry) {
|
|
2101
|
+
this.entries.push(entry);
|
|
2102
|
+
appendFileSync(this.logPath, JSON.stringify(entry) + "\n");
|
|
2103
|
+
debug9(`Decision recorded: ${entry.id} (confidence: ${entry.confidence})`);
|
|
2104
|
+
}
|
|
2105
|
+
getEntries(filter) {
|
|
2106
|
+
let result = this.entries;
|
|
2107
|
+
if (filter) {
|
|
2108
|
+
if (filter.taskId)
|
|
2109
|
+
result = result.filter((e) => e.taskId === filter.taskId);
|
|
2110
|
+
if (filter.agentId)
|
|
2111
|
+
result = result.filter((e) => e.agentId === filter.agentId);
|
|
2112
|
+
if (filter.confidenceMin !== void 0)
|
|
2113
|
+
result = result.filter((e) => e.confidence >= filter.confidenceMin);
|
|
2114
|
+
if (filter.confidenceMax !== void 0)
|
|
2115
|
+
result = result.filter((e) => e.confidence <= filter.confidenceMax);
|
|
2116
|
+
}
|
|
2117
|
+
return [...result];
|
|
2118
|
+
}
|
|
2119
|
+
getUnlabeledEntries() {
|
|
2120
|
+
return this.entries.filter((e) => !e.outcome || e.outcome === "unknown");
|
|
2121
|
+
}
|
|
2122
|
+
getEntriesByTimeRange(from, to) {
|
|
2123
|
+
return this.entries.filter((e) => e.createdAt >= from && e.createdAt <= to);
|
|
2124
|
+
}
|
|
2125
|
+
};
|
|
2126
|
+
var debug10 = Debug("speexor:eval:calibration");
|
|
2127
|
+
var CalibrationAnalyzer = class {
|
|
2128
|
+
analyze(entries) {
|
|
2129
|
+
const buckets = [];
|
|
2130
|
+
const boundaries = [0, 0.2, 0.4, 0.6, 0.8, 1];
|
|
2131
|
+
for (let i = 0; i < boundaries.length - 1; i++) {
|
|
2132
|
+
const lo = boundaries[i];
|
|
2133
|
+
const hi = boundaries[i + 1];
|
|
2134
|
+
const inRange = entries.filter(
|
|
2135
|
+
(e) => e.confidence >= lo && (i === boundaries.length - 2 ? e.confidence <= hi : e.confidence < hi)
|
|
2136
|
+
);
|
|
2137
|
+
const labeled = inRange.filter(
|
|
2138
|
+
(e) => e.outcome && e.outcome !== "unknown"
|
|
2139
|
+
);
|
|
2140
|
+
const correct = labeled.filter((e) => e.outcome === "correct");
|
|
2141
|
+
const range = `${lo.toFixed(1)}-${hi.toFixed(1)}`;
|
|
2142
|
+
buckets.push({
|
|
2143
|
+
range,
|
|
2144
|
+
count: inRange.length,
|
|
2145
|
+
correctRate: labeled.length > 0 ? correct.length / labeled.length : 0
|
|
2146
|
+
});
|
|
2147
|
+
}
|
|
2148
|
+
const labeledEntries = entries.filter(
|
|
2149
|
+
(e) => e.outcome && e.outcome !== "unknown"
|
|
2150
|
+
);
|
|
2151
|
+
const correctEntries = labeledEntries.filter(
|
|
2152
|
+
(e) => e.outcome === "correct"
|
|
2153
|
+
);
|
|
2154
|
+
const report = {
|
|
2155
|
+
buckets,
|
|
2156
|
+
overallAccuracy: labeledEntries.length > 0 ? correctEntries.length / labeledEntries.length : 0,
|
|
2157
|
+
totalDecisions: entries.length,
|
|
2158
|
+
labeledDecisions: labeledEntries.length
|
|
2159
|
+
};
|
|
2160
|
+
debug10(
|
|
2161
|
+
`Calibration report: ${report.labeledDecisions}/${report.totalDecisions} labeled, accuracy ${(report.overallAccuracy * 100).toFixed(1)}%`
|
|
2162
|
+
);
|
|
2163
|
+
return report;
|
|
2164
|
+
}
|
|
2165
|
+
getUnderConfidenceAreas(report) {
|
|
2166
|
+
const areas = [];
|
|
2167
|
+
for (const bucket of report.buckets) {
|
|
2168
|
+
const [lo, hi] = bucket.range.split("-").map(Number);
|
|
2169
|
+
if (hi === 0) continue;
|
|
2170
|
+
const midConfidence = (lo + hi) / 2;
|
|
2171
|
+
if (bucket.count > 0 && bucket.correctRate > midConfidence + 0.1) {
|
|
2172
|
+
areas.push(
|
|
2173
|
+
`Bucket ${bucket.range}: correctRate=${(bucket.correctRate * 100).toFixed(1)}% > midConfidence=${(midConfidence * 100).toFixed(1)}% (underconfident)`
|
|
2174
|
+
);
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
return areas;
|
|
2178
|
+
}
|
|
2179
|
+
getOverConfidenceAreas(report) {
|
|
2180
|
+
const areas = [];
|
|
2181
|
+
for (const bucket of report.buckets) {
|
|
2182
|
+
const [lo, hi] = bucket.range.split("-").map(Number);
|
|
2183
|
+
if (hi === 0) continue;
|
|
2184
|
+
const midConfidence = (lo + hi) / 2;
|
|
2185
|
+
if (bucket.count > 0 && bucket.correctRate < midConfidence - 0.1) {
|
|
2186
|
+
areas.push(
|
|
2187
|
+
`Bucket ${bucket.range}: correctRate=${(bucket.correctRate * 100).toFixed(1)}% < midConfidence=${(midConfidence * 100).toFixed(1)}% (overconfident)`
|
|
2188
|
+
);
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
return areas;
|
|
2192
|
+
}
|
|
2193
|
+
};
|
|
2194
|
+
var debug11 = Debug("speexor:eval:evaluator");
|
|
2195
|
+
var DecisionEvaluator = class {
|
|
2196
|
+
log;
|
|
2197
|
+
analyzer;
|
|
2198
|
+
subscribers = /* @__PURE__ */ new Set();
|
|
2199
|
+
constructor(baseDir) {
|
|
2200
|
+
this.log = new DecisionLog(baseDir);
|
|
2201
|
+
this.analyzer = new CalibrationAnalyzer();
|
|
2202
|
+
}
|
|
2203
|
+
getDecisionLog() {
|
|
2204
|
+
return this.log;
|
|
2205
|
+
}
|
|
2206
|
+
async runEvaluation() {
|
|
2207
|
+
const unlabeled = this.log.getUnlabeledEntries();
|
|
2208
|
+
debug11(`Running evaluation on ${unlabeled.length} unlabeled entries`);
|
|
2209
|
+
return this.getReport();
|
|
2210
|
+
}
|
|
2211
|
+
labelDecision(entryId, outcome) {
|
|
2212
|
+
const all = this.log.getEntries();
|
|
2213
|
+
const entry = all.find((e) => e.id === entryId);
|
|
2214
|
+
if (entry) {
|
|
2215
|
+
entry.outcome = outcome;
|
|
2216
|
+
entry.outcomeLabeledBy = "user";
|
|
2217
|
+
debug11(`Decision ${entryId} labeled as ${outcome} by user`);
|
|
2218
|
+
this.notify();
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
async autoLabelWithLLM(entry) {
|
|
2222
|
+
debug11(`Auto-labeling decision ${entry.id} via LLM judge`);
|
|
2223
|
+
return null;
|
|
2224
|
+
}
|
|
2225
|
+
getReport() {
|
|
2226
|
+
const entries = this.log.getEntries();
|
|
2227
|
+
return this.analyzer.analyze(entries);
|
|
2228
|
+
}
|
|
2229
|
+
subscribe(handler) {
|
|
2230
|
+
this.subscribers.add(handler);
|
|
2231
|
+
return () => {
|
|
2232
|
+
this.subscribers.delete(handler);
|
|
2233
|
+
};
|
|
2234
|
+
}
|
|
2235
|
+
notify() {
|
|
2236
|
+
const report = this.getReport();
|
|
2237
|
+
for (const handler of this.subscribers) {
|
|
2238
|
+
try {
|
|
2239
|
+
handler(report);
|
|
2240
|
+
} catch {
|
|
2241
|
+
debug11("Subscriber handler threw");
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
};
|
|
2246
|
+
async function evalDecisionsCommand() {
|
|
2247
|
+
try {
|
|
2248
|
+
const evaluator = new DecisionEvaluator();
|
|
2249
|
+
const report = await evaluator.runEvaluation();
|
|
2250
|
+
console.log(`
|
|
2251
|
+
${chalk4.bold("\u{1F4CA} Decision Quality Evaluation")}
|
|
2252
|
+
`);
|
|
2253
|
+
console.log(` ${chalk4.dim(`Total decisions: ${report.totalDecisions}`)}`);
|
|
2254
|
+
console.log(` ${chalk4.dim(`Labeled decisions: ${report.labeledDecisions}`)}`);
|
|
2255
|
+
console.log(` Overall accuracy: ${chalk4.cyan(`${(report.overallAccuracy * 100).toFixed(1)}%`)}
|
|
2256
|
+
`);
|
|
2257
|
+
if (report.buckets.length > 0) {
|
|
2258
|
+
console.log(` ${chalk4.bold("Calibration Report:")}
|
|
2259
|
+
`);
|
|
2260
|
+
console.log(` ${chalk4.dim("Confidence Range | Count | Correct Rate")}`);
|
|
2261
|
+
console.log(` ${chalk4.dim("\u2500".repeat(45))}`);
|
|
2262
|
+
for (const bucket of report.buckets) {
|
|
2263
|
+
const bar = "\u2588".repeat(Math.round(bucket.correctRate * 20));
|
|
2264
|
+
const mid = bucket.range.split("-").map(Number);
|
|
2265
|
+
const midConf = ((mid[0] ?? 0) + (mid[1] ?? 1)) / 2;
|
|
2266
|
+
let color = chalk4.green;
|
|
2267
|
+
if (bucket.count > 0) {
|
|
2268
|
+
if (bucket.correctRate < midConf - 0.1) color = chalk4.red;
|
|
2269
|
+
else if (bucket.correctRate > midConf + 0.1) color = chalk4.yellow;
|
|
2270
|
+
}
|
|
2271
|
+
const rateStr = bucket.count > 0 ? `${(bucket.correctRate * 100).toFixed(0).padStart(3)}%` : " N/A";
|
|
2272
|
+
console.log(` ${`${bucket.range.padEnd(17)}`}\u2502 ${String(bucket.count).padStart(4)} \u2502 ${color(rateStr)} ${bar}`);
|
|
2273
|
+
}
|
|
2274
|
+
console.log();
|
|
2275
|
+
}
|
|
2276
|
+
} catch (error) {
|
|
2277
|
+
if (error instanceof Error) {
|
|
2278
|
+
console.error(`
|
|
2279
|
+
${chalk4.red("\u2716")} ${error.message}
|
|
2280
|
+
`);
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
260
2284
|
var __dirname$1 = dirname(fileURLToPath(import.meta.url));
|
|
261
2285
|
function getVersion() {
|
|
262
2286
|
try {
|
|
@@ -271,10 +2295,10 @@ function getVersion() {
|
|
|
271
2295
|
}
|
|
272
2296
|
var program = new Command();
|
|
273
2297
|
program.name("speexor").description("Agent Orchestrator for multi-AI coding agent orchestration (Speexor)").version(getVersion());
|
|
274
|
-
program.command("start [repo]").description("Initialize a project and start the dashboard").option("-p, --port <port>", "Dashboard port", "
|
|
2298
|
+
program.command("start [repo]").description("Initialize a project and start the dashboard").option("-p, --port <port>", "Dashboard port", "7777").option("-n, --name <name>", "Project name").option("--no-dashboard", "Skip starting dashboard").action(startCommand);
|
|
275
2299
|
program.command("agent").description("Manage agents").addCommand(
|
|
276
2300
|
new Command("spawn").description("Spawn a new agent for a task").requiredOption("-t, --task <id>", "Task ID").option("-a, --agent <type>", "Agent type (opencode, claude-code, aider, codex)", "opencode").action(async (opts) => {
|
|
277
|
-
const { agentSpawnCommand } = await import('../agent-
|
|
2301
|
+
const { agentSpawnCommand } = await import('../agent-C64T66XT.js');
|
|
278
2302
|
await agentSpawnCommand(opts);
|
|
279
2303
|
})
|
|
280
2304
|
);
|
|
@@ -282,6 +2306,22 @@ program.command("list").description("List all projects and active agent statuses
|
|
|
282
2306
|
program.command("stop <session-id>").description("Stop an agent session safely").action(stopCommand);
|
|
283
2307
|
program.command("logs <session-id>").description("Tail logs for an agent session").option("-f, --follow", "Follow log output").option("-n, --lines <count>", "Number of lines", "50").action(logsCommand);
|
|
284
2308
|
program.command("config-help").description("Show full config schema reference").action(configHelpCommand);
|
|
2309
|
+
program.command("task").description("Submit and manage tasks").addCommand(
|
|
2310
|
+
new Command("submit").description("Submit a task description for decomposition into a task graph").argument("<description>", "Task description").action(taskSubmitCommand)
|
|
2311
|
+
);
|
|
2312
|
+
program.command("ext").description("Manage extensions").addCommand(
|
|
2313
|
+
new Command("search").description("Search the extension marketplace").argument("<query>", "Search query").action(extSearchCommand)
|
|
2314
|
+
).addCommand(
|
|
2315
|
+
new Command("install").description("Install an extension from the marketplace").argument("<name>", "Extension name").action(extInstallCommand)
|
|
2316
|
+
).addCommand(new Command("list").description("List installed extensions").action(extListCommand)).addCommand(
|
|
2317
|
+
new Command("update").description("Update installed extension(s)").argument("[name]", "Extension name (omit to update all)").action(extUpdateCommand)
|
|
2318
|
+
).addCommand(
|
|
2319
|
+
new Command("remove").description("Remove an installed extension").argument("<name>", "Extension name").action(extRemoveCommand)
|
|
2320
|
+
);
|
|
2321
|
+
program.command("config").description("Manage configuration").addCommand(new Command("validate").description("Validate current config against schema and check version").action(configValidateCommand));
|
|
2322
|
+
program.command("eval").description("Run evaluations and reports").addCommand(
|
|
2323
|
+
new Command("decisions").description("Run Decision Quality evaluation and show calibration report").action(evalDecisionsCommand)
|
|
2324
|
+
);
|
|
285
2325
|
program.parse(process.argv);
|
|
286
2326
|
//# sourceMappingURL=index.js.map
|
|
287
2327
|
//# sourceMappingURL=index.js.map
|