@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.
Files changed (36) hide show
  1. package/README.md +30 -10
  2. package/dist/agentops.js +5 -68
  3. package/dist/cli.js +4 -0
  4. package/dist/db.js +0 -26
  5. package/dist/doctor.js +2 -2
  6. package/dist/install-claude-plugin.js +78 -0
  7. package/dist/mcp-merge.js +18 -19
  8. package/dist/mirrorstore.js +1 -1
  9. package/dist/plugin/.claude-plugin/marketplace.json +13 -0
  10. package/dist/plugin/.claude-plugin/plugin.json +11 -0
  11. package/dist/plugin/config/mcp.codex.toml.example +33 -0
  12. package/dist/plugin/config/mcp.example.json +15 -0
  13. package/dist/plugin/config/mcp.opencode.json.example +16 -0
  14. package/dist/plugin/config/projects.example.json +82 -0
  15. package/dist/plugin/hooks/hooks.json +16 -0
  16. package/dist/plugin/references/codex-integration.md +282 -0
  17. package/dist/plugin/references/config-schema.md +358 -0
  18. package/dist/plugin/references/conventions.md +2159 -0
  19. package/dist/plugin/skills/architect-agent/SKILL.md +231 -0
  20. package/dist/plugin/skills/communication-agent/SKILL.md +247 -0
  21. package/dist/plugin/skills/dev-agent/SKILL.md +373 -0
  22. package/dist/plugin/skills/init/SKILL.md +496 -0
  23. package/dist/plugin/skills/junior-dev-agent/SKILL.md +348 -0
  24. package/dist/plugin/skills/ops-agent/SKILL.md +219 -0
  25. package/dist/plugin/skills/pm-agent/SKILL.md +427 -0
  26. package/dist/plugin/skills/qa-agent/SKILL.md +299 -0
  27. package/dist/plugin/skills/reflect-agent/SKILL.md +271 -0
  28. package/dist/plugin/skills/senior-dev-agent/SKILL.md +353 -0
  29. package/dist/plugin/skills/sweep-agent/SKILL.md +180 -0
  30. package/dist/run-agents.js +373 -0
  31. package/dist/seed.js +4 -3
  32. package/dist/server.js +1 -1
  33. package/dist/shim.js +3 -4
  34. package/dist/tooldefs.js +3 -25
  35. package/package.json +5 -5
  36. package/dist/topicstore.js +0 -174
package/README.md CHANGED
@@ -1,12 +1,12 @@
1
1
  # dev-loop
2
2
 
3
3
  The standalone local **coordination hub** for the [dev-loop](https://github.com/dyzsasd/dev-loop)
4
- agents a **zero-build, zero-native-dependency** MCP system-of-record over `node:sqlite` with
5
- **per-agent identity**, a localhost **web-UI daemon**, an opt-in agent **op-API + thin stdio shim**,
6
- and a **CLI-portable** transport (Claude Code · Codex · opencode).
4
+ agents. It is a **zero-build, zero-native-dependency** MCP system of record over `node:sqlite`,
5
+ with **per-agent identity**, a localhost **web UI daemon**, an opt-in agent **op-API + thin stdio
6
+ shim**, and a transport that works across CLIs (Claude Code · Codex · opencode).
7
7
 
8
- > One trusted host, localhost-only. Identity is **cooperative attribution** (not anti-spoof). Secrets
9
- > live in env by **name** only. See the security envelope in
8
+ > One trusted host, localhost-only. Identity is **cooperative attribution**, not anti-spoofing.
9
+ > Secrets live in env by **name** only. See the security envelope in
10
10
  > [`docs/HUB-ARCHITECTURE.md`](https://github.com/dyzsasd/dev-loop/blob/main/docs/HUB-ARCHITECTURE.md).
11
11
 
12
12
  ## Install
@@ -15,7 +15,14 @@ and a **CLI-portable** transport (Claude Code · Codex · opencode).
15
15
  npm install -g @dyzsasd/dev-loop # requires Node >= 23.6 (built-in node:sqlite + .ts type-stripping; zero build); installs the `dev-loop` + `dev-loop-hub` bins
16
16
  ```
17
17
 
18
- This puts two bins on `PATH`: **`dev-loop`** (the CLI) and **`dev-loop-hub`** (the MCP server entry).
18
+ This installs two binaries on `PATH`: **`dev-loop`** for the CLI and **`dev-loop-hub`** for the
19
+ MCP server entrypoint. The package also ships the agent skills + shared references used by
20
+ `dev-loop run`, so the **scheduler mode needs no plugin** (it injects the skills + the hub MCP
21
+ itself). For Claude Code **interactive** slash commands, register the plugin from npm (no GitHub):
22
+
23
+ ```bash
24
+ dev-loop install-claude-plugin # writes a local npm-source marketplace, then prints the /plugin commands to run
25
+ ```
19
26
 
20
27
  ## CLI
21
28
 
@@ -24,6 +31,8 @@ dev-loop serve run the stdio MCP server (the agent transpo
24
31
  dev-loop shim the thin stdio MCP shim → the loopback daemon op-API
25
32
  dev-loop daemon up|down|status per-project daemon lifecycle — idempotent, auto web UI
26
33
  dev-loop init-service <key> <name> <PREFIX> turnkey-bootstrap a service-backend project
34
+ dev-loop run --cli claude|codex [--project <key>] [--agents core,outward] [--max-fires N] schedule agents (self-injects the hub MCP; no plugin)
35
+ dev-loop install-claude-plugin register a local npm-source marketplace so /plugin install loads it
27
36
  dev-loop mcp-merge <args> merge dev-loop-hub into a product .mcp.json (never clobbers)
28
37
  dev-loop seed <key> <name> [PREFIX] seed a project + actors + labels
29
38
  dev-loop doctor health-check the system-of-record (DOCTOR_OK)
@@ -33,18 +42,29 @@ dev-loop version | help
33
42
 
34
43
  ## Identity & project (the env contract)
35
44
 
36
- Every launcher sets, **per pane**, the identity the write is attributed to:
45
+ Every launcher sets the write identity **per pane**:
37
46
 
38
47
  | Env var | Meaning |
39
48
  |---|---|
40
- | `DEVLOOP_ACTOR` | the per-agent identity (`pm`/`qa`/`dev`/…) — the attribution |
49
+ | `DEVLOOP_ACTOR` | the per-agent identity (`pm`/`qa`/`dev`/...) — the attribution |
41
50
  | `DEVLOOP_PROJECT` | the pinned project key (or resolved from the cwd) |
42
51
  | `DEVLOOP_HUB_DB` | the SQLite system-of-record (default `~/.dev-loop/hub.db`) |
43
52
 
44
- Register it as an MCP server for your CLI `{ "command": "dev-loop", "args": ["serve"] }` (or
45
- `["shim"]` for the daemon transport). Per-CLI recipes + the identity gate:
53
+ Register it as an MCP server for your CLI with `{ "command": "dev-loop", "args": ["serve"] }`,
54
+ or use `["shim"]` for the daemon transport. Per-CLI recipes and the identity gate live in:
46
55
  [`docs/PORTABILITY.md`](https://github.com/dyzsasd/dev-loop/blob/main/docs/PORTABILITY.md).
47
56
 
57
+ For an unattended loop without Claude/Codex `/loop`, run the built-in scheduler:
58
+
59
+ ```bash
60
+ cd /path/to/product-repo # project is inferred from repoPath / repos[].path
61
+ dev-loop run --cli claude --agents core,communication
62
+ dev-loop run --cli codex --agents core,outward
63
+ ```
64
+
65
+ It owns cadence itself and shells out to the selected CLI once per agent fire. Use
66
+ `--project <key>` only when launching from outside the repo or overriding cwd detection.
67
+
48
68
  ## Docs
49
69
 
50
70
  - [Architecture + safety envelope](https://github.com/dyzsasd/dev-loop/blob/main/docs/HUB-ARCHITECTURE.md)
package/dist/agentops.js CHANGED
@@ -3,11 +3,11 @@
3
3
  // since DL-69 (the dispatch-sharing refactor) — the stdio MCP server (server.ts), whose 27 op-backed tool
4
4
  // handlers are now thin call-throughs to agentOp() (server.ts's toMcp() maps {status,body}→MCP ok()/err()).
5
5
  // So each policy — the read SELECTs, the save_issue/save_comment orchestration (the DL-24 per-transition
6
- // assignTo + the DL-32 prod-promotion gate + the REPLACE-labels/APPEND-relatedTo merge), and the doc/topic/
7
- // channel/mirror/label families (which also reuse the shared ticketwrite/docstore/topicstore/channelstore/
6
+ // assignTo + the DL-32 prod-promotion gate + the REPLACE-labels/APPEND-relatedTo merge), and the doc/
7
+ // channel/mirror/label families (which also reuse the shared ticketwrite/docstore/channelstore/
8
8
  // mirrorstore/labelstore) — has EXACTLY ONE definition. The old "edit both files" drift tripwire is RETIRED:
9
9
  // a change to any policy now lands in ONE place, and the differential-parity suite (test/shim.ts +
10
- // test/agent-api.ts, shim ≡ stdio for all 29 tools) is the structural guard against a future re-divergence.
10
+ // test/agent-api.ts, shim ≡ stdio for all 23 tools) is the structural guard against a future re-divergence.
11
11
  //
12
12
  // Each function takes a hub connection + the caller's already-resolved+validated actor (server.ts resolves it
13
13
  // from DEVLOOP_ACTOR + the G1 phantom-actor guard; the daemon from the X-Devloop-Actor header) and returns an
@@ -28,15 +28,9 @@ import { insertTicket, updateTicketRow, insertComment, loadRelease } from "./tic
28
28
  // The doc READS (doc.list/get/history/diff) + list_events are the SINGLE definition of those SELECTs — since
29
29
  // DL-69 server.ts's handlers dispatch through them (no longer a 1:1 duplicate of a server.ts copy).
30
30
  import { resolveDoc, latestVersion, docSave, docPublish, statusForDocErr, DOC_KINDS } from "./docstore.js";
31
- // DL-64 discussion-board family — the topic/post reads + writes (incl. the §25 chair/invited role gates +
32
- // the round/append rules) + the error→HTTP-status map are reused VERBATIM from the shared, side-effect-free
33
- // topicstore (exactly as the doc family reuses docstore.ts), so the op-API and the stdio server.ts can never
34
- // drift on a gate or a response shape. The op-API parses raw JSON, so each handler hand-validates the input
35
- // shapes server.ts gets from zod (the DL-63 read-handler lesson — a non-string id/body must 400, never a 500).
36
- import { topicList, topicGet, topicOpen, postAdd, topicSynthesize, topicClose, statusForTopicErr } from "./topicstore.js";
37
31
  // DL-67 channel family — the channel register/send/poll/ack/status HANDLER logic + the DL-4 roadmap bridge are
38
- // reused VERBATIM from the shared, side-effect-free channelstore (exactly as the doc/topic families reuse
39
- // docstore/topicstore), so the op-API and the stdio server.ts can never drift. channel.send/poll are ASYNC
32
+ // reused VERBATIM from the shared, side-effect-free channelstore (exactly as the doc family reuses
33
+ // docstore), so the op-API and the stdio server.ts can never drift. channel.send/poll are ASYNC
40
34
  // (network/dryrun), so agentOp returns OpResult|Promise<OpResult> and the daemon awaits it. The op-API parses
41
35
  // raw JSON → each handler hand-validates the shapes server.ts gets from zod (DL-63: a non-string arg → 400, never a 500).
42
36
  import { channelRegister, channelSend, channelPoll, channelAck, channelStatus, statusForChannelErr } from "./channelstore.js";
@@ -53,7 +47,6 @@ export const AGENT_OPS = TOOL_NAMES.filter((n) => n !== "whoami");
53
47
  // never mutate, so they bypass both). Kept here next to AGENT_OPS so the two lists can't drift. doc.save /
54
48
  // doc.publish join the ticket writes; the doc/event reads stay read-only (parity with the read ticket ops).
55
49
  export const AGENT_WRITE_OPS = new Set(["save_issue", "save_comment", "doc.save", "doc.publish",
56
- "topic.open", "post.add", "topic.synthesize", "topic.close", // DL-64: the 4 board writes
57
50
  "channel.register", "channel.send", "channel.poll", "channel.ack", // DL-67: the 4 channel writes (register/send/poll/ack mutate the channels/channel_messages tables); channel.status stays a read (query_only)
58
51
  "mirror.push", "create_issue_label"]); // DL-68: the 2 writes (mirror.push → mirror_map + the one-way Linear network write; create_issue_label → labels). mirror.status/list_issue_labels/get_project stay reads (query_only)
59
52
  export const isAgentOp = (s) => AGENT_OPS.includes(s);
@@ -354,56 +347,6 @@ function opDocPublish(db, projectId, actor, a) {
354
347
  const r = docPublish(db, projectId, actor, a);
355
348
  return r.ok ? okR(r.data) : errR(statusForDocErr(r.error), r.error);
356
349
  }
357
- // ─── DL-64: the discussion-board family (topic.*/post.add) — thin op-API wrappers over the shared topicstore ──
358
- // Mirror the doc-family pattern: hand-validate the raw-JSON inputs to a clean 400 (server.ts gets these from
359
- // zod), then delegate to topicstore (which owns the §25 role gates + round/append rules); a TopicResult error
360
- // maps to its HTTP status via statusForTopicErr. The reads (topic.list/topic.get) take the query_only db; the
361
- // writes (topic.open/post.add/topic.synthesize/topic.close ∈ AGENT_WRITE_OPS) take writeDb — the daemon routes.
362
- function opTopicList(db, projectId, actor, a) {
363
- if (a.status !== undefined && a.status !== "open" && a.status !== "closed")
364
- return errR(400, `status must be "open" or "closed"`);
365
- return okR(topicList(db, projectId, actor, a.status));
366
- }
367
- function opTopicGet(db, projectId, projectKey, a) {
368
- if (typeof a.id !== "string")
369
- return errR(400, "id must be a string");
370
- const r = topicGet(db, projectId, projectKey, a.id);
371
- return r.ok ? okR(r.data) : errR(statusForTopicErr(r.error), r.error);
372
- }
373
- function opTopicOpen(db, projectId, actor, a) {
374
- if (typeof a.question !== "string" || !a.question)
375
- return errR(400, "question required (a non-empty string)");
376
- if (!isStrArr(a.invited) || a.invited.length === 0)
377
- return errR(400, "invited required (a non-empty array of strings)");
378
- const r = topicOpen(db, projectId, actor, a);
379
- return r.ok ? okR(r.data) : errR(statusForTopicErr(r.error), r.error);
380
- }
381
- function opPostAdd(db, projectId, projectKey, actor, a) {
382
- if (typeof a.topicId !== "string")
383
- return errR(400, "topicId must be a string");
384
- if (typeof a.body !== "string" || !a.body)
385
- return errR(400, "body required (a non-empty string)");
386
- const r = postAdd(db, projectId, projectKey, actor, a);
387
- return r.ok ? okR(r.data) : errR(statusForTopicErr(r.error), r.error);
388
- }
389
- function opTopicSynthesize(db, projectId, projectKey, actor, a) {
390
- if (typeof a.topicId !== "string")
391
- return errR(400, "topicId must be a string");
392
- if (typeof a.body !== "string" || !a.body)
393
- return errR(400, "body required (a non-empty string)");
394
- if (a.nextRound !== undefined && typeof a.nextRound !== "boolean")
395
- return errR(400, "nextRound must be a boolean");
396
- const r = topicSynthesize(db, projectId, projectKey, actor, a);
397
- return r.ok ? okR(r.data) : errR(statusForTopicErr(r.error), r.error);
398
- }
399
- function opTopicClose(db, projectId, projectKey, actor, a) {
400
- if (typeof a.topicId !== "string")
401
- return errR(400, "topicId must be a string");
402
- if (typeof a.decision !== "string" || !a.decision)
403
- return errR(400, "decision required (a non-empty string)");
404
- const r = topicClose(db, projectId, projectKey, actor, a);
405
- return r.ok ? okR(r.data) : errR(statusForTopicErr(r.error), r.error);
406
- }
407
350
  // ─── DL-67: the IM channel family (channel.*) — thin op-API wrappers over the shared channelstore ──
408
351
  // Mirror the doc/topic pattern: hand-validate the raw-JSON inputs to a clean 400 (server.ts gets these from
409
352
  // zod — the DL-63 lesson), then delegate to channelstore (which owns the §16 line-build, the DL-4 roadmap
@@ -531,12 +474,6 @@ export function agentOp(op, db, projectId, projectKey, actor, args) {
531
474
  case "doc.diff": return opDocDiff(db, projectId, args);
532
475
  case "doc.save": return opDocSave(db, projectId, actor, args);
533
476
  case "doc.publish": return opDocPublish(db, projectId, actor, args);
534
- case "topic.list": return opTopicList(db, projectId, actor, args);
535
- case "topic.get": return opTopicGet(db, projectId, projectKey, args);
536
- case "topic.open": return opTopicOpen(db, projectId, actor, args);
537
- case "post.add": return opPostAdd(db, projectId, projectKey, actor, args);
538
- case "topic.synthesize": return opTopicSynthesize(db, projectId, projectKey, actor, args);
539
- case "topic.close": return opTopicClose(db, projectId, projectKey, actor, args);
540
477
  case "channel.register": return opChannelRegister(db, projectId, actor, args);
541
478
  case "channel.send": return opChannelSend(db, projectId, projectKey, actor, args);
542
479
  case "channel.poll": return opChannelPoll(db, projectId, projectKey, actor);
package/dist/cli.js CHANGED
@@ -20,6 +20,8 @@ const ROUTES = {
20
20
  daemon: ["server", "daemon"], // up | down | status | ensure (DL-41)
21
21
  doctor: ["server", "doctor"],
22
22
  seed: ["seed"],
23
+ run: ["run-agents"], // scheduler: own cadence + shells out to claude/codex once per fire
24
+ "install-claude-plugin": ["install-claude-plugin"], // register a local npm-source marketplace so Claude Code loads the published plugin
23
25
  "init-service": ["init-service"], // turnkey bootstrap (DL-60)
24
26
  "mcp-merge": ["mcp-merge"], // merge into a product .mcp.json, never clobbers (DL-61)
25
27
  "identity-check": ["server", "identity-check"], // the portability gate (PORTABILITY.md §4)
@@ -47,6 +49,8 @@ Usage: dev-loop <command> [args]
47
49
  shim run the thin stdio MCP shim → the loopback daemon op-API (hub.transport:"daemon")
48
50
  daemon up|down|status per-project daemon lifecycle — idempotent, auto-starts the localhost web UI
49
51
  init-service <key> <name> <PREFIX> turnkey-bootstrap a service-backend project (seed → doctor → daemon up)
52
+ run --cli claude|codex [--project <key>] [--agents core,outward] schedule agents by calling the selected CLI
53
+ install-claude-plugin register a local npm-source marketplace so /plugin install can load it
50
54
  mcp-merge <args> merge dev-loop-hub into a product .mcp.json (never clobbers other servers)
51
55
  seed <key> <name> [PREFIX] seed a project + actors + labels into the hub db
52
56
  doctor health-check the hub system-of-record (DOCTOR_OK)
package/dist/db.js CHANGED
@@ -116,32 +116,6 @@ CREATE TABLE IF NOT EXISTS document_versions (
116
116
  UNIQUE(doc_id, version)
117
117
  );
118
118
  CREATE INDEX IF NOT EXISTS idx_docversions_doc ON document_versions(doc_id, version);
119
- -- ── P5 discussion board: the Director chairs; invited agents post per round ────
120
- CREATE TABLE IF NOT EXISTS topics (
121
- id TEXT PRIMARY KEY,
122
- project_id TEXT NOT NULL REFERENCES projects(id),
123
- question TEXT NOT NULL,
124
- invited TEXT NOT NULL DEFAULT '[]', -- JSON array of actor handles
125
- status TEXT NOT NULL DEFAULT 'open' CHECK(status IN ('open','closed')),
126
- round INTEGER NOT NULL DEFAULT 1,
127
- round_opened_at TEXT NOT NULL, -- wall-clock for the state-free termination budget
128
- opened_by TEXT NOT NULL, -- the chair (authority = opened_by)
129
- opened_at TEXT NOT NULL,
130
- closed_at TEXT,
131
- decision TEXT -- inline terminal decision (set on close); DATA, never auto-applied (§17)
132
- );
133
- CREATE INDEX IF NOT EXISTS idx_topics_project_status ON topics(project_id, status);
134
- CREATE TABLE IF NOT EXISTS posts (
135
- id TEXT PRIMARY KEY,
136
- topic_id TEXT NOT NULL REFERENCES topics(id),
137
- round INTEGER NOT NULL,
138
- author TEXT NOT NULL, -- actor HANDLE (attribution)
139
- kind TEXT NOT NULL DEFAULT 'perspective' CHECK(kind IN ('perspective','synthesis')),
140
- body TEXT NOT NULL,
141
- created_at TEXT NOT NULL,
142
- UNIQUE(topic_id, round, author, kind) -- one perspective per (round, author); chair's synthesis coexists
143
- );
144
- CREATE INDEX IF NOT EXISTS idx_posts_topic ON posts(topic_id, round, created_at);
145
119
  -- ── P6 IM channel: per-project provider-agnostic two-way plane (§9/§16/§25) ───
146
120
  -- §16 STRUCTURAL: this table holds the ENV-VAR NAME (config_ref/secret_ref) + the room id
147
121
  -- (channel_ref), NEVER a token/secret/URL. The secret is read from process.env[config_ref]
package/dist/doctor.js CHANGED
@@ -41,7 +41,7 @@ export async function runDoctor(dbPath, opts = {}) {
41
41
  // 2b. A 0-byte file IS a valid (empty) SQLite db, so the read-only open above SUCCEEDS on a
42
42
  // truncated / zeroed / placeholder file — it just carries no schema; a non-SQLite file throws
43
43
  // on the first read. Either way it is not a system-of-record: report INVALID and write nothing.
44
- const HUB_TABLES = ["projects", "tickets", "documents", "topics", "actors", "events"]; // every table step 4 below counts — so a partial/foreign db fails HERE, cleanly, not mid-check
44
+ const HUB_TABLES = ["projects", "tickets", "documents", "actors", "events"]; // every table step 4 below counts — so a partial/foreign db fails HERE, cleanly, not mid-check
45
45
  let missing;
46
46
  try {
47
47
  const present = new Set(db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all().map((r) => r.name));
@@ -67,7 +67,7 @@ export async function runDoctor(dbPath, opts = {}) {
67
67
  Object.values(qc)[0] === "ok" ? pass("quick_check ok (no corruption)") : fail(`quick_check: ${JSON.stringify(qc)}`);
68
68
  // 4. Counts + per-project, and the unique-prefix integrity check (the real multi-project guard)
69
69
  const c = (sql) => db.prepare(sql).get().c;
70
- info(`projects=${c("SELECT count(*) c FROM projects")} tickets=${c("SELECT count(*) c FROM tickets")} docs=${c("SELECT count(*) c FROM documents")} topics=${c("SELECT count(*) c FROM topics")} actors=${c("SELECT count(*) c FROM actors")} events=${c("SELECT count(*) c FROM events")}`);
70
+ info(`projects=${c("SELECT count(*) c FROM projects")} tickets=${c("SELECT count(*) c FROM tickets")} docs=${c("SELECT count(*) c FROM documents")} actors=${c("SELECT count(*) c FROM actors")} events=${c("SELECT count(*) c FROM events")}`);
71
71
  const projects = db.prepare("SELECT id, key, ticket_prefix FROM projects ORDER BY key").all();
72
72
  const countByProject = db.prepare("SELECT count(*) c FROM tickets WHERE project_id = ?");
73
73
  for (const p of projects) {
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env node
2
+ // `dev-loop install-claude-plugin` — register a LOCAL marketplace whose single plugin has an `npm`
3
+ // source, so Claude Code installs the published @dyzsasd/dev-loop plugin from npm (no GitHub, no
4
+ // file-copy that drifts from the npm version). Claude Code marketplaces support an npm plugin source
5
+ // (docs: plugin-marketplaces). We write the tiny marketplace.json + print the two `/plugin` commands
6
+ // (those are interactive — this CLI can't run them).
7
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
8
+ import { homedir } from "node:os";
9
+ import { join, resolve } from "node:path";
10
+ import { fileURLToPath } from "node:url";
11
+ const MARKETPLACE = "dev-loop-npm";
12
+ const PLUGIN = "dev-loop";
13
+ const defaultDest = () => join(homedir(), ".claude", "plugins", "marketplaces", MARKETPLACE);
14
+ function usage() {
15
+ console.log(`dev-loop install-claude-plugin — register a local npm-source marketplace for the Claude plugin
16
+
17
+ Usage:
18
+ dev-loop install-claude-plugin [--dest <dir>] [--package <name>] [--version <semver>] [--dry-run]
19
+
20
+ Writes a marketplace.json whose plugin pulls from npm (default @dyzsasd/dev-loop), then prints the
21
+ two interactive /plugin commands to run. No GitHub, no file copy — the npm package is the single
22
+ source of truth for the plugin version.
23
+
24
+ Options:
25
+ --dest <dir> marketplace dir (default: ~/.claude/plugins/marketplaces/${MARKETPLACE})
26
+ --package <name> npm package (default: @dyzsasd/dev-loop)
27
+ --version <semver> pin a version (default: latest)
28
+ --dry-run print the marketplace.json + commands without writing`);
29
+ }
30
+ function die(msg, code = 2) {
31
+ console.error(`dev-loop install-claude-plugin: ${msg}`);
32
+ process.exit(code);
33
+ }
34
+ export function installClaudePlugin(argv = process.argv.slice(2)) {
35
+ const opts = { dest: defaultDest(), pkg: "@dyzsasd/dev-loop", version: "", dryRun: false };
36
+ for (let i = 0; i < argv.length; i++) {
37
+ const a = argv[i];
38
+ const next = () => argv[++i] ?? die(`${a} requires a value`);
39
+ if (a === "--help" || a === "-h") {
40
+ usage();
41
+ return 0;
42
+ }
43
+ else if (a === "--dest")
44
+ opts.dest = resolve(next());
45
+ else if (a === "--package")
46
+ opts.pkg = next();
47
+ else if (a === "--version")
48
+ opts.version = next();
49
+ else if (a === "--dry-run")
50
+ opts.dryRun = true;
51
+ else
52
+ die(`unknown option '${a}'`);
53
+ }
54
+ const source = { source: "npm", package: opts.pkg };
55
+ if (opts.version)
56
+ source.version = opts.version;
57
+ const marketplace = { name: MARKETPLACE, owner: { name: "Shuai" }, plugins: [{ name: PLUGIN, source }] };
58
+ const file = join(opts.dest, ".claude-plugin", "marketplace.json");
59
+ const json = JSON.stringify(marketplace, null, 2) + "\n";
60
+ if (opts.dryRun) {
61
+ console.log(`would write ${file}:\n${json}`);
62
+ }
63
+ else {
64
+ mkdirSync(join(opts.dest, ".claude-plugin"), { recursive: true });
65
+ writeFileSync(file, json);
66
+ console.log(`wrote ${file}`);
67
+ }
68
+ console.log(`\nNow run these two interactive Claude Code commands:`);
69
+ console.log(` /plugin marketplace add ${opts.dest}`);
70
+ console.log(` /plugin install ${PLUGIN}@${MARKETPLACE}`);
71
+ console.log(`\nThen /reload-plugins (or restart). Skills appear as /dev-loop:pm-agent … /dev-loop:init.`);
72
+ if (!opts.dryRun && !existsSync(file))
73
+ die(`failed to write ${file}`, 1);
74
+ return 0;
75
+ }
76
+ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
77
+ process.exit(installClaudePlugin());
78
+ }
package/dist/mcp-merge.js CHANGED
@@ -2,21 +2,22 @@
2
2
  // so init's `service` auto-wiring registers the hub server WITHOUT destroying any other MCP servers the
3
3
  // product already declares. Composes onto DL-60's init-service seam (c). §16: env-NAME-only — the entry
4
4
  // carries only `${VAR:-default}` env references (copied from the committed template), never a literal secret;
5
- // the hub DB path is intentionally omitted (the server defaults to ~/.dev-loop/hub.db). §17: this is a
5
+ // the hub DB path is intentionally omitted (the server defaults to ~/.dev-loop/hub.db). The normal installed
6
+ // shape is `command:"dev-loop", args:["serve"]`; old source templates with a `server.ts` arg are still patched
7
+ // in place. §17: this is a
6
8
  // data-file utility — it can only ever write the product `.mcp.json`, never a SKILL/conventions/code file.
7
9
  import { existsSync, readFileSync, writeFileSync, renameSync, unlinkSync } from "node:fs";
8
10
  import { join, dirname } from "node:path";
9
11
  import { fileURLToPath, pathToFileURL } from "node:url";
10
12
  const SERVER_NAME = "dev-loop-hub";
11
- // The published npm package packs only hub/dist + README config/ lives OUTSIDE the packed dir, so the repo
12
- // template is absent when installed (Codex review 2026-06-27). This embedded default is the fallback: the
13
- // canonical dev-loop-hub entry shape (env NAME-only; an args slot ending in server.ts that buildEntry rewrites
14
- // to the real absolute path). Keep it in sync with config/mcp.example.json's dev-loop-hub entry.
13
+ // The published npm package may not have the repo's config/ beside dist, so this embedded default is the
14
+ // fallback. Keep it in sync with config/mcp.example.json's dev-loop-hub entry.
15
15
  const DEFAULT_TEMPLATE = {
16
16
  mcpServers: {
17
17
  [SERVER_NAME]: {
18
- command: "node",
19
- args: ["server.ts"],
18
+ type: "stdio",
19
+ command: "dev-loop",
20
+ args: ["serve"],
20
21
  env: { DEVLOOP_ACTOR: "${DEVLOOP_ACTOR:-operator}" },
21
22
  },
22
23
  },
@@ -36,8 +37,9 @@ function resolveTemplate(explicitPath, repoPath) {
36
37
  return DEFAULT_TEMPLATE;
37
38
  }
38
39
  // Build the dev-loop-hub entry FROM the resolved template (the single source of truth for its shape — so a
39
- // future template change propagates), filling the absolute hub server path into `args` and pinning the
40
- // DEVLOOP_PROJECT default to the project key (matches the dogfood `.mcp.json` `${DEVLOOP_PROJECT:-<key>}`).
40
+ // future template change propagates), pinning the DEVLOOP_PROJECT default to the project key (matches the
41
+ // dogfood `.mcp.json` `${DEVLOOP_PROJECT:-<key>}`). Old templates with a `server.ts` arg are rewritten to the
42
+ // supplied source path for back-compat; current templates already use the PATH bin (`dev-loop serve`).
41
43
  function buildEntry(tmpl, hubServerPath, projectKey) {
42
44
  const src = tmpl.mcpServers?.[SERVER_NAME];
43
45
  if (!src || typeof src !== "object")
@@ -47,19 +49,16 @@ function buildEntry(tmpl, hubServerPath, projectKey) {
47
49
  // than write a malformed config. Real project keys are plain identifiers, so this never bites in practice.
48
50
  if (/[${}]/.test(projectKey))
49
51
  throw new Error(`project key ${JSON.stringify(projectKey)} contains '$', '{', or '}', which would break the .mcp.json \${VAR:-default} interpolation (DL-44) — use a plain identifier key`);
50
- // DL-66: the hub server path lands verbatim in the entry's `args` (line below) — another interpolated
51
- // .mcp.json string position — so a path carrying `$`/`{`/`}` would nest a `${...}` that Claude Code
52
- // mis-expands at parse-time interpolation, corrupting the resolved hub path and breaking the launch in that
53
- // pane. Guard it symmetrically with projectKey above; a real absolute checkout path never contains these
54
- // (same defense-in-depth bar the team set for the projectKey side in DL-44).
55
- if (/[${}]/.test(hubServerPath))
56
- throw new Error(`hub server path ${JSON.stringify(hubServerPath)} contains '$', '{', or '}', which would nest a \${...} in the .mcp.json args that Claude Code mis-expands at parse-time interpolation, corrupting the resolved hub path (DL-66) — use a path without those characters`);
57
52
  const e = structuredClone(src);
58
53
  const args = (e.args ?? []);
59
54
  const idx = args.findIndex((a) => typeof a === "string" && a.endsWith("server.ts"));
60
- if (idx < 0)
61
- throw new Error(`template ${SERVER_NAME} entry has no server.ts arg to fill`);
62
- args[idx] = hubServerPath; // the real absolute path, replacing the <ABS-PATH-TO-dev-loop>/... placeholder
55
+ if (idx >= 0) {
56
+ // DL-66: a legacy server path lands verbatim in an interpolated .mcp.json string position, so keep the
57
+ // old guard for old templates. Current `dev-loop serve` templates do not write this path at all.
58
+ if (/[${}]/.test(hubServerPath))
59
+ throw new Error(`hub server path ${JSON.stringify(hubServerPath)} contains '$', '{', or '}', which would nest a \${...} in the .mcp.json args that Claude Code mis-expands at parse-time interpolation, corrupting the resolved hub path (DL-66) — use a path without those characters`);
60
+ args[idx] = hubServerPath; // legacy placeholder replacement
61
+ }
63
62
  e.args = args;
64
63
  // env stays NAME-only; pin the project key as the DEVLOOP_PROJECT default (single-level, no nested ${...} — DL-44)
65
64
  e.env = { ...(e.env ?? {}), DEVLOOP_PROJECT: `\${DEVLOOP_PROJECT:-${projectKey}}` };
@@ -28,7 +28,7 @@ import { isEnvName } from "./channelstore.js";
28
28
  // mirror.push side-effect-free: it previews the would-push `ops`, hits NO network, and persists NO mirror_map
29
29
  // row (DL-11). Set it in the spawned process env (the MIRROR_OK suite + the agent-api/shim npm scripts do).
30
30
  const MIRROR_DRYRUN = process.env.DEVLOOP_MIRROR_DRYRUN === "1";
31
- const MIRROR_BANNER = "> 🤖 Mirrored from the dev-loop hub — edits here are IGNORED and overwritten on the next push. Give direction via the Director (conventions §25).";
31
+ const MIRROR_BANNER = "> 🤖 Mirrored from the dev-loop hub — edits here are IGNORED and overwritten on the next push. Give direction by filing a Todo to PM (conventions §9a).";
32
32
  const toTicket = (r) => ({
33
33
  id: r.id, project_id: r.project_id, title: r.title, description: r.description, type: r.type,
34
34
  state: r.state, assignee: r.assignee, priority: r.priority,
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "dev-loop",
3
+ "owner": { "name": "Shuai" },
4
+ "description": "Autonomous multi-agent SDLC loop with PM/QA/Dev, outward Ops/Architect/Communication roles, optional two-tier Dev, a Codex-portable service hub, and a built-in scheduler.",
5
+ "plugins": [
6
+ {
7
+ "name": "dev-loop",
8
+ "source": "./",
9
+ "version": "0.23.0",
10
+ "description": "Runs a software-development loop coordinated through ticket state. Supports Linear, a local file board, or a node:sqlite service hub with per-agent identity, localhost UI, Lark/Slack channel, one-way Linear mirror, Codex/opencode portability, and a scheduler that calls Claude/Codex CLI without using /loop. Includes Communication for daily public product article drafts."
11
+ }
12
+ ]
13
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "dev-loop",
3
+ "displayName": "dev-loop — autonomous SDLC agents",
4
+ "description": "A multi-agent software delivery loop coordinated through ticket state. PM, QA, Dev, Sweep, Reflect, Ops, Architect, and Communication cover product discovery, testing, implementation, hygiene, retrospectives, production watch, technical-health audits, and public article drafts. The optional senior/junior Dev split lets a stronger model design while a cheaper model implements. Coordination can run on Linear, a local file board, or the service hub with per-agent identity, a localhost web UI, documents, Lark/Slack channel, one-way Linear mirror, Codex/opencode portability, and a built-in scheduler that shells out to Claude or Codex without relying on /loop.",
5
+ "version": "0.23.0",
6
+ "author": {
7
+ "name": "Shuai"
8
+ },
9
+ "keywords": ["linear", "agents", "pm", "qa", "dev", "automation", "sdlc", "workflow"],
10
+ "license": "MIT"
11
+ }
@@ -0,0 +1,33 @@
1
+ # TEMPLATE for backend:"service" on the CODEX CLI (dev-loop P8 portability; see docs/PORTABILITY.md).
2
+ # Recommended install: `npm i -g @dyzsasd/dev-loop`, then merge this into ~/.codex/config.toml.
3
+ #
4
+ # CERTIFIED 2026-06-25 on codex-cli 0.142.0 (docs/PORTABILITY.md §4a). KEY FINDING: Codex spawns the MCP
5
+ # subprocess with ONLY this file's `env` block — it does NOT inherit the launching shell's process env.
6
+ # So DEVLOOP_ACTOR canNOT ride the process env on Codex (the gate returns "operator"). Per-pane identity
7
+ # rides a `-c` OVERRIDE instead, which MERGES a dotted key into the env table below:
8
+ # # one pane = one agent identity:
9
+ # codex exec -c 'mcp_servers.dev-loop-hub.env.DEVLOOP_ACTOR="dev"' \
10
+ # --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check "$PROMPT"
11
+ # → whoami returns {"actor":"dev","project":"dev-loop",…} (project/db preserved from the static env).
12
+ # # communication-agent pane:
13
+ # codex exec -c 'mcp_servers.dev-loop-hub.env.DEVLOOP_ACTOR="communication"' \
14
+ # --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check "$PROMPT"
15
+ # Leave DEVLOOP_ACTOR ABSENT below (the -c override supplies it per pane). VERIFY the [mcp_servers.<name>]
16
+ # schema against your Codex version (formats drift). Confirm with the §4 gate before trusting writes.
17
+
18
+ # OPT-IN daemon transport (DL-55, P2): to route the core ticket tools through the ONE running daemon
19
+ # instead of opening hub.db directly, change args to ["shim"] (from ["serve"]).
20
+ # REQUIRES the per-project daemon up (DL-42 auto-start or `dev-loop daemon up`) + settings_json.hub.transport=
21
+ # "daemon" (DL-43). The shim discovers the loopback port from the DL-41 runfile `~/.dev-loop/daemon-<key>.json`
22
+ # (override with DEVLOOP_HUB_PORT) — never hardcodes 8787. Scope this increment: the 5 core ticket tools +
23
+ # whoami; doc.*/topic.*/channel.* still need the default `serve` entry below.
24
+ [mcp_servers.dev-loop-hub]
25
+ command = "dev-loop"
26
+ args = ["serve"]
27
+ # STATIC, shared-across-panes vars ONLY. DEVLOOP_ACTOR is intentionally ABSENT — on Codex it does NOT ride
28
+ # the process env (CERTIFIED above: Codex doesn't inherit it); supply it per-pane via the `-c` override.
29
+ # Use an ABSOLUTE db path (TOML env is not shell-expanded).
30
+ # DEVLOOP_PROJECT = "" (literal empty — TOML is NOT shell-expanded, so never use `${DEVLOOP_PROJECT:-}`,
31
+ # which would be a literal string and fail the G2 phantom-project guard). Empty ⇒ the hub auto-resolves
32
+ # the project from the spawned process's cwd (DL-13); set a non-empty value here to pin one explicitly.
33
+ env = { DEVLOOP_PROJECT = "", DEVLOOP_HUB_DB = "/ABSOLUTE/PATH/.dev-loop/hub.db" }
@@ -0,0 +1,15 @@
1
+ {
2
+ "_comment": "TEMPLATE for backend:\"service\" (conventions §18; docs/HUB-ARCHITECTURE.md). Copy to your PRODUCT repo root as `.mcp.json` (or pass via `claude --mcp-config`). Recommended install: `npm i -g @dyzsasd/dev-loop`, then use command:\"dev-loop\", args:[\"serve\"]. The ${VAR} values are expanded by Claude Code at config-parse time from each pane's launching shell, so a per-pane `DEVLOOP_ACTOR` attributes writes to the right agent. Keep these single-level (`${VAR:-literal}`) — do NOT nest `${...}` inside a default (DL-44). DEVLOOP_PROJECT defaults to EMPTY (`:-`): when unset the hub auto-resolves the project from the spawned process's cwd (DL-13). To pin one explicitly, export DEVLOOP_PROJECT. The hub DB path is intentionally NOT set here: the server defaults to `~/.dev-loop/hub.db`; to point at a non-default DB, export DEVLOOP_HUB_DB in the launching shell. Source-checkout fallback for plugin developers: command:\"node\", args:[\"/abs/path/to/dev-loop/hub/src/server.ts\"].",
3
+ "_shim": "OPT-IN daemon transport (DL-55): to route the core ticket tools through the ONE running daemon instead of each pane opening hub.db directly, use args:[\"shim\"] instead of [\"serve\"] (or point a source checkout at hub/src/shim.ts). REQUIRES the per-project daemon running and settings_json.hub.transport=\"daemon\" (DL-43). Scope: the 5 core ticket tools + whoami — doc.*/topic.*/channel.*/mirror.*/list_events still need serve.",
4
+ "mcpServers": {
5
+ "dev-loop-hub": {
6
+ "type": "stdio",
7
+ "command": "dev-loop",
8
+ "args": ["serve"],
9
+ "env": {
10
+ "DEVLOOP_ACTOR": "${DEVLOOP_ACTOR:-operator}",
11
+ "DEVLOOP_PROJECT": "${DEVLOOP_PROJECT:-}"
12
+ }
13
+ }
14
+ }
15
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "_comment": "TEMPLATE for backend:\"service\" on OPENCODE (dev-loop P8 portability; see docs/PORTABILITY.md). Recommended install: `npm i -g @dyzsasd/dev-loop`, then merge the `mcp` entry into your opencode config (e.g. ~/.config/opencode/config.json or a repo-local opencode.json). VERIFY against your installed opencode version: (1) the exact `mcp` server schema (this uses type:\"local\" + a command ARRAY + an `environment` map), and (2) whether opencode propagates the launching process env to the MCP subprocess. Per-pane identity is load-bearing: DEVLOOP_ACTOR differs per agent pane, so it must ride the launcher/env or an opencode-specific override. Confirm with the identity gate in docs/PORTABILITY.md.",
3
+ "mcp": {
4
+ "dev-loop-hub": {
5
+ "type": "local",
6
+ "command": ["dev-loop", "serve"],
7
+ "environment": {
8
+ "DEVLOOP_PROJECT": "",
9
+ "DEVLOOP_HUB_DB": "/ABSOLUTE/PATH/.dev-loop/hub.db"
10
+ },
11
+ "_DEVLOOP_PROJECT_note": "literal empty (NOT shell-expanded here) ⇒ the hub auto-resolves the project from the spawned process's cwd (DL-13); set a non-empty value to pin one explicitly, or omit the key.",
12
+ "_shim_note": "OPT-IN daemon transport (DL-55): to route the core ticket tools through the running daemon instead of opening hub.db directly, change command to [\"dev-loop\", \"shim\"]. REQUIRES the per-project daemon up + settings_json.hub.transport=\"daemon\" (DL-43). Scope: the 5 core ticket tools + whoami — doc.*/topic.*/channel.* still need serve.",
13
+ "enabled": true
14
+ }
15
+ }
16
+ }
@@ -0,0 +1,82 @@
1
+ {
2
+ "defaultProject": "monpick",
3
+ "projects": {
4
+ "monpick": {
5
+ "linearTeam": "Citronetic",
6
+ "linearProject": "MonPick",
7
+ "repoPath": "/Users/shuai/workspace/citronetic/monpick",
8
+ "strategyDoc": "shopmy-analysis/15-parity-report.md",
9
+ "mode": "dry-run",
10
+ "autonomy": "ask",
11
+ "testEnv": {
12
+ "baseUrl": "https://monpick.vercel.app",
13
+ "setup": "python3 -m venv .venv && .venv/bin/pip install -q playwright && .venv/bin/playwright install chromium",
14
+ "testCommand": ".venv/bin/python3 tests/{suite}",
15
+ "notes": "Playwright Python suites in tests/. Run testEnv.setup once if the .venv/playwright harness is missing. Personas: demo-creator@monpick.dev / password123 (creator), demo-brand@monpick.dev / password123 (brand), demo-shopper@monpick.dev / password123 (shopper)."
16
+ },
17
+ "build": {
18
+ "typecheck": "npx tsc --noEmit",
19
+ "build": "pnpm build",
20
+ "test": "pnpm exec tsx tests/*.test.ts"
21
+ },
22
+ "git": {
23
+ "defaultBranch": "main",
24
+ "autoCommit": true,
25
+ "autoPush": true,
26
+ "autoDeploy": true
27
+ },
28
+ "deploy": {
29
+ "command": "vercel --prod --yes"
30
+ },
31
+ "codex": {
32
+ "_comment": "OPTIONAL Codex companion (conventions §24). Absent OR enabled:false OR codex CLI not on PATH ⇒ never invoked (100% unchanged). Needs the `codex` CLI (npm i -g @openai/codex; codex login) + the codex-plugin-cc plugin.",
33
+ "enabled": true,
34
+ "review": true,
35
+ "rescue": false,
36
+ "imageGen": true,
37
+ "assetsDir": "public/generated",
38
+ "model": null,
39
+ "effort": null
40
+ },
41
+ "communication": {
42
+ "_comment": "OPTIONAL communication-agent: drafts one public-facing product article per cadence. Draft-only: never publishes externally, never commits/pushes/deploys. Codex launch uses DEVLOOP_ACTOR=communication via the PORTABILITY.md identity recipe.",
43
+ "cadence": "daily",
44
+ "language": "en",
45
+ "audience": "current and prospective users",
46
+ "tone": "clear, concrete, human, and restrained",
47
+ "maxWords": 900,
48
+ "sourceWindowDays": 7,
49
+ "output": "data",
50
+ "outputDir": "communications",
51
+ "repoOutputDir": "docs/communications",
52
+ "includeUnreleased": false
53
+ },
54
+ "notify": {
55
+ "_comment": "OPTIONAL (conventions §9): ping the operator on a human-park (blocked+needs-pm+Bail-shape: external-prereq) via a Slack/Lark webhook. webhookEnv names an env var holding the webhook URL (a §16 secret — NEVER inline a real URL in a committed file). Absent => no-op. type: slack | lark.",
56
+ "type": "lark",
57
+ "webhookEnv": "DEVLOOP_NOTIFY_WEBHOOK",
58
+ "events": ["human-parked"]
59
+ },
60
+ "blockedStateName": null
61
+ },
62
+ "acme-suite": {
63
+ "_comment": "MULTI-REPO EXAMPLE (conventions §19). monpick above stays single-repo via top-level repoPath/build/git — back-compat. Here repos[] replaces a single repoPath; each ticket targets a repo via a repo:<name> label. role docs/primary picks the doc-home repo for strategyDoc; lang is informational; build/defaultBranch/deploy/contributorSkill resolve per-repo else top-level; autoCommit/autoPush/autoDeploy stay product-level in git.",
64
+ "linearTeam": "Acme",
65
+ "linearProject": "Acme Suite",
66
+ "repos": [
67
+ { "name": "web", "path": "/abs/path/to/acme-web", "role": "primary", "lang": "ts", "defaultBranch": "main", "deploy": { "command": "vercel --prod --yes", "healthCheck": "https://acme.example.com" } },
68
+ { "name": "api", "path": "/abs/path/to/acme-api", "role": "docs", "lang": "go", "defaultBranch": "main", "build": { "typecheck": "go build ./...", "test": "go test ./..." }, "deploy": { "command": "flyctl deploy", "healthCheck": "https://api.acme.example.com/healthz" } }
69
+ ],
70
+ "strategyDoc": "docs/strategy.md",
71
+ "mode": "dry-run",
72
+ "autonomy": "ask",
73
+ "testEnv": {
74
+ "baseUrl": "https://acme.example.com",
75
+ "notes": "One product baseUrl (conventions §19 limit: the api repo has no URL of its own). Personas in a vault — ask user."
76
+ },
77
+ "build": { "typecheck": "pnpm -r typecheck", "build": "pnpm -r build" },
78
+ "git": { "defaultBranch": "main", "autoCommit": true, "autoPush": true, "autoDeploy": false },
79
+ "blockedStateName": null
80
+ }
81
+ }
82
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "description": "dev-loop turnkey auto-start (DL-42, §17 operator-applied [pm-proposal]). On session start, ensure the per-project hub daemon + web UI is up via DL-41's idempotent `daemon up`. Safe-by-construction: `up` is cwd-resolved and a clean no-op for a non-service / Linear / local project (prints 'no project resolved … nothing to start', exit 0), so a project not using the hub is byte-for-byte unaffected. localhost-only (§16); output is swallowed and the exit forced 0 so a SessionStart never pollutes context or aborts startup.",
3
+ "hooks": {
4
+ "SessionStart": [
5
+ {
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hub/src/server.ts\" daemon up >/dev/null 2>&1 || true",
10
+ "timeout": 15
11
+ }
12
+ ]
13
+ }
14
+ ]
15
+ }
16
+ }