@dyzsasd/dev-loop 0.22.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -10
- package/dist/agentops.js +5 -68
- package/dist/cli.js +4 -0
- package/dist/db.js +0 -26
- package/dist/doctor.js +2 -2
- package/dist/install-claude-plugin.js +78 -0
- package/dist/mcp-merge.js +18 -19
- package/dist/mirrorstore.js +1 -1
- package/dist/plugin/.claude-plugin/marketplace.json +13 -0
- package/dist/plugin/.claude-plugin/plugin.json +11 -0
- package/dist/plugin/config/mcp.codex.toml.example +33 -0
- package/dist/plugin/config/mcp.example.json +15 -0
- package/dist/plugin/config/mcp.opencode.json.example +16 -0
- package/dist/plugin/config/projects.example.json +82 -0
- package/dist/plugin/hooks/hooks.json +16 -0
- package/dist/plugin/references/codex-integration.md +282 -0
- package/dist/plugin/references/config-schema.md +358 -0
- package/dist/plugin/references/conventions.md +2159 -0
- package/dist/plugin/skills/architect-agent/SKILL.md +231 -0
- package/dist/plugin/skills/communication-agent/SKILL.md +247 -0
- package/dist/plugin/skills/dev-agent/SKILL.md +373 -0
- package/dist/plugin/skills/init/SKILL.md +496 -0
- package/dist/plugin/skills/junior-dev-agent/SKILL.md +348 -0
- package/dist/plugin/skills/ops-agent/SKILL.md +219 -0
- package/dist/plugin/skills/pm-agent/SKILL.md +427 -0
- package/dist/plugin/skills/qa-agent/SKILL.md +299 -0
- package/dist/plugin/skills/reflect-agent/SKILL.md +271 -0
- package/dist/plugin/skills/senior-dev-agent/SKILL.md +353 -0
- package/dist/plugin/skills/sweep-agent/SKILL.md +180 -0
- package/dist/run-agents.js +373 -0
- package/dist/seed.js +4 -3
- package/dist/server.js +1 -1
- package/dist/shim.js +3 -4
- package/dist/tooldefs.js +3 -25
- package/package.json +5 -5
- package/dist/topicstore.js +0 -174
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// `dev-loop run` — a small scheduler that fires agent SKILLs through a headless CLI.
|
|
3
|
+
// It deliberately does NOT depend on Claude/Codex `/loop`; it owns cadence here and
|
|
4
|
+
// shells out to `claude -p` or `codex exec` once per agent fire.
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
6
|
+
import { createWriteStream, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
import { dirname, join, resolve } from "node:path";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
import { resolveProjectFromCwd } from "./resolve-project.js";
|
|
11
|
+
const VALID_AGENTS = [
|
|
12
|
+
"pm", "qa", "dev", "senior-dev", "junior-dev", "sweep", "reflect",
|
|
13
|
+
"ops", "architect", "communication",
|
|
14
|
+
];
|
|
15
|
+
const AGENT_SET = new Set(VALID_AGENTS);
|
|
16
|
+
const GROUPS = {
|
|
17
|
+
core: ["pm", "qa", "dev", "sweep"],
|
|
18
|
+
split: ["pm", "qa", "senior-dev", "junior-dev", "sweep"],
|
|
19
|
+
outward: ["ops", "architect", "communication"],
|
|
20
|
+
all: [...VALID_AGENTS],
|
|
21
|
+
};
|
|
22
|
+
const DEFAULT_AGENTS = GROUPS.core;
|
|
23
|
+
const DEFAULT_INTERVALS = {
|
|
24
|
+
pm: 5 * 60_000,
|
|
25
|
+
qa: 5 * 60_000,
|
|
26
|
+
dev: 5 * 60_000,
|
|
27
|
+
"senior-dev": 5 * 60_000,
|
|
28
|
+
"junior-dev": 5 * 60_000,
|
|
29
|
+
sweep: 30 * 60_000,
|
|
30
|
+
reflect: 24 * 60 * 60_000,
|
|
31
|
+
ops: 10 * 60_000,
|
|
32
|
+
architect: 24 * 60 * 60_000,
|
|
33
|
+
communication: 24 * 60 * 60_000,
|
|
34
|
+
};
|
|
35
|
+
const here = dirname(fileURLToPath(import.meta.url)); // hub/src (dev) | dist (build)
|
|
36
|
+
const EXT = fileURLToPath(import.meta.url).endsWith(".js") ? ".js" : ".ts"; // server sibling: .ts source / .js published
|
|
37
|
+
const isPluginRoot = (p) => existsSync(join(p, "skills")) && existsSync(join(p, "references"));
|
|
38
|
+
const defaultRoot = () => {
|
|
39
|
+
// Source checkout: hub/src -> repo root. Published package: dist/plugin -> bundled skills/references.
|
|
40
|
+
const candidates = [join(here, "plugin"), resolve(here, "..", "..")];
|
|
41
|
+
return candidates.find(isPluginRoot) ?? resolve(here, "..", "..");
|
|
42
|
+
};
|
|
43
|
+
const defaultDataDir = () => process.env.CLAUDE_PLUGIN_DATA || join(homedir(), ".claude", "plugins", "data", "dev-loop");
|
|
44
|
+
const defaultHubDb = () => process.env.DEVLOOP_HUB_DB || join(homedir(), ".dev-loop", "hub.db");
|
|
45
|
+
function usage() {
|
|
46
|
+
console.log(`dev-loop run — schedule dev-loop agents with a headless CLI
|
|
47
|
+
|
|
48
|
+
Usage:
|
|
49
|
+
dev-loop run --cli claude [--project <key>] [--agents core,communication]
|
|
50
|
+
dev-loop run --cli codex [--project <key>] [--agents core,outward]
|
|
51
|
+
|
|
52
|
+
Cadence is owned by this process, not by Claude/Codex /loop. Each fire shells out once:
|
|
53
|
+
claude -p <agent skill prompt>
|
|
54
|
+
codex exec ... <agent skill prompt>
|
|
55
|
+
|
|
56
|
+
Options:
|
|
57
|
+
--cli claude|codex CLI to invoke (default: claude)
|
|
58
|
+
--project <key> project key; optional. Defaults to DEVLOOP_PROJECT, then cwd→repo match, then defaultProject
|
|
59
|
+
--agents <list> comma list of agents or groups: core, split, outward, all
|
|
60
|
+
--agent <name> add one agent; may repeat
|
|
61
|
+
--dev-split replace dev with senior-dev + junior-dev in the selected set
|
|
62
|
+
--interval <agent=dur> override cadence, e.g. pm=2m, communication=24h; may repeat
|
|
63
|
+
--once run each selected agent once, then exit
|
|
64
|
+
--dry-run print resolved commands; do not launch Claude/Codex
|
|
65
|
+
--root <path> dev-loop checkout root (default: inferred, or CLAUDE_PLUGIN_ROOT)
|
|
66
|
+
--data <path> plugin data dir (default: CLAUDE_PLUGIN_DATA or ~/.claude/plugins/data/dev-loop)
|
|
67
|
+
--hub-db <path> hub db path (default: DEVLOOP_HUB_DB or ~/.dev-loop/hub.db)
|
|
68
|
+
--cwd <path> working directory for CLI subprocesses (default: project repoPath)
|
|
69
|
+
--mcp-config <path> claude: MCP config to load + --strict-mcp-config (default: <cwd>/.mcp.json if present)
|
|
70
|
+
--max-fires <n> stop after N total agent fires, then drain + exit (cost guard; default 0 = unlimited)
|
|
71
|
+
--codex-safe omit Codex's unsafe bypass flags; useful for read-only/dry runs
|
|
72
|
+
--cli-arg <arg> pass an extra arg to the selected CLI before the prompt; may repeat
|
|
73
|
+
(CLI binaries: set DEVLOOP_CLAUDE_BIN / DEVLOOP_CODEX_BIN to override)
|
|
74
|
+
|
|
75
|
+
Durations accept ms/s/m/h/d. Default agents: core = pm,qa,dev,sweep.`);
|
|
76
|
+
}
|
|
77
|
+
function die(msg, code = 2) {
|
|
78
|
+
console.error(`dev-loop run: ${msg}`);
|
|
79
|
+
process.exit(code);
|
|
80
|
+
}
|
|
81
|
+
function parseDuration(input) {
|
|
82
|
+
const m = input.trim().match(/^(\d+(?:\.\d+)?)(ms|s|m|h|d)?$/);
|
|
83
|
+
if (!m)
|
|
84
|
+
die(`invalid duration '${input}'`);
|
|
85
|
+
const n = Number(m[1]);
|
|
86
|
+
const unit = m[2] ?? "m";
|
|
87
|
+
const mult = unit === "ms" ? 1 : unit === "s" ? 1_000 : unit === "m" ? 60_000 : unit === "h" ? 60 * 60_000 : 24 * 60 * 60_000;
|
|
88
|
+
const ms = Math.round(n * mult);
|
|
89
|
+
if (!Number.isFinite(ms) || ms <= 0)
|
|
90
|
+
die(`invalid duration '${input}'`);
|
|
91
|
+
return ms;
|
|
92
|
+
}
|
|
93
|
+
function formatDuration(ms) {
|
|
94
|
+
if (ms % (24 * 60 * 60_000) === 0)
|
|
95
|
+
return `${ms / (24 * 60 * 60_000)}d`;
|
|
96
|
+
if (ms % (60 * 60_000) === 0)
|
|
97
|
+
return `${ms / (60 * 60_000)}h`;
|
|
98
|
+
if (ms % 60_000 === 0)
|
|
99
|
+
return `${ms / 60_000}m`;
|
|
100
|
+
if (ms % 1_000 === 0)
|
|
101
|
+
return `${ms / 1_000}s`;
|
|
102
|
+
return `${ms}ms`;
|
|
103
|
+
}
|
|
104
|
+
function expandAgentSpec(parts) {
|
|
105
|
+
const out = [];
|
|
106
|
+
for (const raw of parts.flatMap((p) => p.split(","))) {
|
|
107
|
+
const name = raw.trim();
|
|
108
|
+
if (!name)
|
|
109
|
+
continue;
|
|
110
|
+
if (GROUPS[name])
|
|
111
|
+
out.push(...GROUPS[name]);
|
|
112
|
+
else if (AGENT_SET.has(name))
|
|
113
|
+
out.push(name);
|
|
114
|
+
else
|
|
115
|
+
die(`unknown agent/group '${name}'`);
|
|
116
|
+
}
|
|
117
|
+
return [...new Set(out)];
|
|
118
|
+
}
|
|
119
|
+
function parseArgs(argv) {
|
|
120
|
+
const agentSpecs = [];
|
|
121
|
+
const intervals = { ...DEFAULT_INTERVALS };
|
|
122
|
+
const extraArgs = [];
|
|
123
|
+
const opts = {
|
|
124
|
+
cli: process.env.DEVLOOP_RUNNER_CLI || "claude",
|
|
125
|
+
agents: [],
|
|
126
|
+
intervals,
|
|
127
|
+
once: false,
|
|
128
|
+
dryRun: false,
|
|
129
|
+
devSplit: false,
|
|
130
|
+
root: process.env.CLAUDE_PLUGIN_ROOT || process.env.DEVLOOP_PLUGIN_ROOT || defaultRoot(),
|
|
131
|
+
dataDir: defaultDataDir(),
|
|
132
|
+
hubDb: defaultHubDb(),
|
|
133
|
+
claudeBin: process.env.DEVLOOP_CLAUDE_BIN || "claude",
|
|
134
|
+
codexBin: process.env.DEVLOOP_CODEX_BIN || "codex",
|
|
135
|
+
codexSafe: false,
|
|
136
|
+
maxFires: 0,
|
|
137
|
+
extraArgs,
|
|
138
|
+
};
|
|
139
|
+
for (let i = 0; i < argv.length; i++) {
|
|
140
|
+
const a = argv[i];
|
|
141
|
+
const next = () => argv[++i] ?? die(`${a} requires a value`);
|
|
142
|
+
if (a === "--help" || a === "-h") {
|
|
143
|
+
usage();
|
|
144
|
+
process.exit(0);
|
|
145
|
+
}
|
|
146
|
+
else if (a === "--cli") {
|
|
147
|
+
const v = next();
|
|
148
|
+
if (v !== "claude" && v !== "codex")
|
|
149
|
+
die("--cli must be claude or codex");
|
|
150
|
+
opts.cli = v;
|
|
151
|
+
}
|
|
152
|
+
else if (a === "--project")
|
|
153
|
+
opts.project = next();
|
|
154
|
+
else if (a === "--agents")
|
|
155
|
+
agentSpecs.push(next());
|
|
156
|
+
else if (a === "--agent")
|
|
157
|
+
agentSpecs.push(next());
|
|
158
|
+
else if (a === "--dev-split")
|
|
159
|
+
opts.devSplit = true;
|
|
160
|
+
else if (a === "--interval") {
|
|
161
|
+
const raw = next();
|
|
162
|
+
const eq = raw.indexOf("=");
|
|
163
|
+
if (eq <= 0)
|
|
164
|
+
die("--interval must look like agent=duration");
|
|
165
|
+
const agent = raw.slice(0, eq);
|
|
166
|
+
if (!AGENT_SET.has(agent))
|
|
167
|
+
die(`unknown agent in --interval '${agent}'`);
|
|
168
|
+
intervals[agent] = parseDuration(raw.slice(eq + 1));
|
|
169
|
+
}
|
|
170
|
+
else if (a === "--once")
|
|
171
|
+
opts.once = true;
|
|
172
|
+
else if (a === "--dry-run")
|
|
173
|
+
opts.dryRun = true;
|
|
174
|
+
else if (a === "--root")
|
|
175
|
+
opts.root = resolve(next());
|
|
176
|
+
else if (a === "--data")
|
|
177
|
+
opts.dataDir = resolve(next());
|
|
178
|
+
else if (a === "--hub-db")
|
|
179
|
+
opts.hubDb = resolve(next());
|
|
180
|
+
else if (a === "--cwd")
|
|
181
|
+
opts.cwd = resolve(next());
|
|
182
|
+
else if (a === "--mcp-config")
|
|
183
|
+
opts.mcpConfig = resolve(next());
|
|
184
|
+
else if (a === "--max-fires") {
|
|
185
|
+
opts.maxFires = Number(next());
|
|
186
|
+
if (!Number.isInteger(opts.maxFires) || opts.maxFires < 0)
|
|
187
|
+
die("--max-fires must be a non-negative integer (0 = unlimited)");
|
|
188
|
+
}
|
|
189
|
+
else if (a === "--codex-safe")
|
|
190
|
+
opts.codexSafe = true;
|
|
191
|
+
else if (a === "--cli-arg")
|
|
192
|
+
extraArgs.push(next());
|
|
193
|
+
else
|
|
194
|
+
die(`unknown option '${a}'`);
|
|
195
|
+
}
|
|
196
|
+
let agents = expandAgentSpec(agentSpecs.length ? agentSpecs : DEFAULT_AGENTS);
|
|
197
|
+
if (opts.devSplit) {
|
|
198
|
+
agents = agents.flatMap((a) => a === "dev" ? ["senior-dev", "junior-dev"] : [a]);
|
|
199
|
+
agents = [...new Set(agents)];
|
|
200
|
+
}
|
|
201
|
+
opts.agents = agents;
|
|
202
|
+
return opts;
|
|
203
|
+
}
|
|
204
|
+
function readProjects(dataDir) {
|
|
205
|
+
const p = process.env.DEVLOOP_PROJECTS_JSON || join(dataDir, "projects.json");
|
|
206
|
+
if (!existsSync(p))
|
|
207
|
+
return null;
|
|
208
|
+
try {
|
|
209
|
+
return JSON.parse(readFileSync(p, "utf8"));
|
|
210
|
+
}
|
|
211
|
+
catch (e) {
|
|
212
|
+
die(`could not parse ${p}: ${e.message}`, 1);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
function resolveProject(opts, cfg) {
|
|
216
|
+
const explicit = opts.project || process.env.DEVLOOP_PROJECT?.trim();
|
|
217
|
+
if (explicit)
|
|
218
|
+
return explicit;
|
|
219
|
+
const fromCwd = cfg ? resolveProjectFromCwd(opts.cwd || process.cwd(), cfg) : null;
|
|
220
|
+
return fromCwd || cfg?.defaultProject || Object.keys(cfg?.projects ?? {})[0] || "demo";
|
|
221
|
+
}
|
|
222
|
+
function resolveCwd(opts, cfg, project) {
|
|
223
|
+
if (opts.cwd)
|
|
224
|
+
return opts.cwd;
|
|
225
|
+
const p = cfg?.projects?.[project];
|
|
226
|
+
const primaryRepo = p?.repos?.find((r) => r.role === "primary" && r.path)?.path;
|
|
227
|
+
const docRepo = p?.repos?.find((r) => r.role === "docs" && r.path)?.path;
|
|
228
|
+
return p?.repoPath || primaryRepo || docRepo || p?.repos?.find((r) => r.path)?.path || process.cwd();
|
|
229
|
+
}
|
|
230
|
+
function stripFrontmatter(raw) {
|
|
231
|
+
const lines = raw.split(/\r?\n/);
|
|
232
|
+
if (lines[0]?.trim() !== "---")
|
|
233
|
+
return raw;
|
|
234
|
+
const end = lines.findIndex((line, i) => i > 0 && line.trim() === "---");
|
|
235
|
+
return end > 0 ? lines.slice(end + 1).join("\n").trimStart() : raw;
|
|
236
|
+
}
|
|
237
|
+
function readPrompt(opts, agent) {
|
|
238
|
+
const skill = join(opts.root, "skills", `${agent}-agent`, "SKILL.md");
|
|
239
|
+
if (!existsSync(skill))
|
|
240
|
+
die(`skill file not found for '${agent}': ${skill}. Pass --root <dev-loop checkout>.`, 1);
|
|
241
|
+
const body = stripFrontmatter(readFileSync(skill, "utf8"))
|
|
242
|
+
.replaceAll("${CLAUDE_PLUGIN_ROOT}", opts.root)
|
|
243
|
+
.replaceAll("${CLAUDE_PLUGIN_DATA}", opts.dataDir);
|
|
244
|
+
return `You are launched by dev-loop's own scheduler. Run exactly one fresh fire for this agent, then stop.\n\n${body}`;
|
|
245
|
+
}
|
|
246
|
+
function shellQuote(s) {
|
|
247
|
+
return /^[A-Za-z0-9_/:=.,@%+-]+$/.test(s) ? s : `'${s.replaceAll("'", "'\\''")}'`;
|
|
248
|
+
}
|
|
249
|
+
// The dev-loop-hub MCP server the scheduler injects itself, so NEITHER CLI needs the plugin or a
|
|
250
|
+
// pre-existing config. Points at this package's own server entry (.ts source / .js published) + the
|
|
251
|
+
// resolved hub db, with the per-fire actor/project. claude takes it as inline --mcp-config JSON;
|
|
252
|
+
// codex takes the same shape as `-c` overrides (which define the server, not just patch env).
|
|
253
|
+
const serverEntry = join(here, `server${EXT}`);
|
|
254
|
+
function commandFor(opts, agent, project, prompt) {
|
|
255
|
+
if (opts.cli === "claude") {
|
|
256
|
+
// explicit --mcp-config file wins; otherwise inject the hub inline so a fresh project needs no .mcp.json.
|
|
257
|
+
const mcpArg = opts.mcpConfig ?? JSON.stringify({
|
|
258
|
+
mcpServers: { "dev-loop-hub": { command: "node", args: [serverEntry], env: { DEVLOOP_ACTOR: agent, DEVLOOP_PROJECT: project, DEVLOOP_HUB_DB: opts.hubDb } } },
|
|
259
|
+
});
|
|
260
|
+
return { command: opts.claudeBin, args: ["--mcp-config", mcpArg, "--strict-mcp-config", ...opts.extraArgs, "-p", prompt] };
|
|
261
|
+
}
|
|
262
|
+
const args = [
|
|
263
|
+
"exec",
|
|
264
|
+
...opts.extraArgs,
|
|
265
|
+
"-c", `mcp_servers.dev-loop-hub.command="node"`,
|
|
266
|
+
"-c", `mcp_servers.dev-loop-hub.args=["${serverEntry}"]`,
|
|
267
|
+
"-c", `mcp_servers.dev-loop-hub.env.DEVLOOP_ACTOR="${agent}"`,
|
|
268
|
+
"-c", `mcp_servers.dev-loop-hub.env.DEVLOOP_PROJECT="${project}"`,
|
|
269
|
+
"-c", `mcp_servers.dev-loop-hub.env.DEVLOOP_HUB_DB="${opts.hubDb}"`,
|
|
270
|
+
];
|
|
271
|
+
if (!opts.codexSafe)
|
|
272
|
+
args.push("--dangerously-bypass-approvals-and-sandbox", "--skip-git-repo-check");
|
|
273
|
+
args.push(prompt);
|
|
274
|
+
return { command: opts.codexBin, args };
|
|
275
|
+
}
|
|
276
|
+
function displayCommand(command, args, prompt) {
|
|
277
|
+
return [command, ...args.map((a) => a === prompt ? `<prompt:${prompt.length} chars>` : a).map(shellQuote)].join(" ");
|
|
278
|
+
}
|
|
279
|
+
async function runAgent(opts, agent, project, cwd) {
|
|
280
|
+
const prompt = readPrompt(opts, agent);
|
|
281
|
+
const { command, args } = commandFor(opts, agent, project, prompt);
|
|
282
|
+
const env = {
|
|
283
|
+
...process.env,
|
|
284
|
+
DEVLOOP_ACTOR: agent,
|
|
285
|
+
DEVLOOP_PROJECT: project,
|
|
286
|
+
DEVLOOP_HUB_DB: opts.hubDb,
|
|
287
|
+
CLAUDE_PLUGIN_ROOT: opts.root,
|
|
288
|
+
CLAUDE_PLUGIN_DATA: opts.dataDir,
|
|
289
|
+
};
|
|
290
|
+
const rendered = displayCommand(command, args, prompt);
|
|
291
|
+
if (opts.dryRun) {
|
|
292
|
+
console.log(`[dry-run] ${agent}: cwd=${cwd}`);
|
|
293
|
+
console.log(`[dry-run] ${agent}: ${rendered}`);
|
|
294
|
+
return 0;
|
|
295
|
+
}
|
|
296
|
+
const logDir = opts.logDir || join(opts.dataDir, project, "runner-logs");
|
|
297
|
+
mkdirSync(logDir, { recursive: true });
|
|
298
|
+
const logPath = join(logDir, `${agent}.log`);
|
|
299
|
+
const log = createWriteStream(logPath, { flags: "a" });
|
|
300
|
+
log.write(`\n\n===== ${new Date().toISOString()} ${rendered} cwd=${cwd} =====\n`);
|
|
301
|
+
console.log(`[${new Date().toISOString()}] ${agent}: start (${opts.cli}); log ${logPath}`);
|
|
302
|
+
const child = spawn(command, args, { cwd, env, stdio: ["ignore", "pipe", "pipe"] });
|
|
303
|
+
activeChildren.add(child);
|
|
304
|
+
child.stdout.on("data", (d) => { process.stdout.write(`[${agent}] ${d}`); log.write(d); });
|
|
305
|
+
child.stderr.on("data", (d) => { process.stderr.write(`[${agent}] ${d}`); log.write(d); });
|
|
306
|
+
return await new Promise((resolveExit) => {
|
|
307
|
+
child.on("error", (e) => { log.write(`\nERROR: ${e.message}\n`); console.error(`[${agent}] failed to start: ${e.message}`); resolveExit(1); });
|
|
308
|
+
child.on("close", (code, signal) => {
|
|
309
|
+
activeChildren.delete(child);
|
|
310
|
+
log.write(`\n===== exit code=${code ?? "null"} signal=${signal ?? "null"} =====\n`);
|
|
311
|
+
log.end();
|
|
312
|
+
console.log(`[${new Date().toISOString()}] ${agent}: exit ${code ?? `signal ${signal}`}`);
|
|
313
|
+
resolveExit(code ?? 1);
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
const activeChildren = new Set();
|
|
318
|
+
async function main() {
|
|
319
|
+
const opts = parseArgs(process.argv.slice(2));
|
|
320
|
+
const cfg = readProjects(opts.dataDir);
|
|
321
|
+
const project = resolveProject(opts, cfg);
|
|
322
|
+
const cwd = resolveCwd(opts, cfg, project);
|
|
323
|
+
if (!existsSync(cwd))
|
|
324
|
+
die(`cwd does not exist: ${cwd}`, 1);
|
|
325
|
+
console.log(`dev-loop run: cli=${opts.cli} project=${project} cwd=${cwd}`);
|
|
326
|
+
console.log(`dev-loop run: root=${opts.root} data=${opts.dataDir} hubDb=${opts.hubDb}`);
|
|
327
|
+
console.log(`dev-loop run: agents=${opts.agents.map((a) => `${a}@${formatDuration(opts.intervals[a])}`).join(", ")}`);
|
|
328
|
+
if (opts.once) {
|
|
329
|
+
const results = await Promise.all(opts.agents.map((a) => runAgent(opts, a, project, cwd)));
|
|
330
|
+
process.exit(results.every((c) => c === 0) ? 0 : 1);
|
|
331
|
+
}
|
|
332
|
+
const slots = opts.agents.map((agent) => ({ agent, nextAt: Date.now(), running: false }));
|
|
333
|
+
let stopping = false;
|
|
334
|
+
let fired = 0; // total fires started; --max-fires caps it (0 = unlimited)
|
|
335
|
+
const stop = () => {
|
|
336
|
+
if (stopping)
|
|
337
|
+
return;
|
|
338
|
+
stopping = true;
|
|
339
|
+
clearInterval(timer);
|
|
340
|
+
console.log("dev-loop run: stopping; forwarding SIGINT to active agent processes");
|
|
341
|
+
for (const child of activeChildren)
|
|
342
|
+
child.kill("SIGINT");
|
|
343
|
+
if (activeChildren.size === 0)
|
|
344
|
+
process.exit(0);
|
|
345
|
+
};
|
|
346
|
+
process.on("SIGINT", stop);
|
|
347
|
+
process.on("SIGTERM", stop);
|
|
348
|
+
const tick = () => {
|
|
349
|
+
const now = Date.now();
|
|
350
|
+
for (const slot of slots) {
|
|
351
|
+
if (stopping || slot.running || slot.nextAt > now)
|
|
352
|
+
continue;
|
|
353
|
+
slot.running = true;
|
|
354
|
+
fired++;
|
|
355
|
+
runAgent(opts, slot.agent, project, cwd)
|
|
356
|
+
.catch((e) => { console.error(`[${slot.agent}] ${e instanceof Error ? e.message : String(e)}`); return 1; })
|
|
357
|
+
.finally(() => {
|
|
358
|
+
slot.running = false;
|
|
359
|
+
slot.nextAt = Date.now() + opts.intervals[slot.agent];
|
|
360
|
+
if (stopping && activeChildren.size === 0)
|
|
361
|
+
process.exit(0);
|
|
362
|
+
});
|
|
363
|
+
if (opts.maxFires && fired >= opts.maxFires) {
|
|
364
|
+
console.log(`dev-loop run: reached --max-fires ${opts.maxFires}; draining active fires then exiting`);
|
|
365
|
+
stop();
|
|
366
|
+
break;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
const timer = setInterval(tick, 1_000);
|
|
371
|
+
tick();
|
|
372
|
+
}
|
|
373
|
+
main().catch((e) => die(e instanceof Error ? e.message : String(e), 1));
|
package/dist/seed.js
CHANGED
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
// Run directly (`node src/seed.ts <key> <name>`) or called by the server on first run.
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
import { openDb, nowIso } from "./db.js";
|
|
5
|
-
// The live dev-loop agents + the human operator.
|
|
5
|
+
// The live dev-loop agents + the human operator.
|
|
6
6
|
// DL split (senior/junior dev): `senior-dev` + `junior-dev` join as ACTIVE actors; the legacy single
|
|
7
7
|
// `dev` STAYS ACTIVE (NOT retired) — it remains the canonical single-pane fallback for non-split
|
|
8
8
|
// projects (e.g. monpick on Linear), so adding the two-tier model breaks no existing project.
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
// Communication is an active outward actor for public article drafts; it writes drafts, not tickets.
|
|
10
|
+
const AGENT_HANDLES = ["pm", "qa", "dev", "senior-dev", "junior-dev", "sweep", "reflect", "ops", "architect", "communication"];
|
|
11
|
+
// `signal` is a RETIRED actor: kept as an INACTIVE actor so its historical comment/event
|
|
11
12
|
// attribution stays readable, but refused for NEW writes (actorExists/G1 filter active=1).
|
|
12
13
|
const RETIRED_HANDLES = ["signal"];
|
|
13
14
|
// §4 label taxonomy (+ the `notified` workflow label from §9 notify).
|
package/dist/server.js
CHANGED
|
@@ -79,7 +79,7 @@ if (process.argv[2] === "daemon") {
|
|
|
79
79
|
await daemonLifecycle(sub); // resolves project from env/cwd; calls process.exit
|
|
80
80
|
}
|
|
81
81
|
const db = openDb(DB_PATH);
|
|
82
|
-
ensureActors(db); // the
|
|
82
|
+
ensureActors(db); // the configured agents + operator are always present (needed for attribution + the guard below)
|
|
83
83
|
// P3/G1 — phantom-actor guard: a typo'd DEVLOOP_ACTOR would silently write an unattributable
|
|
84
84
|
// author into created_by / events.actor / comments.author. Refuse to start instead (exit non-zero
|
|
85
85
|
// ⇒ the MCP client can't connect ⇒ the failure is visible to the launching pane).
|
package/dist/shim.js
CHANGED
|
@@ -11,12 +11,11 @@
|
|
|
11
11
|
//
|
|
12
12
|
// SCOPE: the 5 core ticket tools (list_issues/get_issue/save_issue/save_comment/list_comments) + a LOCAL
|
|
13
13
|
// whoami (DL-55), PLUS (DL-62) the doc/event family — list_events + doc.list/get/history/diff/save/publish,
|
|
14
|
-
// PLUS (DL-64) the discussion-board family — topic.list/get/open + post.add + topic.synthesize/close,
|
|
15
14
|
// PLUS (DL-67) the IM channel family — channel.register/send/poll/ack/status, PLUS (DL-68) P7 mirror +
|
|
16
15
|
// label/project — mirror.push/mirror.status + list_issue_labels/create_issue_label/get_project. That is the
|
|
17
|
-
// FINAL slice: the shim now proxies ALL
|
|
18
|
-
// The shim holds NO SoR / NO ticket/doc/
|
|
19
|
-
// op-API (which mirrors server.ts 1:1 via agentops.ts + the shared docstore/
|
|
16
|
+
// FINAL slice: the shim now proxies ALL 23 server.ts tools — a 100% server.ts drop-in.
|
|
17
|
+
// The shim holds NO SoR / NO ticket/doc/channel/mirror logic (Decision #3): a pure thin client over the
|
|
18
|
+
// op-API (which mirrors server.ts 1:1 via agentops.ts + the shared docstore/channelstore/mirrorstore/labelstore).
|
|
20
19
|
//
|
|
21
20
|
// DL-85: the tool { name, description, inputSchema } registry is now SHARED from tooldefs.ts (registerTools),
|
|
22
21
|
// so the names/schemas can no longer drift between this shim and server.ts by hand — the old "PARITY TRIPWIRE:
|
package/dist/tooldefs.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// dev-loop hub — the SINGLE source of the MCP tool surface (DL-85). Before this, server.ts (direct-db) and
|
|
2
|
-
// shim.ts (daemon-transport) each copy-pasted all
|
|
3
|
-
// triples byte-identically; the only per-file difference is the handler (dispatch vs proxy). Here the
|
|
2
|
+
// shim.ts (daemon-transport) each copy-pasted all 23 `registerTool(name, {description, inputSchema}, handler)`
|
|
3
|
+
// triples byte-identically; the only per-file difference is the handler (dispatch vs proxy). Here the 23
|
|
4
4
|
// {name, description, inputSchema} triples live ONCE; each entrypoint calls registerTools() and supplies only
|
|
5
5
|
// its per-name handler factory. The ok()/err() MCP-result helpers are shared from here too.
|
|
6
6
|
//
|
|
@@ -12,7 +12,7 @@ import { z } from "zod";
|
|
|
12
12
|
import { DOC_KINDS } from "./docstore.js"; // the doc-kind enum for doc.save's zod schema (a shared schema constant, not SoR/doc logic)
|
|
13
13
|
export const ok = (data) => ({ content: [{ type: "text", text: JSON.stringify(data) }] });
|
|
14
14
|
export const err = (message) => ({ isError: true, content: [{ type: "text", text: JSON.stringify({ error: message }) }] });
|
|
15
|
-
// ─── the canonical tool-name list — whoami (answered locally per transport) + the
|
|
15
|
+
// ─── the canonical tool-name list — whoami (answered locally per transport) + the 22 op-backed tools ────────
|
|
16
16
|
// agentops.ts derives AGENT_OPS = TOOL_NAMES minus "whoami" (the only tool that is NOT an op-API op), so this
|
|
17
17
|
// is the ONE source of the tool/op names. Order matches the historical AGENT_OPS order (registration order is
|
|
18
18
|
// irrelevant to MCP — tools resolve by name — but keeping it stable keeps diffs/feeds readable).
|
|
@@ -20,7 +20,6 @@ export const TOOL_NAMES = [
|
|
|
20
20
|
"whoami",
|
|
21
21
|
"list_issues", "get_issue", "save_issue", "save_comment", "list_comments",
|
|
22
22
|
"list_events", "doc.list", "doc.get", "doc.history", "doc.diff", "doc.save", "doc.publish",
|
|
23
|
-
"topic.list", "topic.get", "topic.open", "post.add", "topic.synthesize", "topic.close",
|
|
24
23
|
"channel.register", "channel.send", "channel.poll", "channel.ack", "channel.status",
|
|
25
24
|
"mirror.push", "mirror.status", "list_issue_labels", "create_issue_label", "get_project",
|
|
26
25
|
];
|
|
@@ -68,27 +67,6 @@ const DEFS = {
|
|
|
68
67
|
description: "OPERATOR-ONLY: publish a draft version → current (the live doc). Cooperative role-gate (DEVLOOP_ACTOR=operator), not anti-spoof — see §18/HUB-ARCHITECTURE §16.",
|
|
69
68
|
inputSchema: { slug: z.string().optional(), kind: z.string().optional(), version: z.number().int().positive() },
|
|
70
69
|
},
|
|
71
|
-
"topic.open": {
|
|
72
|
-
description: "Open a discussion topic (the caller becomes the chair = opened_by). invited = actor handles asked to post a perspective. Director-style use; any actor may chair its own topics.",
|
|
73
|
-
inputSchema: { question: z.string().min(1), invited: z.array(z.string()).min(1) },
|
|
74
|
-
},
|
|
75
|
-
"topic.list": {
|
|
76
|
-
description: "List discussion topics (no post bodies). Each row carries the current round, round_opened_at, and YOUR/the invited set's `pending` for this round (who still owes a perspective).",
|
|
77
|
-
inputSchema: { status: z.enum(["open", "closed"]).optional() },
|
|
78
|
-
},
|
|
79
|
-
"topic.get": { description: "A topic + all its posts (perspectives + the chair's synthesis), oldest first.", inputSchema: { id: z.string() } },
|
|
80
|
-
"post.add": {
|
|
81
|
-
description: "Post YOUR perspective to an OPEN topic you're invited to — once per round, your lane only (attributed to DEVLOOP_ACTOR). Append-only; you never edit/synthesize/close.",
|
|
82
|
-
inputSchema: { topicId: z.string(), body: z.string().min(1) },
|
|
83
|
-
},
|
|
84
|
-
"topic.synthesize": {
|
|
85
|
-
description: "CHAIR-ONLY (ACTOR === opened_by): write a synthesis post at the current round, optionally bumping to the next round (resets the round clock). Does NOT close — use topic.close to record the decision.",
|
|
86
|
-
inputSchema: { topicId: z.string(), body: z.string().min(1), nextRound: z.boolean().optional() },
|
|
87
|
-
},
|
|
88
|
-
"topic.close": {
|
|
89
|
-
description: "CHAIR-ONLY (ACTOR === opened_by): close the topic with a terminal decision. The decision is DATA (a recorded conclusion) — it NEVER auto-applies a code/SKILL/conventions change (§17).",
|
|
90
|
-
inputSchema: { topicId: z.string(), decision: z.string().min(1) },
|
|
91
|
-
},
|
|
92
70
|
"channel.register": {
|
|
93
71
|
description: "Idempotently register/update this project's IM channel from config. Stores ONLY the ENV-VAR NAMES (configRef = bot token / lark app_id; secretRef = lark app_secret) + the room id — NEVER a token/secret.",
|
|
94
72
|
inputSchema: { provider: z.enum(["slack", "lark"]), configRef: z.string().min(1), secretRef: z.string().optional(), channelRef: z.string().min(1) },
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dyzsasd/dev-loop",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "Standalone local coordination hub for the dev-loop agents — a zero-build, zero-native-dep MCP system-of-record over node:sqlite with per-agent identity, a localhost web-UI daemon, an opt-in agent op-API + thin stdio shim,
|
|
5
|
+
"description": "Standalone local coordination hub for the dev-loop agents — a zero-build, zero-native-dep MCP system-of-record over node:sqlite with per-agent identity, a localhost web-UI daemon, an opt-in agent op-API + thin stdio shim, a CLI-portable transport (Claude Code / Codex / opencode), and a scheduler that shells out to Claude/Codex without relying on /loop. See https://github.com/dyzsasd/dev-loop (docs/HUB-ARCHITECTURE.md, docs/PORTABILITY.md).",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"dev-loop",
|
|
8
8
|
"agents",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"README.md"
|
|
39
39
|
],
|
|
40
40
|
"scripts": {
|
|
41
|
-
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
41
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json && mkdir -p dist/plugin && cp -R ../.claude-plugin ../skills ../references ../hooks ../config dist/plugin/",
|
|
42
42
|
"prepack": "npm run build",
|
|
43
43
|
"start": "DEVLOOP_CREATE_PROJECT=1 node src/server.ts",
|
|
44
44
|
"daemon": "node src/daemon.ts",
|
|
@@ -51,11 +51,11 @@
|
|
|
51
51
|
"release-version": "node src/release-version.ts",
|
|
52
52
|
"doctor": "node src/server.ts doctor",
|
|
53
53
|
"identity-check": "node src/server.ts identity-check",
|
|
54
|
+
"run-agents-test": "node test/run-agents.ts",
|
|
54
55
|
"smoke": "node test/smoke.ts",
|
|
55
56
|
"loop": "node test/loop.ts",
|
|
56
57
|
"isolation": "node test/isolation.ts",
|
|
57
58
|
"docs": "node test/docs.ts",
|
|
58
|
-
"board": "node test/board.ts",
|
|
59
59
|
"channel": "node test/channel.ts",
|
|
60
60
|
"mirror": "node test/mirror.ts",
|
|
61
61
|
"identity": "node test/identity.ts",
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"shim": "DEVLOOP_CHANNEL_DRYRUN=1 DEVLOOP_CHANNEL_TOKEN=xoxb-DRYRUNSECRET DEVLOOP_MIRROR_DRYRUN=1 node test/shim.ts",
|
|
65
65
|
"lifecycle": "node test/lifecycle.ts",
|
|
66
66
|
"lifecycle-race": "node test/lifecycle-race.ts",
|
|
67
|
-
"test": "node test/smoke.ts && node test/loop.ts && node test/isolation.ts && node test/docs.ts && node test/
|
|
67
|
+
"test": "node test/smoke.ts && node test/loop.ts && node test/isolation.ts && node test/docs.ts && node test/channel.ts && node test/mirror.ts && node test/identity.ts && node test/run-agents.ts && node test/resolve-project.ts && node test/daemon.ts && DEVLOOP_CHANNEL_DRYRUN=1 DEVLOOP_CHANNEL_TOKEN=xoxb-DRYRUNSECRET DEVLOOP_MIRROR_DRYRUN=1 node test/agent-api.ts && DEVLOOP_CHANNEL_DRYRUN=1 DEVLOOP_CHANNEL_TOKEN=xoxb-DRYRUNSECRET DEVLOOP_MIRROR_DRYRUN=1 node test/shim.ts && node test/lifecycle.ts && node test/lifecycle-race.ts && node test/blocked.ts && node test/no-progress.ts && node test/migrate.ts && node test/release.ts && node test/mcp-config.ts && node test/init-service.ts && node test/mcp-merge.ts && node test/wal-checkpoint.ts && node test/version-sync.ts && node test/build-artifact.ts && node test/accept-rate.ts && node test/cycle-stage.ts && node test/seed.ts && node test/cli-tickets.ts && node test/open-wip.ts",
|
|
68
68
|
"cli-tickets": "node test/cli-tickets.ts",
|
|
69
69
|
"open-wip": "node test/open-wip.ts",
|
|
70
70
|
"wal-checkpoint": "node test/wal-checkpoint.ts",
|