agent-dealer 0.0.0 → 0.1.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/bundle/server/dist/adapters/agent-deck.js +136 -0
- package/bundle/server/dist/adapters/agent-health.js +109 -0
- package/bundle/server/dist/adapters/external.js +21 -0
- package/bundle/server/dist/adapters/linear-inbox.js +112 -0
- package/bundle/server/dist/adapters/linear-sync.js +197 -0
- package/bundle/server/dist/cli-env.js +62 -0
- package/bundle/server/dist/config/load-env.js +78 -0
- package/bundle/server/dist/db/index.js +120 -0
- package/bundle/server/dist/db/migrate.js +6 -0
- package/bundle/server/dist/db/schema.sql +84 -0
- package/bundle/server/dist/index.js +30 -0
- package/bundle/server/dist/intake/agent-routing.js +32 -0
- package/bundle/server/dist/paths.js +16 -0
- package/bundle/server/dist/queue/dispatcher.js +305 -0
- package/bundle/server/dist/repository/agents.js +81 -0
- package/bundle/server/dist/repository/intake-settings.js +124 -0
- package/bundle/server/dist/repository/runs.js +421 -0
- package/bundle/server/dist/routes/index.js +552 -0
- package/bundle/server/dist/runners/claude.js +122 -0
- package/bundle/server/dist/runners/models.js +138 -0
- package/bundle/server/dist/runners/persist.js +100 -0
- package/bundle/server/dist/runners/prompts.js +164 -0
- package/bundle/server/dist/runners/reflect.js +77 -0
- package/bundle/server/dist/runners/run-context.js +168 -0
- package/bundle/server/dist/runners/stream-json.js +157 -0
- package/bundle/server/dist/static-ui.js +30 -0
- package/bundle/server/dist/trace-summary.js +93 -0
- package/bundle/server/dist/usage-summary.js +49 -0
- package/bundle/server/package.json +38 -0
- package/bundle/server/static-ui/apple-touch-icon.png +0 -0
- package/bundle/server/static-ui/assets/index-BPfTEO0-.js +77 -0
- package/bundle/server/static-ui/assets/index-Blmq-P5I.css +1 -0
- package/bundle/server/static-ui/favicon.png +0 -0
- package/bundle/server/static-ui/fonts/Monaco.ttf +0 -0
- package/bundle/server/static-ui/index.html +17 -0
- package/bundle/shared/dist/agents.d.ts +289 -0
- package/bundle/shared/dist/agents.js +55 -0
- package/bundle/shared/dist/execution.d.ts +14 -0
- package/bundle/shared/dist/execution.js +40 -0
- package/bundle/shared/dist/index.d.ts +2223 -0
- package/bundle/shared/dist/index.js +325 -0
- package/bundle/shared/dist/runtime.d.ts +3 -0
- package/bundle/shared/dist/runtime.js +2 -0
- package/bundle/shared/package.json +32 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +8 -0
- package/dist/cli-check.d.ts +6 -0
- package/dist/cli-check.js +29 -0
- package/dist/doctor.d.ts +1 -0
- package/dist/doctor.js +74 -0
- package/dist/env.d.ts +11 -0
- package/dist/env.js +39 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +69 -0
- package/dist/paths.d.ts +6 -0
- package/dist/paths.js +36 -0
- package/dist/postinstall.d.ts +1 -0
- package/dist/postinstall.js +19 -0
- package/dist/setup.d.ts +6 -0
- package/dist/setup.js +29 -0
- package/dist/start.d.ts +6 -0
- package/dist/start.js +75 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +10 -0
- package/package.json +35 -4
- package/templates/prod.env.example +24 -0
- package/README.md +0 -7
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import dotenv from "dotenv";
|
|
6
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const repoRoot = path.resolve(__dirname, "../../../..");
|
|
8
|
+
const DEFAULTS = {
|
|
9
|
+
development: {
|
|
10
|
+
home: path.join(os.homedir(), ".agent-dealer-dev"),
|
|
11
|
+
port: "3221",
|
|
12
|
+
webPort: "3222",
|
|
13
|
+
webUrl: "http://localhost:3222",
|
|
14
|
+
apiUrl: "http://127.0.0.1:3221",
|
|
15
|
+
},
|
|
16
|
+
production: {
|
|
17
|
+
home: path.join(os.homedir(), ".agent-dealer"),
|
|
18
|
+
port: "2221",
|
|
19
|
+
webPort: "2222",
|
|
20
|
+
webUrl: "http://localhost:2222",
|
|
21
|
+
apiUrl: "http://127.0.0.1:2221",
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
export function getRepoRoot() {
|
|
25
|
+
return repoRoot;
|
|
26
|
+
}
|
|
27
|
+
export function resolveAgentDealerEnv() {
|
|
28
|
+
const raw = process.env.AGENT_DEALER_ENV ?? process.env.NODE_ENV;
|
|
29
|
+
return raw === "production" ? "production" : "development";
|
|
30
|
+
}
|
|
31
|
+
function homeDirForMode(mode) {
|
|
32
|
+
return DEFAULTS[mode].home;
|
|
33
|
+
}
|
|
34
|
+
export function resolveEnvFilePath(mode) {
|
|
35
|
+
return path.join(homeDirForMode(mode), ".env");
|
|
36
|
+
}
|
|
37
|
+
function envFileForMode(mode) {
|
|
38
|
+
return resolveEnvFilePath(mode);
|
|
39
|
+
}
|
|
40
|
+
function setDefault(key, value) {
|
|
41
|
+
if (process.env[key] === undefined || process.env[key] === "") {
|
|
42
|
+
process.env[key] = value;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function applyDefaults(mode) {
|
|
46
|
+
const d = DEFAULTS[mode];
|
|
47
|
+
setDefault("AGENT_DEALER_HOME", d.home);
|
|
48
|
+
setDefault("PORT", d.port);
|
|
49
|
+
setDefault("WEB_PORT", d.webPort);
|
|
50
|
+
setDefault("AGENT_DEALER_WEB_URL", d.webUrl);
|
|
51
|
+
setDefault("AGENT_DEALER_API", d.apiUrl);
|
|
52
|
+
}
|
|
53
|
+
function shortenHome(p) {
|
|
54
|
+
const home = os.homedir();
|
|
55
|
+
return p.startsWith(home) ? `~${p.slice(home.length)}` : p;
|
|
56
|
+
}
|
|
57
|
+
export function formatEnvStartupLine(mode, envFile) {
|
|
58
|
+
const home = process.env.AGENT_DEALER_HOME ?? DEFAULTS[mode].home;
|
|
59
|
+
const port = process.env.PORT ?? DEFAULTS[mode].port;
|
|
60
|
+
const envLabel = fs.existsSync(envFile) ? envFile : `${envFile} (missing)`;
|
|
61
|
+
return `agent-dealer [${mode}] env=${envLabel} home=${shortenHome(home)} port=${port}`;
|
|
62
|
+
}
|
|
63
|
+
export function loadAgentDealerEnv() {
|
|
64
|
+
const mode = resolveAgentDealerEnv();
|
|
65
|
+
const envFile = envFileForMode(mode);
|
|
66
|
+
if (fs.existsSync(envFile)) {
|
|
67
|
+
dotenv.config({ path: envFile });
|
|
68
|
+
}
|
|
69
|
+
else if (mode === "development") {
|
|
70
|
+
const legacy = path.resolve(repoRoot, ".env");
|
|
71
|
+
if (fs.existsSync(legacy)) {
|
|
72
|
+
dotenv.config({ path: legacy });
|
|
73
|
+
console.warn(`agent-dealer: loaded legacy ${legacy} — move to ${envFile} (see scripts/templates/dev.env.example)`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
applyDefaults(mode);
|
|
77
|
+
return { mode, envFile };
|
|
78
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import Database from "better-sqlite3";
|
|
5
|
+
import { readFileSync } from "node:fs";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { BUILTIN_AGENT_CLAUDE_ID, BUILTIN_AGENT_CURSOR_ID, CURSOR_DEFAULT_MODEL, } from "@agent-dealer/shared";
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
export function getDataDir() {
|
|
10
|
+
const home = process.env.AGENT_DEALER_HOME ?? path.join(os.homedir(), ".agent-dealer");
|
|
11
|
+
fs.mkdirSync(home, { recursive: true });
|
|
12
|
+
return home;
|
|
13
|
+
}
|
|
14
|
+
export function getDbPath() {
|
|
15
|
+
return path.join(getDataDir(), "dealer.db");
|
|
16
|
+
}
|
|
17
|
+
let dbInstance = null;
|
|
18
|
+
export function getDb() {
|
|
19
|
+
if (!dbInstance) {
|
|
20
|
+
dbInstance = new Database(getDbPath());
|
|
21
|
+
dbInstance.pragma("journal_mode = WAL");
|
|
22
|
+
dbInstance.pragma("foreign_keys = ON");
|
|
23
|
+
}
|
|
24
|
+
return dbInstance;
|
|
25
|
+
}
|
|
26
|
+
export function migrate() {
|
|
27
|
+
const db = getDb();
|
|
28
|
+
const schema = readFileSync(path.join(__dirname, "schema.sql"), "utf8");
|
|
29
|
+
db.exec(schema);
|
|
30
|
+
const runCols = db.prepare("PRAGMA table_info(runs)").all();
|
|
31
|
+
if (!runCols.some((c) => c.name === "agent_id")) {
|
|
32
|
+
db.exec("ALTER TABLE runs ADD COLUMN agent_id TEXT REFERENCES agents(id)");
|
|
33
|
+
db.exec("ALTER TABLE runs ADD COLUMN agent_name TEXT");
|
|
34
|
+
}
|
|
35
|
+
const agentCols = db.prepare("PRAGMA table_info(agents)").all();
|
|
36
|
+
if (!agentCols.some((c) => c.name === "workspace_root")) {
|
|
37
|
+
db.exec("ALTER TABLE agents ADD COLUMN workspace_root TEXT");
|
|
38
|
+
}
|
|
39
|
+
const runCols2 = db.prepare("PRAGMA table_info(runs)").all();
|
|
40
|
+
if (!runCols2.some((c) => c.name === "external_label")) {
|
|
41
|
+
db.exec("ALTER TABLE runs ADD COLUMN external_label TEXT");
|
|
42
|
+
}
|
|
43
|
+
const agentCols2 = db.prepare("PRAGMA table_info(agents)").all();
|
|
44
|
+
if (!agentCols2.some((c) => c.name === "default_plan_model")) {
|
|
45
|
+
db.exec("ALTER TABLE agents ADD COLUMN default_plan_model TEXT");
|
|
46
|
+
db.exec("ALTER TABLE agents ADD COLUMN default_execute_model TEXT");
|
|
47
|
+
}
|
|
48
|
+
const runCols3 = db.prepare("PRAGMA table_info(runs)").all();
|
|
49
|
+
if (!runCols3.some((c) => c.name === "plan_model")) {
|
|
50
|
+
db.exec("ALTER TABLE runs ADD COLUMN plan_model TEXT");
|
|
51
|
+
db.exec("ALTER TABLE runs ADD COLUMN execute_model TEXT");
|
|
52
|
+
}
|
|
53
|
+
seedBuiltinAgents(db);
|
|
54
|
+
seedIntakeSettings(db);
|
|
55
|
+
migrateLegacyAgentDeckPort(db);
|
|
56
|
+
// Default agents are normal rows — clear legacy built-in flag.
|
|
57
|
+
db.exec("UPDATE agents SET is_builtin = 0 WHERE is_builtin = 1");
|
|
58
|
+
}
|
|
59
|
+
function migrateLegacyAgentDeckPort(db) {
|
|
60
|
+
const row = db
|
|
61
|
+
.prepare("SELECT value_json FROM intake_settings WHERE key = ?")
|
|
62
|
+
.get("agentDeck.port");
|
|
63
|
+
if (!row)
|
|
64
|
+
return;
|
|
65
|
+
try {
|
|
66
|
+
const port = JSON.parse(row.value_json);
|
|
67
|
+
if (port === 11111) {
|
|
68
|
+
db.prepare("UPDATE intake_settings SET value_json = ? WHERE key = ?").run("1111", "agentDeck.port");
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
/* ignore */
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function seedIntakeSettings(db) {
|
|
76
|
+
const stateFilter = (process.env.LINEAR_STATE_FILTER ?? "Todo")
|
|
77
|
+
.split(",")
|
|
78
|
+
.map((s) => s.trim())
|
|
79
|
+
.filter(Boolean);
|
|
80
|
+
const teamId = process.env.LINEAR_TEAM_ID ?? null;
|
|
81
|
+
const defaults = {
|
|
82
|
+
"linear.stateFilter": stateFilter.length > 0 ? stateFilter : ["Todo"],
|
|
83
|
+
"linear.teamId": teamId,
|
|
84
|
+
"linear.assigneeMe": true,
|
|
85
|
+
"linear.defaultAgentId": null,
|
|
86
|
+
"linear.syncEnabled": true,
|
|
87
|
+
"linear.routingRules": [],
|
|
88
|
+
"agentDeck.host": "127.0.0.1",
|
|
89
|
+
"agentDeck.port": 1111,
|
|
90
|
+
};
|
|
91
|
+
const deckFromEnv = process.env.AGENT_DECK_API_URL;
|
|
92
|
+
if (deckFromEnv) {
|
|
93
|
+
try {
|
|
94
|
+
const u = new URL(deckFromEnv);
|
|
95
|
+
defaults["agentDeck.host"] = u.hostname;
|
|
96
|
+
defaults["agentDeck.port"] = u.port ? Number(u.port) : 1111;
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
/* keep defaults */
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const insert = db.prepare("INSERT OR IGNORE INTO intake_settings (key, value_json) VALUES (?, ?)");
|
|
103
|
+
for (const [key, value] of Object.entries(defaults)) {
|
|
104
|
+
insert.run(key, JSON.stringify(value));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function seedBuiltinAgents(db) {
|
|
108
|
+
const now = new Date().toISOString();
|
|
109
|
+
const insert = db.prepare(`
|
|
110
|
+
INSERT OR IGNORE INTO agents (id, name, runtime, deck_id, deck_name, playbook_id, is_builtin, created_at, updated_at)
|
|
111
|
+
VALUES (?, ?, ?, NULL, NULL, NULL, 0, ?, ?)
|
|
112
|
+
`);
|
|
113
|
+
insert.run(BUILTIN_AGENT_CLAUDE_ID, "Claude", "claude_code", now, now);
|
|
114
|
+
insert.run(BUILTIN_AGENT_CURSOR_ID, "Cursor", "cursor_local", now, now);
|
|
115
|
+
db.prepare(`UPDATE agents SET
|
|
116
|
+
default_plan_model = COALESCE(default_plan_model, ?),
|
|
117
|
+
default_execute_model = COALESCE(default_execute_model, ?),
|
|
118
|
+
updated_at = ?
|
|
119
|
+
WHERE id = ? AND runtime = 'cursor_local'`).run(CURSOR_DEFAULT_MODEL, CURSOR_DEFAULT_MODEL, now, BUILTIN_AGENT_CURSOR_ID);
|
|
120
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { formatEnvStartupLine, loadAgentDealerEnv, } from "../config/load-env.js";
|
|
2
|
+
import { migrate, getDbPath } from "./index.js";
|
|
3
|
+
const { mode, envFile } = loadAgentDealerEnv();
|
|
4
|
+
console.log(formatEnvStartupLine(mode, envFile));
|
|
5
|
+
migrate();
|
|
6
|
+
console.log("Database migrated:", getDbPath());
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS agents (
|
|
2
|
+
id TEXT PRIMARY KEY,
|
|
3
|
+
name TEXT NOT NULL,
|
|
4
|
+
runtime TEXT NOT NULL,
|
|
5
|
+
deck_id TEXT,
|
|
6
|
+
deck_name TEXT,
|
|
7
|
+
playbook_id TEXT,
|
|
8
|
+
workspace_root TEXT,
|
|
9
|
+
default_plan_model TEXT,
|
|
10
|
+
default_execute_model TEXT,
|
|
11
|
+
is_builtin INTEGER NOT NULL DEFAULT 0,
|
|
12
|
+
created_at TEXT NOT NULL,
|
|
13
|
+
updated_at TEXT NOT NULL
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
CREATE INDEX IF NOT EXISTS idx_agents_name ON agents(name);
|
|
17
|
+
|
|
18
|
+
CREATE TABLE IF NOT EXISTS runs (
|
|
19
|
+
id TEXT PRIMARY KEY,
|
|
20
|
+
source TEXT NOT NULL,
|
|
21
|
+
external_id TEXT,
|
|
22
|
+
external_label TEXT,
|
|
23
|
+
task_category TEXT NOT NULL,
|
|
24
|
+
title TEXT NOT NULL,
|
|
25
|
+
description TEXT,
|
|
26
|
+
repo TEXT,
|
|
27
|
+
artifact_workspace TEXT,
|
|
28
|
+
agent_id TEXT REFERENCES agents(id),
|
|
29
|
+
agent_name TEXT,
|
|
30
|
+
deck_id TEXT,
|
|
31
|
+
deck_name TEXT,
|
|
32
|
+
playbook_id TEXT,
|
|
33
|
+
runtime TEXT,
|
|
34
|
+
plan_model TEXT,
|
|
35
|
+
execute_model TEXT,
|
|
36
|
+
status TEXT NOT NULL,
|
|
37
|
+
lineage_id TEXT,
|
|
38
|
+
acceptance_criteria TEXT,
|
|
39
|
+
approval_gates_json TEXT,
|
|
40
|
+
budget_json TEXT,
|
|
41
|
+
created_at TEXT NOT NULL,
|
|
42
|
+
updated_at TEXT NOT NULL
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
CREATE INDEX IF NOT EXISTS idx_runs_status ON runs(status);
|
|
46
|
+
CREATE INDEX IF NOT EXISTS idx_runs_external ON runs(source, external_id);
|
|
47
|
+
|
|
48
|
+
CREATE TABLE IF NOT EXISTS artifacts (
|
|
49
|
+
id TEXT PRIMARY KEY,
|
|
50
|
+
run_id TEXT NOT NULL REFERENCES runs(id),
|
|
51
|
+
kind TEXT NOT NULL,
|
|
52
|
+
content_json TEXT,
|
|
53
|
+
blob_path TEXT,
|
|
54
|
+
author TEXT NOT NULL,
|
|
55
|
+
created_at TEXT NOT NULL
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
CREATE INDEX IF NOT EXISTS idx_artifacts_run ON artifacts(run_id);
|
|
59
|
+
|
|
60
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
61
|
+
id TEXT PRIMARY KEY,
|
|
62
|
+
run_id TEXT NOT NULL REFERENCES runs(id),
|
|
63
|
+
type TEXT NOT NULL,
|
|
64
|
+
payload_json TEXT,
|
|
65
|
+
ts TEXT NOT NULL
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
CREATE INDEX IF NOT EXISTS idx_events_run ON events(run_id);
|
|
69
|
+
|
|
70
|
+
CREATE TABLE IF NOT EXISTS approval_gates (
|
|
71
|
+
id TEXT PRIMARY KEY,
|
|
72
|
+
run_id TEXT NOT NULL REFERENCES runs(id),
|
|
73
|
+
action_type TEXT NOT NULL,
|
|
74
|
+
status TEXT NOT NULL,
|
|
75
|
+
resolved_by TEXT,
|
|
76
|
+
resolved_at TEXT
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
CREATE INDEX IF NOT EXISTS idx_gates_run ON approval_gates(run_id);
|
|
80
|
+
|
|
81
|
+
CREATE TABLE IF NOT EXISTS intake_settings (
|
|
82
|
+
key TEXT PRIMARY KEY,
|
|
83
|
+
value_json TEXT NOT NULL
|
|
84
|
+
);
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { formatEnvStartupLine, loadAgentDealerEnv, } from "./config/load-env.js";
|
|
2
|
+
import Fastify from "fastify";
|
|
3
|
+
import cors from "@fastify/cors";
|
|
4
|
+
import { enrichPathForCliTools } from "./cli-env.js";
|
|
5
|
+
import { migrate } from "./db/index.js";
|
|
6
|
+
import { registerRoutes } from "./routes/index.js";
|
|
7
|
+
import { startQueue } from "./queue/dispatcher.js";
|
|
8
|
+
import { registerStaticUi } from "./static-ui.js";
|
|
9
|
+
const { mode, envFile } = loadAgentDealerEnv();
|
|
10
|
+
console.log(formatEnvStartupLine(mode, envFile));
|
|
11
|
+
const port = Number(process.env.PORT ?? 2221);
|
|
12
|
+
async function main() {
|
|
13
|
+
enrichPathForCliTools();
|
|
14
|
+
migrate();
|
|
15
|
+
const app = Fastify({ logger: true });
|
|
16
|
+
await app.register(cors, { origin: true });
|
|
17
|
+
await registerRoutes(app);
|
|
18
|
+
const uiDist = await registerStaticUi(app);
|
|
19
|
+
startQueue();
|
|
20
|
+
await app.listen({ port, host: "0.0.0.0" });
|
|
21
|
+
const base = `http://127.0.0.1:${port}`;
|
|
22
|
+
console.log(`agent-dealer API ${base}`);
|
|
23
|
+
if (uiDist) {
|
|
24
|
+
console.log(`agent-dealer dashboard ${base}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
main().catch((err) => {
|
|
28
|
+
console.error(err);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { listAgentsWithHealth } from "../adapters/agent-health.js";
|
|
2
|
+
import { listAgents } from "../repository/agents.js";
|
|
3
|
+
export async function resolveAgentForIssue(issue, settings, explicitAgentId) {
|
|
4
|
+
if (explicitAgentId) {
|
|
5
|
+
return { agentId: explicitAgentId, reason: "explicit agentId on promote" };
|
|
6
|
+
}
|
|
7
|
+
const issueLabels = new Set((issue.labels ?? []).map((l) => l.toLowerCase()));
|
|
8
|
+
for (const rule of settings.routingRules) {
|
|
9
|
+
if (issueLabels.has(rule.label.toLowerCase())) {
|
|
10
|
+
return {
|
|
11
|
+
agentId: rule.agentId,
|
|
12
|
+
reason: `label:${rule.label} → configured agent`,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
if (settings.defaultAgentId) {
|
|
17
|
+
return {
|
|
18
|
+
agentId: settings.defaultAgentId,
|
|
19
|
+
reason: "linear.defaultAgentId",
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
const agents = await listAgentsWithHealth(listAgents());
|
|
23
|
+
const healthyWithWorkspace = agents.filter((a) => a.healthy && a.workspaceRoot);
|
|
24
|
+
if (healthyWithWorkspace.length > 0) {
|
|
25
|
+
const pick = healthyWithWorkspace[0];
|
|
26
|
+
return {
|
|
27
|
+
agentId: pick.id,
|
|
28
|
+
reason: `first healthy agent with workspace (${pick.name})`,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
throw new Error("No agent available — set defaultAgentId, routing rules, or configure a healthy agent with workspace");
|
|
32
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { getDataDir } from "./db/index.js";
|
|
4
|
+
export function getTemporalDir() {
|
|
5
|
+
return path.join(getDataDir(), ".temporal");
|
|
6
|
+
}
|
|
7
|
+
export function getTemporalOutputDir() {
|
|
8
|
+
const dir = path.join(getTemporalDir(), "output");
|
|
9
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
10
|
+
return dir;
|
|
11
|
+
}
|
|
12
|
+
export function getTemporalLogsDir() {
|
|
13
|
+
const dir = path.join(getTemporalDir(), "logs");
|
|
14
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
15
|
+
return dir;
|
|
16
|
+
}
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import { addArtifact, countByStatus, getLatestArtifact, getRun, listRuns, transitionRun, } from "../repository/runs.js";
|
|
2
|
+
import { runAgent } from "../runners/claude.js";
|
|
3
|
+
import { persistRunOutput, seedDeliverableFromParent } from "../runners/persist.js";
|
|
4
|
+
import { checkAgentDeckHealth } from "../adapters/agent-deck.js";
|
|
5
|
+
import { pollLinearIssues } from "../adapters/external.js";
|
|
6
|
+
import { syncLinearForRun } from "../adapters/linear-sync.js";
|
|
7
|
+
import { listAgentsWithHealth } from "../adapters/agent-health.js";
|
|
8
|
+
import { listAgents } from "../repository/agents.js";
|
|
9
|
+
import { runReflect } from "../runners/reflect.js";
|
|
10
|
+
const activeRuns = new Map();
|
|
11
|
+
const activePlanDrafts = new Set();
|
|
12
|
+
const activeReflects = new Set();
|
|
13
|
+
let listeners = [];
|
|
14
|
+
let pollTimer = null;
|
|
15
|
+
let dispatchTimer = null;
|
|
16
|
+
function maxConcurrent() {
|
|
17
|
+
return Number(process.env.MAX_CONCURRENT_RUNS ?? 2);
|
|
18
|
+
}
|
|
19
|
+
function runtimeFor(run) {
|
|
20
|
+
return run.runtime ?? "claude_code";
|
|
21
|
+
}
|
|
22
|
+
function needsPlanDraft(run) {
|
|
23
|
+
if (run.status !== "plan_pending" && run.status !== "queued")
|
|
24
|
+
return false;
|
|
25
|
+
if (!run.runtime)
|
|
26
|
+
return false;
|
|
27
|
+
if (getLatestArtifact(run.id, "draft_plan"))
|
|
28
|
+
return false;
|
|
29
|
+
if (activePlanDrafts.has(run.id))
|
|
30
|
+
return false;
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
function sortQueueFifo(runs) {
|
|
34
|
+
return [...runs].sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
35
|
+
}
|
|
36
|
+
function listPlanningPhaseRuns() {
|
|
37
|
+
return sortQueueFifo(listRuns("queued").concat(listRuns("plan_pending")));
|
|
38
|
+
}
|
|
39
|
+
/** Plan written and idle — human review lane (excludes active re-draft). */
|
|
40
|
+
function isReadyForPlanReview(run) {
|
|
41
|
+
return (run.status === "plan_pending" &&
|
|
42
|
+
!!getLatestArtifact(run.id, "draft_plan") &&
|
|
43
|
+
!activePlanDrafts.has(run.id));
|
|
44
|
+
}
|
|
45
|
+
export function getSnapshot() {
|
|
46
|
+
const all = listRuns();
|
|
47
|
+
const runningRuns = all.filter((r) => r.status === "running");
|
|
48
|
+
const waitingExecution = sortQueueFifo(all.filter((r) => r.status === "plan_approved"));
|
|
49
|
+
const resultReviewRuns = all
|
|
50
|
+
.filter((r) => r.status === "review" || r.status === "failed")
|
|
51
|
+
.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
52
|
+
const recentDone = all
|
|
53
|
+
.filter((r) => r.status === "done")
|
|
54
|
+
.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))
|
|
55
|
+
.slice(0, 10);
|
|
56
|
+
const planningPhase = listPlanningPhaseRuns();
|
|
57
|
+
const awaitingPlanReview = planningPhase.filter(isReadyForPlanReview);
|
|
58
|
+
const planningActiveRuns = planningPhase.filter((r) => activePlanDrafts.has(r.id));
|
|
59
|
+
const planningQueuedRuns = planningPhase.filter((r) => !activePlanDrafts.has(r.id) && !isReadyForPlanReview(r));
|
|
60
|
+
return {
|
|
61
|
+
planReviewCount: awaitingPlanReview.length,
|
|
62
|
+
resultReviewCount: resultReviewRuns.length,
|
|
63
|
+
maxConcurrent: maxConcurrent(),
|
|
64
|
+
planningActiveRuns,
|
|
65
|
+
planningQueuedRuns,
|
|
66
|
+
runningRuns,
|
|
67
|
+
waitingExecution,
|
|
68
|
+
resultReviewRuns,
|
|
69
|
+
recentDone,
|
|
70
|
+
awaitingPlanReview,
|
|
71
|
+
runs: all,
|
|
72
|
+
agentDeckOnline: false,
|
|
73
|
+
agents: [],
|
|
74
|
+
agentIssueCount: 0,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
export async function getSnapshotAsync() {
|
|
78
|
+
const snap = getSnapshot();
|
|
79
|
+
snap.agentDeckOnline = await checkAgentDeckHealth();
|
|
80
|
+
snap.agents = await listAgentsWithHealth(listAgents());
|
|
81
|
+
snap.agentIssueCount = snap.agents.filter((a) => !a.healthy).length;
|
|
82
|
+
return snap;
|
|
83
|
+
}
|
|
84
|
+
export function subscribe(listener) {
|
|
85
|
+
listeners.push(listener);
|
|
86
|
+
return () => {
|
|
87
|
+
listeners = listeners.filter((l) => l !== listener);
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
async function notify() {
|
|
91
|
+
const snap = await getSnapshotAsync();
|
|
92
|
+
for (const l of listeners)
|
|
93
|
+
l(snap);
|
|
94
|
+
}
|
|
95
|
+
export function startQueue() {
|
|
96
|
+
const pollMs = Number(process.env.POLL_INTERVAL_MS ?? 30_000);
|
|
97
|
+
if (!pollTimer) {
|
|
98
|
+
pollTimer = setInterval(() => {
|
|
99
|
+
pollLinearIssues().catch((e) => console.error("[linear]", e));
|
|
100
|
+
}, pollMs);
|
|
101
|
+
pollLinearIssues().catch((e) => console.error("[linear]", e));
|
|
102
|
+
}
|
|
103
|
+
if (!dispatchTimer) {
|
|
104
|
+
dispatchTimer = setInterval(() => {
|
|
105
|
+
dispatch().catch((e) => console.error("[queue]", e));
|
|
106
|
+
}, 3000);
|
|
107
|
+
dispatch().catch((e) => console.error("[queue]", e));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
export function stopQueue() {
|
|
111
|
+
if (pollTimer)
|
|
112
|
+
clearInterval(pollTimer);
|
|
113
|
+
if (dispatchTimer)
|
|
114
|
+
clearInterval(dispatchTimer);
|
|
115
|
+
pollTimer = null;
|
|
116
|
+
dispatchTimer = null;
|
|
117
|
+
}
|
|
118
|
+
/** Fire-and-forget: agent drafts plan after task intake. */
|
|
119
|
+
export function schedulePlanDraft(run) {
|
|
120
|
+
if (!needsPlanDraft(run))
|
|
121
|
+
return;
|
|
122
|
+
if (activePlanDrafts.has(run.id))
|
|
123
|
+
return;
|
|
124
|
+
activePlanDrafts.add(run.id);
|
|
125
|
+
void draftPlan(run)
|
|
126
|
+
.catch((e) => console.error("[plan-draft]", run.id, e))
|
|
127
|
+
.finally(() => {
|
|
128
|
+
activePlanDrafts.delete(run.id);
|
|
129
|
+
notify().catch(console.error);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
/** Fire-and-forget: post-review playbook reflect (D3 learning loop). */
|
|
133
|
+
export function scheduleReflect(run, opts) {
|
|
134
|
+
if (!run.playbookId || !run.deckId)
|
|
135
|
+
return;
|
|
136
|
+
if (activeReflects.has(run.id))
|
|
137
|
+
return;
|
|
138
|
+
void (async () => {
|
|
139
|
+
const online = await checkAgentDeckHealth();
|
|
140
|
+
if (!online)
|
|
141
|
+
return;
|
|
142
|
+
activeReflects.add(run.id);
|
|
143
|
+
try {
|
|
144
|
+
await runReflect(run, opts);
|
|
145
|
+
}
|
|
146
|
+
catch (e) {
|
|
147
|
+
console.error("[reflect]", run.id, e);
|
|
148
|
+
}
|
|
149
|
+
finally {
|
|
150
|
+
activeReflects.delete(run.id);
|
|
151
|
+
notify().catch(console.error);
|
|
152
|
+
}
|
|
153
|
+
})();
|
|
154
|
+
}
|
|
155
|
+
async function dispatchPlanDrafts() {
|
|
156
|
+
const pending = listRuns("plan_pending").concat(listRuns("queued"));
|
|
157
|
+
const slots = maxConcurrent() - activePlanDrafts.size;
|
|
158
|
+
if (slots <= 0)
|
|
159
|
+
return;
|
|
160
|
+
for (const run of pending) {
|
|
161
|
+
if (!needsPlanDraft(run))
|
|
162
|
+
continue;
|
|
163
|
+
if (activePlanDrafts.size >= maxConcurrent())
|
|
164
|
+
break;
|
|
165
|
+
activePlanDrafts.add(run.id);
|
|
166
|
+
void draftPlan(run)
|
|
167
|
+
.catch((e) => console.error("[plan-draft]", run.id, e))
|
|
168
|
+
.finally(() => {
|
|
169
|
+
activePlanDrafts.delete(run.id);
|
|
170
|
+
notify().catch(console.error);
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
async function dispatch() {
|
|
175
|
+
await dispatchPlanDrafts();
|
|
176
|
+
const running = countByStatus("running");
|
|
177
|
+
const slots = maxConcurrent() - running;
|
|
178
|
+
if (slots <= 0)
|
|
179
|
+
return;
|
|
180
|
+
const approved = sortQueueFifo(listRuns("plan_approved"));
|
|
181
|
+
for (const run of approved.slice(0, slots)) {
|
|
182
|
+
if (activeRuns.has(run.id))
|
|
183
|
+
continue;
|
|
184
|
+
const job = executeRun(run).finally(() => {
|
|
185
|
+
activeRuns.delete(run.id);
|
|
186
|
+
notify().catch(console.error);
|
|
187
|
+
});
|
|
188
|
+
activeRuns.set(run.id, job);
|
|
189
|
+
}
|
|
190
|
+
await notify();
|
|
191
|
+
}
|
|
192
|
+
export async function kickRun(run) {
|
|
193
|
+
if (run.status !== "plan_approved") {
|
|
194
|
+
throw new Error(`Run must be plan_approved to kick, got ${run.status}`);
|
|
195
|
+
}
|
|
196
|
+
await dispatch();
|
|
197
|
+
}
|
|
198
|
+
export async function draftPlan(run, opts) {
|
|
199
|
+
if (run.status === "queued") {
|
|
200
|
+
transitionRun(run.id, "plan_pending");
|
|
201
|
+
}
|
|
202
|
+
const updated = getRun(run.id);
|
|
203
|
+
if (!updated.runtime) {
|
|
204
|
+
throw new Error("Select an agent runtime before planning");
|
|
205
|
+
}
|
|
206
|
+
if (!opts?.replace) {
|
|
207
|
+
syncLinearForRun(updated, "planning_started").catch((e) => console.error("[linear-sync] planning_started:", e));
|
|
208
|
+
}
|
|
209
|
+
const rt = runtimeFor(updated);
|
|
210
|
+
try {
|
|
211
|
+
const result = await runAgent(updated, "plan");
|
|
212
|
+
const persisted = persistRunOutput({
|
|
213
|
+
run: updated,
|
|
214
|
+
phase: "plan",
|
|
215
|
+
runtime: rt,
|
|
216
|
+
exitCode: result.exitCode,
|
|
217
|
+
logPath: result.logPath,
|
|
218
|
+
rawTranscript: result.transcript,
|
|
219
|
+
});
|
|
220
|
+
const after = getRun(run.id);
|
|
221
|
+
if (!after || (after.status !== "plan_pending" && after.status !== "queued")) {
|
|
222
|
+
// Human approved or run moved on while agent was still planning — ignore late result.
|
|
223
|
+
await notify();
|
|
224
|
+
return after ?? updated;
|
|
225
|
+
}
|
|
226
|
+
if (persisted.planMarkdown) {
|
|
227
|
+
addArtifact(updated.id, "draft_plan", { markdown: persisted.planMarkdown, sessionId: persisted.sessionId }, "agent", result.logPath);
|
|
228
|
+
}
|
|
229
|
+
else if (!opts?.replace) {
|
|
230
|
+
addArtifact(updated.id, "feedback", { error: "Agent did not return a plan" }, "system");
|
|
231
|
+
}
|
|
232
|
+
const planFailed = !persisted.planMarkdown && result.exitCode !== 0;
|
|
233
|
+
if (planFailed) {
|
|
234
|
+
transitionRun(run.id, "failed");
|
|
235
|
+
addArtifact(run.id, "feedback", { error: "Planning failed", exitCode: result.exitCode }, "system");
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
catch (err) {
|
|
239
|
+
const after = getRun(run.id);
|
|
240
|
+
if (after && (after.status === "plan_pending" || after.status === "queued")) {
|
|
241
|
+
transitionRun(run.id, "failed");
|
|
242
|
+
addArtifact(run.id, "feedback", { error: String(err) }, "system");
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
await notify();
|
|
246
|
+
return getRun(run.id);
|
|
247
|
+
}
|
|
248
|
+
/** Fire-and-forget: human requested a new agent plan (replaces draft). */
|
|
249
|
+
export function scheduleRedraft(run) {
|
|
250
|
+
if (activePlanDrafts.has(run.id))
|
|
251
|
+
return false;
|
|
252
|
+
if (run.status !== "plan_pending" && run.status !== "queued")
|
|
253
|
+
return false;
|
|
254
|
+
if (!run.runtime)
|
|
255
|
+
return false;
|
|
256
|
+
activePlanDrafts.add(run.id);
|
|
257
|
+
void draftPlan(run, { replace: true })
|
|
258
|
+
.catch((e) => console.error("[plan-redraft]", run.id, e))
|
|
259
|
+
.finally(() => {
|
|
260
|
+
activePlanDrafts.delete(run.id);
|
|
261
|
+
notify().catch(console.error);
|
|
262
|
+
});
|
|
263
|
+
return true;
|
|
264
|
+
}
|
|
265
|
+
/** Human requested a new agent plan (replaces draft). @deprecated prefer scheduleRedraft */
|
|
266
|
+
export async function redraftPlan(run) {
|
|
267
|
+
return draftPlan(run, { replace: true });
|
|
268
|
+
}
|
|
269
|
+
async function executeRun(run) {
|
|
270
|
+
const rt = runtimeFor(run);
|
|
271
|
+
try {
|
|
272
|
+
seedDeliverableFromParent(run);
|
|
273
|
+
transitionRun(run.id, "running");
|
|
274
|
+
await notify();
|
|
275
|
+
const result = await runAgent(run, "execute");
|
|
276
|
+
const persisted = persistRunOutput({
|
|
277
|
+
run,
|
|
278
|
+
phase: "execute",
|
|
279
|
+
runtime: rt,
|
|
280
|
+
exitCode: result.exitCode,
|
|
281
|
+
logPath: result.logPath,
|
|
282
|
+
rawTranscript: result.transcript,
|
|
283
|
+
});
|
|
284
|
+
if (result.exitCode === 0 && !persisted.blocked) {
|
|
285
|
+
const updated = transitionRun(run.id, "review");
|
|
286
|
+
syncLinearForRun(updated, "review").catch((e) => console.error("[linear-sync] review:", e));
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
transitionRun(run.id, "failed");
|
|
290
|
+
addArtifact(run.id, "feedback", {
|
|
291
|
+
error: persisted.blocked ? "Execution blocked" : "Execution failed",
|
|
292
|
+
exitCode: result.exitCode,
|
|
293
|
+
blocker: persisted.blockerSummary,
|
|
294
|
+
}, "system");
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
catch (err) {
|
|
298
|
+
transitionRun(run.id, "failed");
|
|
299
|
+
addArtifact(run.id, "feedback", { error: String(err) }, "system");
|
|
300
|
+
}
|
|
301
|
+
await notify();
|
|
302
|
+
}
|
|
303
|
+
export async function forceDispatch() {
|
|
304
|
+
await dispatch();
|
|
305
|
+
}
|