declare-cc 0.6.0 → 1.0.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.
@@ -1,9 +1,30 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
6
  var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
4
9
  var __commonJS = (cb, mod) => function __require() {
5
10
  return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
6
11
  };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
7
28
 
8
29
  // src/git/commit.js
9
30
  var require_commit = __commonJS({
@@ -1531,7 +1552,7 @@ var require_help = __commonJS({
1531
1552
  usage: "/declare:help"
1532
1553
  }
1533
1554
  ],
1534
- version: "0.5.9"
1555
+ version: "1.0.0"
1535
1556
  };
1536
1557
  }
1537
1558
  module2.exports = { runHelp: runHelp2 };
@@ -1862,6 +1883,7 @@ var require_add_milestones_batch = __commonJS({
1862
1883
  milestones.push({
1863
1884
  id,
1864
1885
  title: input.title,
1886
+ description: input.description || "",
1865
1887
  status: "PENDING",
1866
1888
  realizes,
1867
1889
  hasPlan: false
@@ -1872,7 +1894,7 @@ var require_add_milestones_batch = __commonJS({
1872
1894
  decl.milestones.push(id);
1873
1895
  }
1874
1896
  }
1875
- results.push({ id, title: input.title, realizes, status: "PENDING" });
1897
+ results.push({ id, title: input.title, description: input.description || "", realizes, status: "PENDING" });
1876
1898
  }
1877
1899
  const futureOutput = writeFutureFile(declarations, projectName);
1878
1900
  writeFileSync(futurePath, futureOutput, "utf-8");
@@ -4255,11 +4277,92 @@ var require_health_check = __commonJS({
4255
4277
  }
4256
4278
  });
4257
4279
 
4280
+ // src/server/ai-runner.js
4281
+ var require_ai_runner = __commonJS({
4282
+ "src/server/ai-runner.js"(exports2, module2) {
4283
+ "use strict";
4284
+ var _query;
4285
+ async function getQuery() {
4286
+ if (!_query) {
4287
+ const sdk = await import("@anthropic-ai/claude-agent-sdk");
4288
+ _query = sdk.query;
4289
+ }
4290
+ return _query;
4291
+ }
4292
+ async function runAI(prompt, opts = {}) {
4293
+ const queryFn = await getQuery();
4294
+ const abortController = opts.abortController || new AbortController();
4295
+ const timeoutMs = opts.withTools || opts.allowedTools ? 10 * 60 * 1e3 : 2 * 60 * 1e3;
4296
+ const timeoutId = setTimeout(() => {
4297
+ if (!abortController.signal.aborted) abortController.abort();
4298
+ }, timeoutMs);
4299
+ const savedClaudeCode = process.env.CLAUDECODE;
4300
+ delete process.env.CLAUDECODE;
4301
+ try {
4302
+ const env = { ...process.env };
4303
+ delete env.CLAUDECODE;
4304
+ const queryOpts = {
4305
+ model: opts.model || "haiku",
4306
+ maxTurns: opts.maxTurns || 1,
4307
+ cwd: opts.cwd || process.cwd(),
4308
+ abortController,
4309
+ env,
4310
+ systemPrompt: opts.systemPrompt || "You are a helpful assistant. Respond concisely and directly."
4311
+ };
4312
+ if (opts.withTools || opts.allowedTools) {
4313
+ queryOpts.allowedTools = opts.allowedTools || ["Read", "Write", "Edit", "Bash", "Glob", "Grep"];
4314
+ queryOpts.permissionMode = "bypassPermissions";
4315
+ if (!opts.maxTurns) queryOpts.maxTurns = 10;
4316
+ } else {
4317
+ queryOpts.tools = [];
4318
+ }
4319
+ const conversation = queryFn({
4320
+ prompt,
4321
+ options: queryOpts
4322
+ });
4323
+ let resultText = "";
4324
+ for await (const message of conversation) {
4325
+ if (message.type === "assistant") {
4326
+ const content = message.message?.content;
4327
+ if (Array.isArray(content)) {
4328
+ for (const block of content) {
4329
+ if (block.type === "text" && block.text) {
4330
+ resultText += block.text;
4331
+ if (opts.onText) opts.onText(block.text);
4332
+ }
4333
+ }
4334
+ }
4335
+ } else if (message.type === "result") {
4336
+ if (message.subtype === "success" && message.result) {
4337
+ resultText = message.result;
4338
+ } else if (message.is_error) {
4339
+ const errDetail = message.errors?.join(", ") || message.error || JSON.stringify(message);
4340
+ console.error("[ai-runner] SDK error result:", errDetail);
4341
+ return { text: "", error: errDetail || "AI query failed" };
4342
+ }
4343
+ }
4344
+ }
4345
+ return { text: resultText };
4346
+ } catch (err) {
4347
+ console.error("[ai-runner] Exception:", err.message || err);
4348
+ if (abortController.signal.aborted) {
4349
+ return { text: "", error: "Cancelled" };
4350
+ }
4351
+ return { text: "", error: String(err.message || err) };
4352
+ } finally {
4353
+ clearTimeout(timeoutId);
4354
+ if (savedClaudeCode) process.env.CLAUDECODE = savedClaudeCode;
4355
+ }
4356
+ }
4357
+ module2.exports = { runAI };
4358
+ }
4359
+ });
4360
+
4258
4361
  // src/server/process-manager.js
4259
4362
  var require_process_manager = __commonJS({
4260
4363
  "src/server/process-manager.js"(exports2, module2) {
4261
4364
  "use strict";
4262
- var { spawn } = require("node:child_process");
4365
+ var { runAI } = require_ai_runner();
4263
4366
  var fs = require("node:fs");
4264
4367
  var path = require("node:path");
4265
4368
  var { findMilestoneFolder } = require_milestone_folders();
@@ -4270,7 +4373,49 @@ var require_process_manager = __commonJS({
4270
4373
  } catch (_) {
4271
4374
  }
4272
4375
  }
4273
- function createProcessManager(sseClients, cwd) {
4376
+ function buildExecutionPrompt(actionId, milestoneId, ctx) {
4377
+ if (!ctx) {
4378
+ return `Run /declare:execute ${milestoneId} for action ${actionId} only. Do not ask questions, execute autonomously.`;
4379
+ }
4380
+ const lines = [
4381
+ `Execute action ${actionId} for milestone ${milestoneId}. Work autonomously \u2014 do not ask questions.`,
4382
+ ""
4383
+ ];
4384
+ if (ctx.declaration) {
4385
+ lines.push(`## Declaration: ${ctx.declaration.id}`);
4386
+ lines.push(ctx.declaration.statement);
4387
+ lines.push("");
4388
+ }
4389
+ if (ctx.milestone) {
4390
+ lines.push(`## Milestone: ${ctx.milestone.id} \u2014 ${ctx.milestone.title}`);
4391
+ if (ctx.milestone.description) {
4392
+ lines.push(ctx.milestone.description);
4393
+ }
4394
+ lines.push("");
4395
+ }
4396
+ lines.push(`## Action: ${ctx.action.id} \u2014 ${ctx.action.title}`);
4397
+ if (ctx.action.produces) {
4398
+ lines.push(`**Produces:** ${ctx.action.produces}`);
4399
+ }
4400
+ lines.push("");
4401
+ if (ctx.siblingActions.length > 0) {
4402
+ lines.push("## Other actions for this milestone (do NOT do these, just for context):");
4403
+ for (const s of ctx.siblingActions) {
4404
+ const statusMark = s.status === "DONE" ? " [DONE]" : "";
4405
+ lines.push(`- ${s.id}: ${s.title}${s.produces ? " (produces: " + s.produces + ")" : ""}${statusMark}`);
4406
+ }
4407
+ lines.push("");
4408
+ }
4409
+ lines.push("## Instructions");
4410
+ lines.push("1. Read the project context files: .planning/FUTURE.md, .planning/MILESTONES.md, .planning/STATE.md");
4411
+ lines.push('2. Understand what this action needs to deliver based on the "Produces" field above');
4412
+ lines.push("3. Explore the codebase to find the right files to modify");
4413
+ lines.push("4. Implement the changes \u2014 write real, working code");
4414
+ lines.push("5. Verify your changes work (run relevant tests, check for errors)");
4415
+ lines.push("6. Commit your changes with a descriptive message");
4416
+ return lines.join("\n");
4417
+ }
4418
+ function createProcessManager(sseClients, cwd, registry) {
4274
4419
  const processes = /* @__PURE__ */ new Map();
4275
4420
  function broadcast(event, data) {
4276
4421
  const payload = `event: ${event}
@@ -4285,32 +4430,14 @@ data: ${JSON.stringify(data)}
4285
4430
  }
4286
4431
  }
4287
4432
  }
4288
- function createLineHandler(actionId, streamName, logPath) {
4289
- let buffer = "";
4290
- return (chunk) => {
4291
- buffer += chunk.toString();
4292
- const lines = buffer.split("\n");
4293
- buffer = lines.pop() || "";
4294
- for (const line of lines) {
4295
- broadcast("action-output", { actionId, text: line, stream: streamName });
4296
- appendLog(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] [${actionId}] [${streamName}] ${line}`);
4297
- }
4298
- };
4299
- }
4300
- function execute(actionId, milestoneId) {
4433
+ function execute(actionId, milestoneId, ctx) {
4301
4434
  if (processes.size > 0) {
4302
4435
  return { error: "busy", status: 409 };
4303
4436
  }
4304
4437
  if (processes.has(actionId)) {
4305
4438
  return { error: "already_running", status: 409 };
4306
4439
  }
4307
- const prompt = `Run /declare:execute ${milestoneId} for action ${actionId} only. Do not ask questions, execute autonomously.`;
4308
- const spawnEnv = { ...process.env, FORCE_COLOR: "0" };
4309
- delete spawnEnv.CLAUDECODE;
4310
- const proc = spawn("claude", ["-p", prompt], {
4311
- cwd,
4312
- env: spawnEnv
4313
- });
4440
+ const prompt = buildExecutionPrompt(actionId, milestoneId, ctx);
4314
4441
  const planningDir = path.join(cwd, ".planning");
4315
4442
  const milestoneFolder = findMilestoneFolder(planningDir, milestoneId);
4316
4443
  let logPath;
@@ -4320,28 +4447,56 @@ data: ${JSON.stringify(data)}
4320
4447
  process.stderr.write(`[declare] Warning: milestone folder not found for ${milestoneId}, skipping execution log
4321
4448
  `);
4322
4449
  }
4323
- processes.set(actionId, { proc, milestoneId, logPath });
4450
+ const abortController = new AbortController();
4451
+ let agentId;
4452
+ if (registry) {
4453
+ const agent = registry.spawn("execution", actionId, milestoneId);
4454
+ agentId = agent.id;
4455
+ }
4456
+ processes.set(actionId, { abortController, milestoneId, logPath, agentId });
4324
4457
  appendLog(logPath, `
4325
4458
  === START ${actionId} @ ${(/* @__PURE__ */ new Date()).toISOString()} ===`);
4326
- if (proc.stdout) {
4327
- proc.stdout.on("data", createLineHandler(actionId, "stdout", logPath));
4328
- }
4329
- if (proc.stderr) {
4330
- proc.stderr.on("data", createLineHandler(actionId, "stderr", logPath));
4331
- }
4332
- proc.on("close", (exitCode) => {
4459
+ runAI(prompt, {
4460
+ cwd,
4461
+ model: "sonnet",
4462
+ withTools: true,
4463
+ maxTurns: 10,
4464
+ abortController,
4465
+ onText: (chunk) => {
4466
+ broadcast("action-output", { actionId, text: chunk, stream: "stdout" });
4467
+ appendLog(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] [${actionId}] [stdout] ${chunk}`);
4468
+ }
4469
+ }).then(({ text, error }) => {
4333
4470
  const entry = processes.get(actionId);
4334
- appendLog(entry?.logPath, `=== END ${actionId} @ ${(/* @__PURE__ */ new Date()).toISOString()} exit=${exitCode ?? -1} ===
4471
+ const entryAgentId = entry?.agentId;
4472
+ const exitCode = error ? 1 : 0;
4473
+ appendLog(entry?.logPath, `=== END ${actionId} @ ${(/* @__PURE__ */ new Date()).toISOString()} exit=${exitCode} ===
4335
4474
  `);
4336
4475
  processes.delete(actionId);
4337
- broadcast("action-complete", { actionId, exitCode: exitCode ?? -1 });
4338
- });
4339
- proc.on("error", (_err) => {
4476
+ broadcast("action-complete", { actionId, exitCode });
4477
+ if (registry && entryAgentId) {
4478
+ if (exitCode === 0) {
4479
+ const mFolder = entry?.logPath ? path.dirname(entry.logPath) : null;
4480
+ registry.complete(entryAgentId, {
4481
+ actionId,
4482
+ milestoneId: entry?.milestoneId || milestoneId,
4483
+ summaryPath: mFolder ? path.join(mFolder, actionId + "-SUMMARY.md") : null,
4484
+ logPath: entry?.logPath || null
4485
+ });
4486
+ } else {
4487
+ registry.fail(entryAgentId, exitCode, error || "execution failed");
4488
+ }
4489
+ }
4490
+ }).catch((err) => {
4340
4491
  const entry = processes.get(actionId);
4492
+ const entryAgentId = entry?.agentId;
4341
4493
  appendLog(entry?.logPath, `=== ERROR ${actionId} @ ${(/* @__PURE__ */ new Date()).toISOString()} ===
4342
4494
  `);
4343
4495
  processes.delete(actionId);
4344
4496
  broadcast("action-complete", { actionId, exitCode: -1 });
4497
+ if (registry && entryAgentId) {
4498
+ registry.fail(entryAgentId, -1, String(err.message || err));
4499
+ }
4345
4500
  });
4346
4501
  return { ok: true };
4347
4502
  }
@@ -4350,7 +4505,7 @@ data: ${JSON.stringify(data)}
4350
4505
  if (!entry) {
4351
4506
  return { error: "not_running", status: 404 };
4352
4507
  }
4353
- entry.proc.kill("SIGTERM");
4508
+ entry.abortController.abort();
4354
4509
  return { ok: true };
4355
4510
  }
4356
4511
  function running() {
@@ -4366,7 +4521,7 @@ data: ${JSON.stringify(data)}
4366
4521
  var require_derivation_runner = __commonJS({
4367
4522
  "src/server/derivation-runner.js"(exports2, module2) {
4368
4523
  "use strict";
4369
- var { spawn } = require("node:child_process");
4524
+ var { runAI } = require_ai_runner();
4370
4525
  function buildPrompt(declarationId, declarations) {
4371
4526
  let targets;
4372
4527
  if (declarationId) {
@@ -4377,10 +4532,10 @@ var require_derivation_runner = __commonJS({
4377
4532
  );
4378
4533
  }
4379
4534
  const formatted = targets.map((d) => `- ${d.id}: ${d.statement}`).join("\n");
4380
- return 'You are deriving milestones for a Declare project. Given these declarations, propose 2-4 milestones per declaration by asking "For this to be true, what must be true?" Output ONLY a JSON array with no markdown fencing: [{"title": "milestone title", "realizes": "D-XX", "reason": "why this must be true"}]. Declarations:\n\n' + formatted;
4535
+ return 'You are deriving milestones for a Declare project. Given these declarations, propose 2-4 milestones per declaration by asking "For this to be true, what must be true?" Each milestone needs a concise title AND a detailed description explaining what it delivers, its scope, and success criteria. Output ONLY a JSON array with no markdown fencing: [{"title": "short milestone title", "description": "Detailed description of what this milestone delivers, its scope and boundaries, and how to verify it is complete.", "realizes": "D-XX", "reason": "why this must be true"}]. Declarations:\n\n' + formatted;
4381
4536
  }
4382
- function createDerivationRunner(sseClients, cwd) {
4383
- let current = null;
4537
+ function createDerivationRunner(sseClients, cwd, registry) {
4538
+ const sessions = /* @__PURE__ */ new Map();
4384
4539
  function broadcast(event, data) {
4385
4540
  const payload = `event: ${event}
4386
4541
  data: ${JSON.stringify(data)}
@@ -4394,88 +4549,117 @@ data: ${JSON.stringify(data)}
4394
4549
  }
4395
4550
  }
4396
4551
  }
4397
- function createLineHandler(sessionId, streamName, accumulator) {
4398
- let buffer = "";
4399
- return (chunk) => {
4400
- const text = chunk.toString();
4401
- if (streamName === "stdout") {
4402
- accumulator.text += text;
4403
- }
4404
- buffer += text;
4405
- const lines = buffer.split("\n");
4406
- buffer = lines.pop() || "";
4407
- for (const line of lines) {
4552
+ function derive(declarationId, declarations) {
4553
+ const sessionId = `deriv-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
4554
+ const prompt = buildPrompt(declarationId, declarations);
4555
+ const abortController = new AbortController();
4556
+ let agentId;
4557
+ if (registry) {
4558
+ const agent = registry.spawn("derivation", declarationId || "all", "");
4559
+ agentId = agent.id;
4560
+ }
4561
+ sessions.set(sessionId, { abortController, agentId, declarationId, startTime: Date.now() });
4562
+ broadcast("derivation-output", {
4563
+ sessionId,
4564
+ declarationId,
4565
+ text: "Spawning AI agent\u2026",
4566
+ stream: "status"
4567
+ });
4568
+ runAI(prompt, {
4569
+ cwd,
4570
+ model: "sonnet",
4571
+ maxTurns: 1,
4572
+ abortController,
4573
+ onText: (chunk) => {
4408
4574
  broadcast("derivation-output", {
4409
4575
  sessionId,
4410
- text: line,
4411
- stream: streamName
4576
+ declarationId,
4577
+ text: chunk,
4578
+ stream: "stdout"
4412
4579
  });
4413
4580
  }
4414
- };
4415
- }
4416
- function derive(declarationId, declarations) {
4417
- if (current) {
4418
- return { error: "busy", status: 409 };
4419
- }
4420
- const sessionId = `deriv-${Date.now()}`;
4421
- const prompt = buildPrompt(declarationId, declarations);
4422
- const env = { ...process.env, FORCE_COLOR: "0" };
4423
- delete env.CLAUDECODE;
4424
- const proc = spawn("claude", ["-p", prompt, "--output-format", "text"], {
4425
- cwd,
4426
- env
4427
- });
4428
- current = { sessionId, proc };
4429
- const stdout = { text: "" };
4430
- if (proc.stdout) {
4431
- proc.stdout.on("data", createLineHandler(sessionId, "stdout", stdout));
4432
- }
4433
- if (proc.stderr) {
4434
- proc.stderr.on("data", createLineHandler(sessionId, "stderr", stdout));
4435
- }
4436
- proc.on("close", (exitCode) => {
4581
+ }).then(({ text, error }) => {
4582
+ const session = sessions.get(sessionId);
4583
+ const closingAgentId = session ? session.agentId : void 0;
4584
+ sessions.delete(sessionId);
4585
+ if (error) {
4586
+ broadcast("derivation-complete", {
4587
+ sessionId,
4588
+ declarationId,
4589
+ exitCode: 1,
4590
+ milestones: null,
4591
+ error
4592
+ });
4593
+ if (registry && closingAgentId) {
4594
+ registry.fail(closingAgentId, 1, error);
4595
+ }
4596
+ return;
4597
+ }
4437
4598
  let milestones = null;
4438
- if (exitCode === 0) {
4439
- try {
4440
- milestones = JSON.parse(stdout.text.trim());
4441
- } catch (_) {
4599
+ try {
4600
+ milestones = JSON.parse(text.trim());
4601
+ } catch (_) {
4602
+ const match = text.match(/\[[\s\S]*\]/);
4603
+ if (match) {
4604
+ try {
4605
+ milestones = JSON.parse(match[0]);
4606
+ } catch (__) {
4607
+ }
4442
4608
  }
4443
4609
  }
4444
- current = null;
4445
4610
  broadcast("derivation-complete", {
4446
4611
  sessionId,
4447
- exitCode: exitCode ?? -1,
4612
+ declarationId,
4613
+ exitCode: 0,
4448
4614
  milestones
4449
4615
  });
4450
- });
4451
- proc.on("error", (_err) => {
4452
- current = null;
4616
+ if (registry && closingAgentId) {
4617
+ const milestoneIds = Array.isArray(milestones) ? milestones.map((m) => m.id || m.title || "unknown").filter(Boolean) : [];
4618
+ registry.complete(closingAgentId, { milestones: milestoneIds });
4619
+ }
4620
+ }).catch((err) => {
4621
+ const session = sessions.get(sessionId);
4622
+ const errorAgentId = session ? session.agentId : void 0;
4623
+ sessions.delete(sessionId);
4453
4624
  broadcast("derivation-complete", {
4454
4625
  sessionId,
4626
+ declarationId,
4455
4627
  exitCode: -1,
4456
- milestones: null
4628
+ milestones: null,
4629
+ error: String(err.message || err)
4457
4630
  });
4631
+ if (registry && errorAgentId) {
4632
+ registry.fail(errorAgentId, -1, "error");
4633
+ }
4458
4634
  });
4459
4635
  return { ok: true, sessionId };
4460
4636
  }
4461
- function stop() {
4462
- if (!current) {
4463
- return { error: "not_running", status: 404 };
4637
+ function stop(sessionId) {
4638
+ if (sessionId) {
4639
+ const session = sessions.get(sessionId);
4640
+ if (!session) {
4641
+ return { error: "not_running", status: 404 };
4642
+ }
4643
+ session.abortController.abort();
4644
+ return { ok: true };
4645
+ }
4646
+ return stopAll();
4647
+ }
4648
+ function stopAll() {
4649
+ for (const [, session] of sessions) {
4650
+ session.abortController.abort();
4464
4651
  }
4465
- current.proc.kill("SIGTERM");
4466
4652
  return { ok: true };
4467
4653
  }
4468
4654
  function running() {
4469
- return current ? current.sessionId : null;
4655
+ if (sessions.size === 0) return null;
4656
+ return Array.from(sessions.entries()).map(([id, s]) => ({
4657
+ sessionId: id,
4658
+ declarationId: s.declarationId,
4659
+ startTime: s.startTime
4660
+ }));
4470
4661
  }
4471
- return { derive, stop, running };
4472
- }
4473
- if (require.main === module2) {
4474
- const runner = createDerivationRunner(/* @__PURE__ */ new Set(), ".");
4475
- console.log("derive:", typeof runner.derive);
4476
- console.log("stop:", typeof runner.stop);
4477
- console.log("running:", typeof runner.running);
4478
- console.log("OK");
4662
+ return { derive, stop, stopAll, running };
4479
4663
  }
4480
4664
  module2.exports = { createDerivationRunner };
4481
4665
  }
@@ -4485,7 +4669,7 @@ data: ${JSON.stringify(data)}
4485
4669
  var require_action_derivation_runner = __commonJS({
4486
4670
  "src/server/action-derivation-runner.js"(exports2, module2) {
4487
4671
  "use strict";
4488
- var { spawn } = require("node:child_process");
4672
+ var { runAI } = require_ai_runner();
4489
4673
  function buildActionPrompt(milestone, existingActions) {
4490
4674
  let prompt = `You are deriving actions for a Declare project milestone. An action is a concrete piece of work that causes (moves toward) a milestone. Given this milestone, propose 2-5 actions by asking "What work must be done to achieve this?" Output ONLY a JSON array with no markdown fencing: [{"title": "action title", "produces": "what this action delivers", "reason": "why this is needed"}].
4491
4675
 
@@ -4501,8 +4685,8 @@ Milestone:
4501
4685
  }
4502
4686
  return prompt;
4503
4687
  }
4504
- function createActionDerivationRunner(sseClients, cwd) {
4505
- let current = null;
4688
+ function createActionDerivationRunner(sseClients, cwd, registry) {
4689
+ const sessions = /* @__PURE__ */ new Map();
4506
4690
  function broadcast(event, data) {
4507
4691
  const payload = `event: ${event}
4508
4692
  data: ${JSON.stringify(data)}
@@ -4516,86 +4700,107 @@ data: ${JSON.stringify(data)}
4516
4700
  }
4517
4701
  }
4518
4702
  }
4519
- function createLineHandler(sessionId, streamName, accumulator) {
4520
- let buffer = "";
4521
- return (chunk) => {
4522
- const text = chunk.toString();
4523
- if (streamName === "stdout") {
4524
- accumulator.text += text;
4525
- }
4526
- buffer += text;
4527
- const lines = buffer.split("\n");
4528
- buffer = lines.pop() || "";
4529
- for (const line of lines) {
4530
- broadcast("action-derivation-output", {
4531
- sessionId,
4532
- text: line,
4533
- stream: streamName
4534
- });
4535
- }
4536
- };
4537
- }
4538
4703
  function derive(milestone, existingActions) {
4539
- if (current) {
4540
- return { error: "busy", status: 409 };
4541
- }
4542
4704
  const sessionId = `action-deriv-${Date.now()}`;
4543
4705
  const prompt = buildActionPrompt(milestone, existingActions);
4544
- const env = { ...process.env, FORCE_COLOR: "0" };
4545
- delete env.CLAUDECODE;
4546
- const proc = spawn("claude", ["-p", prompt, "--output-format", "text"], {
4706
+ const abortController = new AbortController();
4707
+ let agentId;
4708
+ if (registry) {
4709
+ const agent = registry.spawn("action-derivation", milestone.id, milestone.id);
4710
+ agentId = agent.id;
4711
+ }
4712
+ sessions.set(sessionId, { milestoneId: milestone.id, agentId, abortController, startTime: Date.now() });
4713
+ runAI(prompt, {
4547
4714
  cwd,
4548
- env
4549
- });
4550
- current = { sessionId, milestoneId: milestone.id, proc };
4551
- const stdout = { text: "" };
4552
- if (proc.stdout) {
4553
- proc.stdout.on("data", createLineHandler(sessionId, "stdout", stdout));
4554
- }
4555
- if (proc.stderr) {
4556
- proc.stderr.on("data", createLineHandler(sessionId, "stderr", stdout));
4557
- }
4558
- proc.on("close", (exitCode) => {
4715
+ model: "haiku",
4716
+ maxTurns: 1,
4717
+ abortController,
4718
+ onText: (text) => {
4719
+ broadcast("action-derivation-output", {
4720
+ sessionId,
4721
+ milestoneId: milestone.id,
4722
+ text,
4723
+ stream: "stdout"
4724
+ });
4725
+ }
4726
+ }).then(({ text, error }) => {
4727
+ const session = sessions.get(sessionId);
4728
+ const closingAgentId = session ? session.agentId : void 0;
4729
+ sessions.delete(sessionId);
4559
4730
  let actions = null;
4560
- if (exitCode === 0) {
4731
+ const exitCode = error ? 1 : 0;
4732
+ if (!error && text) {
4561
4733
  try {
4562
- actions = JSON.parse(stdout.text.trim());
4734
+ actions = JSON.parse(text.trim());
4563
4735
  } catch (_) {
4736
+ const jsonMatch = text.match(/\[[\s\S]*\]/);
4737
+ if (jsonMatch) {
4738
+ try {
4739
+ actions = JSON.parse(jsonMatch[0]);
4740
+ } catch (_2) {
4741
+ }
4742
+ }
4564
4743
  }
4565
4744
  }
4566
- current = null;
4567
4745
  broadcast("action-derivation-complete", {
4568
4746
  sessionId,
4569
- exitCode: exitCode ?? -1,
4747
+ milestoneId: milestone.id,
4748
+ exitCode,
4570
4749
  actions
4571
4750
  });
4572
- });
4573
- proc.on("error", (_err) => {
4574
- current = null;
4575
- broadcast("action-derivation-complete", {
4576
- sessionId,
4577
- exitCode: -1,
4578
- actions: null
4579
- });
4751
+ if (registry && closingAgentId) {
4752
+ if (exitCode === 0) {
4753
+ registry.complete(closingAgentId, {
4754
+ milestoneId: milestone.id,
4755
+ actionCount: Array.isArray(actions) ? actions.length : null
4756
+ });
4757
+ } else {
4758
+ registry.fail(closingAgentId, 1, error || "action derivation failed");
4759
+ }
4760
+ }
4761
+ }).catch((err) => {
4762
+ const session = sessions.get(sessionId);
4763
+ const closingAgentId = session ? session.agentId : void 0;
4764
+ sessions.delete(sessionId);
4765
+ broadcast("action-derivation-complete", { sessionId, milestoneId: milestone.id, exitCode: 1, actions: null });
4766
+ if (registry && closingAgentId) {
4767
+ registry.fail(closingAgentId, 1, String(err));
4768
+ }
4580
4769
  });
4581
4770
  return { ok: true, sessionId };
4582
4771
  }
4583
- function stop() {
4584
- if (!current) {
4585
- return { error: "not_running", status: 404 };
4772
+ function stop(sessionId) {
4773
+ if (sessionId) {
4774
+ const session = sessions.get(sessionId);
4775
+ if (!session) {
4776
+ return { error: "not_running", status: 404 };
4777
+ }
4778
+ session.abortController.abort();
4779
+ return { ok: true };
4780
+ }
4781
+ return stopAll();
4782
+ }
4783
+ function stopAll() {
4784
+ for (const [, session] of sessions) {
4785
+ session.abortController.abort();
4586
4786
  }
4587
- current.proc.kill("SIGTERM");
4588
4787
  return { ok: true };
4589
4788
  }
4590
4789
  function running() {
4591
- return current ? current.sessionId : null;
4790
+ if (sessions.size === 0) return null;
4791
+ return Array.from(sessions.entries()).map(([id, s]) => ({
4792
+ sessionId: id,
4793
+ milestoneId: s.milestoneId,
4794
+ startTime: s.startTime
4795
+ }));
4592
4796
  }
4593
- return { derive, stop, running };
4797
+ return { derive, stop, stopAll, running };
4594
4798
  }
4595
4799
  if (require.main === module2) {
4596
4800
  const runner = createActionDerivationRunner(/* @__PURE__ */ new Set(), ".");
4597
4801
  console.log("derive:", typeof runner.derive);
4598
4802
  console.log("stop:", typeof runner.stop);
4803
+ console.log("stopAll:", typeof runner.stopAll);
4599
4804
  console.log("running:", typeof runner.running);
4600
4805
  console.log("OK");
4601
4806
  }
@@ -4607,14 +4812,15 @@ data: ${JSON.stringify(data)}
4607
4812
  var require_revision_runner = __commonJS({
4608
4813
  "src/server/revision-runner.js"(exports2, module2) {
4609
4814
  "use strict";
4610
- var { spawn } = require("node:child_process");
4815
+ var { runAI } = require_ai_runner();
4611
4816
  var fs = require("node:fs");
4612
4817
  var path = require("node:path");
4613
4818
  function buildRevisionPrompt(artifactContent, annotations) {
4614
4819
  const annotationList = annotations.map((a) => `- Line ${a.line}: ${a.text}`).join("\n");
4615
- return "You are revising a plan artifact based on reviewer annotations. Do NOT implement anything \u2014 only update the plan document.\n\n## Current plan content\n\n" + artifactContent + "\n\n## Reviewer annotations to address\n\n" + annotationList + "\n\n## Instructions\n\nRevise the plan above to address ALL the reviewer's annotations. Output ONLY the revised plan content \u2014 no explanations, no markdown fencing, no preamble. The output will directly replace the current file.";
4820
+ return "## Current plan content\n\n" + artifactContent + "\n\n## Reviewer annotations to address\n\n" + annotationList + "\n\n## Instructions\n\nRevise the plan above to address ALL the reviewer's annotations. Output ONLY the revised plan content \u2014 no explanations, no markdown fencing, no preamble. The output will directly replace the current file.";
4616
4821
  }
4617
- function createRevisionRunner(sseClients, cwd, onComplete) {
4822
+ var SYSTEM_PROMPT = "You are revising a plan artifact based on reviewer annotations. Do NOT implement anything \u2014 only update the plan document. Output ONLY the revised content with no markdown fencing or preamble.";
4823
+ function createRevisionRunner(sseClients, cwd, onComplete, registry) {
4618
4824
  let current = null;
4619
4825
  function broadcast(event, data) {
4620
4826
  const payload = `event: ${event}
@@ -4629,26 +4835,6 @@ data: ${JSON.stringify(data)}
4629
4835
  }
4630
4836
  }
4631
4837
  }
4632
- function createLineHandler(sessionId, nodeId, streamName, accumulator) {
4633
- let buffer = "";
4634
- return (chunk) => {
4635
- const text = chunk.toString();
4636
- if (streamName === "stdout") {
4637
- accumulator.text += text;
4638
- }
4639
- buffer += text;
4640
- const lines = buffer.split("\n");
4641
- buffer = lines.pop() || "";
4642
- for (const line of lines) {
4643
- broadcast("revision-output", {
4644
- sessionId,
4645
- nodeId,
4646
- text: line,
4647
- stream: streamName
4648
- });
4649
- }
4650
- };
4651
- }
4652
4838
  function stripMarkdownFencing(text) {
4653
4839
  const trimmed = text.trim();
4654
4840
  const fenceMatch = trimmed.match(/^```(?:markdown)?\s*\n([\s\S]*?)\n```\s*$/);
@@ -4677,70 +4863,90 @@ data: ${JSON.stringify(data)}
4677
4863
  fs.copyFileSync(artifactPath, versionedPath);
4678
4864
  } catch (_) {
4679
4865
  }
4680
- const spawnEnv = { ...process.env, FORCE_COLOR: "0" };
4681
- delete spawnEnv.CLAUDECODE;
4682
- const proc = spawn("claude", ["-p", prompt, "--output-format", "text"], {
4683
- cwd,
4684
- env: spawnEnv
4685
- });
4686
- current = { sessionId, proc, nodeId };
4687
- const stdout = { text: "" };
4688
- if (proc.stdout) {
4689
- proc.stdout.on("data", createLineHandler(sessionId, nodeId, "stdout", stdout));
4690
- }
4691
- if (proc.stderr) {
4692
- proc.stderr.on("data", createLineHandler(sessionId, nodeId, "stderr", stdout));
4866
+ const abortController = new AbortController();
4867
+ let agentId;
4868
+ if (registry) {
4869
+ const agent = registry.spawn("revision", nodeId, "");
4870
+ agentId = agent.id;
4693
4871
  }
4694
- proc.on("close", (exitCode) => {
4872
+ current = { sessionId, abortController, nodeId, agentId };
4873
+ runAI(prompt, {
4874
+ cwd,
4875
+ model: "sonnet",
4876
+ systemPrompt: SYSTEM_PROMPT,
4877
+ abortController,
4878
+ onText: (chunk) => {
4879
+ broadcast("revision-output", {
4880
+ sessionId,
4881
+ nodeId,
4882
+ text: chunk,
4883
+ stream: "stdout"
4884
+ });
4885
+ }
4886
+ }).then(({ text, error }) => {
4695
4887
  const completedNodeId = current ? current.nodeId : nodeId;
4888
+ const closingAgentId = current ? current.agentId : void 0;
4696
4889
  current = null;
4697
- if (exitCode === 0) {
4698
- try {
4699
- const revisedContent = stripMarkdownFencing(stdout.text);
4700
- fs.writeFileSync(artifactPath, revisedContent, "utf-8");
4701
- const annPath = path.join(cwd, ".planning", "annotations", completedNodeId.toUpperCase() + ".json");
4702
- let annData = { nodeId: completedNodeId.toUpperCase(), annotations: [], revisionRound: 0 };
4703
- if (fs.existsSync(annPath)) {
4704
- try {
4705
- annData = JSON.parse(fs.readFileSync(annPath, "utf-8"));
4706
- } catch (_) {
4707
- }
4890
+ if (error) {
4891
+ broadcast("revision-complete", {
4892
+ sessionId,
4893
+ nodeId: completedNodeId,
4894
+ exitCode: 1,
4895
+ error: true
4896
+ });
4897
+ if (registry && closingAgentId) {
4898
+ registry.fail(closingAgentId, 1, error);
4899
+ }
4900
+ return;
4901
+ }
4902
+ try {
4903
+ const revisedContent = stripMarkdownFencing(text);
4904
+ fs.writeFileSync(artifactPath, revisedContent, "utf-8");
4905
+ const annPath = path.join(cwd, ".planning", "annotations", completedNodeId.toUpperCase() + ".json");
4906
+ let annData = { nodeId: completedNodeId.toUpperCase(), annotations: [], revisionRound: 0 };
4907
+ if (fs.existsSync(annPath)) {
4908
+ try {
4909
+ annData = JSON.parse(fs.readFileSync(annPath, "utf-8"));
4910
+ } catch (_) {
4708
4911
  }
4709
- const newRound = (annData.revisionRound || 0) + 1;
4710
- annData.revisionRound = newRound;
4711
- const annDir = path.dirname(annPath);
4712
- fs.mkdirSync(annDir, { recursive: true });
4713
- fs.writeFileSync(annPath, JSON.stringify(annData, null, 2), "utf-8");
4714
- broadcast("revision-complete", {
4715
- sessionId,
4716
- nodeId: completedNodeId,
4717
- exitCode,
4718
- revisionRound: newRound
4719
- });
4720
- if (onComplete) {
4721
- try {
4722
- onComplete(completedNodeId);
4723
- } catch (_) {
4724
- }
4912
+ }
4913
+ const newRound = (annData.revisionRound || 0) + 1;
4914
+ annData.revisionRound = newRound;
4915
+ const annDir = path.dirname(annPath);
4916
+ fs.mkdirSync(annDir, { recursive: true });
4917
+ fs.writeFileSync(annPath, JSON.stringify(annData, null, 2), "utf-8");
4918
+ broadcast("revision-complete", {
4919
+ sessionId,
4920
+ nodeId: completedNodeId,
4921
+ exitCode: 0,
4922
+ revisionRound: newRound
4923
+ });
4924
+ if (onComplete) {
4925
+ try {
4926
+ onComplete(completedNodeId);
4927
+ } catch (_) {
4725
4928
  }
4726
- } catch (err) {
4727
- broadcast("revision-complete", {
4728
- sessionId,
4929
+ }
4930
+ if (registry && closingAgentId) {
4931
+ registry.complete(closingAgentId, {
4729
4932
  nodeId: completedNodeId,
4730
- exitCode: -1,
4731
- error: true
4933
+ planPath: artifactPath,
4934
+ revisionRound: newRound
4732
4935
  });
4733
4936
  }
4734
- } else {
4937
+ } catch (err) {
4735
4938
  broadcast("revision-complete", {
4736
4939
  sessionId,
4737
4940
  nodeId: completedNodeId,
4738
- exitCode: exitCode ?? -1,
4941
+ exitCode: -1,
4739
4942
  error: true
4740
4943
  });
4944
+ if (registry && closingAgentId) {
4945
+ registry.fail(closingAgentId, -1, "revision post-processing error");
4946
+ }
4741
4947
  }
4742
- });
4743
- proc.on("error", (_err) => {
4948
+ }).catch((err) => {
4949
+ const errorAgentId = current ? current.agentId : void 0;
4744
4950
  current = null;
4745
4951
  broadcast("revision-complete", {
4746
4952
  sessionId,
@@ -4748,6 +4954,9 @@ data: ${JSON.stringify(data)}
4748
4954
  exitCode: -1,
4749
4955
  error: true
4750
4956
  });
4957
+ if (registry && errorAgentId) {
4958
+ registry.fail(errorAgentId, -1, "error");
4959
+ }
4751
4960
  });
4752
4961
  return { ok: true, sessionId };
4753
4962
  }
@@ -4755,7 +4964,7 @@ data: ${JSON.stringify(data)}
4755
4964
  if (!current) {
4756
4965
  return { error: "not_running", status: 404 };
4757
4966
  }
4758
- current.proc.kill("SIGTERM");
4967
+ current.abortController.abort();
4759
4968
  return { ok: true };
4760
4969
  }
4761
4970
  function running() {
@@ -5204,57 +5413,252 @@ data: ${JSON.stringify(data)}
5204
5413
  }
5205
5414
  });
5206
5415
 
5207
- // src/server/pipeline-runner.js
5208
- var require_pipeline_runner = __commonJS({
5209
- "src/server/pipeline-runner.js"(exports2, module2) {
5416
+ // src/commands/lifecycle-stages.js
5417
+ var require_lifecycle_stages = __commonJS({
5418
+ "src/commands/lifecycle-stages.js"(exports2, module2) {
5210
5419
  "use strict";
5211
- var { spawn, execSync } = require("node:child_process");
5212
- var fs = require("node:fs");
5213
- var path = require("node:path");
5214
- var { findMilestoneFolder } = require_milestone_folders();
5215
- var STATE_FILE = ".planning/pipeline-state.json";
5216
- var OUTPUT_BUFFER_MAX = 5e4;
5217
- function appendLog(logPath, line) {
5218
- if (!logPath) return;
5219
- try {
5220
- fs.appendFileSync(logPath, line + "\n", "utf-8");
5221
- } catch (_) {
5420
+ var COMPLETED_STATUSES = /* @__PURE__ */ new Set(["DONE", "KEPT", "HONORED"]);
5421
+ var EXECUTING_STATUSES = /* @__PURE__ */ new Set(["EXECUTING", "IN_PROGRESS", "RUNNING"]);
5422
+ function computeLifecycleStages(graph, runningActionIds) {
5423
+ const { declarations = [], milestones = [], actions = [] } = graph;
5424
+ const running = runningActionIds || /* @__PURE__ */ new Set();
5425
+ const stages = {
5426
+ "needs-planning": [],
5427
+ "needs-approval": [],
5428
+ "ready-to-execute": [],
5429
+ "in-execution": [],
5430
+ "done": []
5431
+ };
5432
+ const milestoneById = new Map(milestones.map((m) => [m.id, m]));
5433
+ const actionsByMilestone = /* @__PURE__ */ new Map();
5434
+ for (const a of actions) {
5435
+ for (const mId of a.causes || []) {
5436
+ if (!actionsByMilestone.has(mId)) actionsByMilestone.set(mId, []);
5437
+ actionsByMilestone.get(mId).push(a);
5438
+ }
5222
5439
  }
5223
- }
5224
- function isTransientFailure(exitCode, stderrOutput) {
5225
- if (exitCode === 124 || exitCode === 137 || exitCode === -1) return true;
5226
- const patterns = /ETIMEDOUT|ECONNRESET|ECONNREFUSED|ENOMEM|SIGKILL|SIGTERM|socket hang up|network timeout/i;
5227
- return patterns.test(stderrOutput);
5228
- }
5229
- function generateExecutionReport(cwd, results, pipelineStartTime, pipelineStopped, startSha) {
5230
- try {
5231
- let endSha = "unknown";
5232
- try {
5233
- endSha = execSync("git rev-parse --short HEAD", { cwd }).toString().trim();
5234
- } catch (_) {
5440
+ const milestonesByDecl = /* @__PURE__ */ new Map();
5441
+ for (const d of declarations) {
5442
+ milestonesByDecl.set(d.id, (d.milestones || []).filter((mId) => milestoneById.has(mId)));
5443
+ }
5444
+ for (const d of declarations) {
5445
+ const status = (d.status || "").toUpperCase();
5446
+ if (COMPLETED_STATUSES.has(status)) {
5447
+ stages["done"].push(makeItem(d, "declaration", "done"));
5448
+ continue;
5235
5449
  }
5236
- const endTime = Date.now();
5237
- const totalMs = endTime - pipelineStartTime;
5238
- const totalMin = Math.floor(totalMs / 6e4);
5239
- const totalSec = Math.floor(totalMs % 6e4 / 1e3);
5240
- const passed = results.filter((r) => r.exitCode === 0).length;
5241
- const failed = results.filter((r) => r.exitCode !== 0).length;
5242
- const overallStatus = pipelineStopped ? "STOPPED" : failed > 0 ? "FAILED" : "SUCCESS";
5243
- const startedAt = new Date(pipelineStartTime).toISOString();
5244
- const completedAt = new Date(endTime).toISOString();
5245
- let rows = "";
5246
- for (let i = 0; i < results.length; i++) {
5247
- const r = results[i];
5248
- const status = r.exitCode === 0 ? "PASS" : "FAIL";
5249
- const durMin = Math.floor(r.durationMs / 6e4);
5250
- const durSec = Math.floor(r.durationMs % 6e4 / 1e3);
5251
- const retried = r.retried ? `Yes (${r.attempts})` : "No";
5252
- rows += `| ${i + 1} | ${r.actionId} | ${r.milestoneId} | ${status} | ${durMin}m ${durSec}s | ${retried} |
5253
- `;
5450
+ const myMilestones = milestonesByDecl.get(d.id) || [];
5451
+ if (myMilestones.length === 0) {
5452
+ stages["needs-planning"].push(makeItem(d, "declaration", "needs-planning"));
5453
+ continue;
5254
5454
  }
5255
- const report = `# Execution Report
5256
-
5257
- **Status:** ${overallStatus}
5455
+ if (d.reviewState !== "approved") {
5456
+ stages["needs-approval"].push(makeItem(d, "declaration", "needs-approval"));
5457
+ continue;
5458
+ }
5459
+ const allMilestonesDone = myMilestones.every((mId) => {
5460
+ const m = milestoneById.get(mId);
5461
+ return m && COMPLETED_STATUSES.has((m.status || "").toUpperCase());
5462
+ });
5463
+ if (allMilestonesDone) {
5464
+ stages["done"].push(makeItem(d, "declaration", "done"));
5465
+ } else {
5466
+ }
5467
+ }
5468
+ for (const m of milestones) {
5469
+ const status = (m.status || "").toUpperCase();
5470
+ if (COMPLETED_STATUSES.has(status)) {
5471
+ stages["done"].push(makeItem(m, "milestone", "done"));
5472
+ continue;
5473
+ }
5474
+ const myActions = actionsByMilestone.get(m.id) || [];
5475
+ const hasPlan = m.hasPlan || myActions.length > 0;
5476
+ if (!hasPlan) {
5477
+ stages["needs-planning"].push(makeItem(m, "milestone", "needs-planning"));
5478
+ continue;
5479
+ }
5480
+ if (m.reviewState !== "approved") {
5481
+ stages["needs-approval"].push(makeItem(m, "milestone", "needs-approval"));
5482
+ continue;
5483
+ }
5484
+ const hasExecuting = myActions.some(
5485
+ (a) => EXECUTING_STATUSES.has((a.status || "").toUpperCase()) || running.has(a.id)
5486
+ );
5487
+ if (hasExecuting) {
5488
+ stages["in-execution"].push(makeItem(m, "milestone", "in-execution"));
5489
+ continue;
5490
+ }
5491
+ const allActionsDone = myActions.length > 0 && myActions.every(
5492
+ (a) => COMPLETED_STATUSES.has((a.status || "").toUpperCase())
5493
+ );
5494
+ if (allActionsDone) {
5495
+ stages["done"].push(makeItem(m, "milestone", "done"));
5496
+ continue;
5497
+ }
5498
+ const deps = m.dependsOn || [];
5499
+ const depsBlocked = deps.some((depId) => {
5500
+ const dep = milestoneById.get(depId);
5501
+ return !dep || !COMPLETED_STATUSES.has((dep.status || "").toUpperCase());
5502
+ });
5503
+ if (depsBlocked) {
5504
+ stages["needs-approval"].push(makeItem(m, "milestone", "needs-approval"));
5505
+ } else {
5506
+ stages["ready-to-execute"].push(makeItem(m, "milestone", "ready-to-execute"));
5507
+ }
5508
+ }
5509
+ for (const a of actions) {
5510
+ const status = (a.status || "").toUpperCase();
5511
+ if (COMPLETED_STATUSES.has(status)) {
5512
+ stages["done"].push(makeItem(a, "action", "done"));
5513
+ continue;
5514
+ }
5515
+ if (EXECUTING_STATUSES.has(status) || running.has(a.id)) {
5516
+ stages["in-execution"].push(makeItem(a, "action", "in-execution"));
5517
+ continue;
5518
+ }
5519
+ if (a.reviewState !== "approved") {
5520
+ stages["needs-approval"].push(makeItem(a, "action", "needs-approval"));
5521
+ continue;
5522
+ }
5523
+ const parentMilestones = (a.causes || []).map((mId) => milestoneById.get(mId)).filter(Boolean);
5524
+ const parentBlocked = parentMilestones.some((m) => {
5525
+ const deps = m.dependsOn || [];
5526
+ return deps.some((depId) => {
5527
+ const dep = milestoneById.get(depId);
5528
+ return !dep || !COMPLETED_STATUSES.has((dep.status || "").toUpperCase());
5529
+ });
5530
+ });
5531
+ if (parentBlocked) {
5532
+ stages["needs-approval"].push(makeItem(a, "action", "needs-approval"));
5533
+ } else {
5534
+ stages["ready-to-execute"].push(makeItem(a, "action", "ready-to-execute"));
5535
+ }
5536
+ }
5537
+ const nextAction = computeNextAction(stages, declarations, milestones, actions);
5538
+ const total = declarations.length + milestones.length + actions.length;
5539
+ const done = stages["done"].length;
5540
+ const percentage = total > 0 ? Math.round(done / total * 100) : 0;
5541
+ return {
5542
+ stages,
5543
+ nextAction,
5544
+ progress: { total, done, percentage }
5545
+ };
5546
+ }
5547
+ function computeNextAction(stages, declarations, milestones, actions) {
5548
+ const planningDecl = stages["needs-planning"].find((item) => item.type === "declaration");
5549
+ if (planningDecl) {
5550
+ return {
5551
+ action: "derive-milestones",
5552
+ label: `Plan milestones for ${planningDecl.id}`,
5553
+ targetId: planningDecl.id,
5554
+ targetType: "declaration"
5555
+ };
5556
+ }
5557
+ const planningMile = stages["needs-planning"].find((item) => item.type === "milestone");
5558
+ if (planningMile) {
5559
+ return {
5560
+ action: "derive-actions",
5561
+ label: `Plan actions for ${planningMile.id}`,
5562
+ targetId: planningMile.id,
5563
+ targetType: "milestone"
5564
+ };
5565
+ }
5566
+ if (stages["needs-approval"].length > 0) {
5567
+ const first = stages["needs-approval"][0];
5568
+ return {
5569
+ action: "approve",
5570
+ label: `Review ${first.id}`,
5571
+ targetId: first.id,
5572
+ targetType: first.type
5573
+ };
5574
+ }
5575
+ if (stages["ready-to-execute"].length > 0) {
5576
+ return {
5577
+ action: "execute",
5578
+ label: "Execute approved actions"
5579
+ };
5580
+ }
5581
+ if (stages["in-execution"].length > 0) {
5582
+ return {
5583
+ action: "view-execution",
5584
+ label: "Execution in progress"
5585
+ };
5586
+ }
5587
+ const total = declarations.length + milestones.length + actions.length;
5588
+ if (total > 0) {
5589
+ return {
5590
+ action: "complete",
5591
+ label: "All items complete"
5592
+ };
5593
+ }
5594
+ return null;
5595
+ }
5596
+ function makeItem(node, type, stage) {
5597
+ return {
5598
+ id: node.id,
5599
+ title: node.title || node.statement || node.id,
5600
+ type,
5601
+ status: node.status || "PENDING",
5602
+ reviewState: node.reviewState,
5603
+ stage
5604
+ };
5605
+ }
5606
+ module2.exports = { computeLifecycleStages, COMPLETED_STATUSES, EXECUTING_STATUSES };
5607
+ }
5608
+ });
5609
+
5610
+ // src/server/pipeline-runner.js
5611
+ var require_pipeline_runner = __commonJS({
5612
+ "src/server/pipeline-runner.js"(exports2, module2) {
5613
+ "use strict";
5614
+ var { runAI } = require_ai_runner();
5615
+ var { execSync } = require("node:child_process");
5616
+ var fs = require("node:fs");
5617
+ var path = require("node:path");
5618
+ var { findMilestoneFolder } = require_milestone_folders();
5619
+ var STATE_FILE = ".planning/pipeline-state.json";
5620
+ var OUTPUT_BUFFER_MAX = 5e4;
5621
+ function appendLog(logPath, line) {
5622
+ if (!logPath) return;
5623
+ try {
5624
+ fs.appendFileSync(logPath, line + "\n", "utf-8");
5625
+ } catch (_) {
5626
+ }
5627
+ }
5628
+ function isTransientFailure(exitCode, errorOutput) {
5629
+ if (exitCode === 124 || exitCode === 137 || exitCode === -1) return true;
5630
+ const patterns = /ETIMEDOUT|ECONNRESET|ECONNREFUSED|ENOMEM|SIGKILL|SIGTERM|socket hang up|network timeout|Cancelled/i;
5631
+ return patterns.test(errorOutput);
5632
+ }
5633
+ function generateExecutionReport(cwd, results, pipelineStartTime, pipelineStopped, startSha) {
5634
+ try {
5635
+ let endSha = "unknown";
5636
+ try {
5637
+ endSha = execSync("git rev-parse --short HEAD", { cwd }).toString().trim();
5638
+ } catch (_) {
5639
+ }
5640
+ const endTime = Date.now();
5641
+ const totalMs = endTime - pipelineStartTime;
5642
+ const totalMin = Math.floor(totalMs / 6e4);
5643
+ const totalSec = Math.floor(totalMs % 6e4 / 1e3);
5644
+ const passed = results.filter((r) => r.exitCode === 0).length;
5645
+ const failed = results.filter((r) => r.exitCode !== 0).length;
5646
+ const overallStatus = pipelineStopped ? "STOPPED" : failed > 0 ? "FAILED" : "SUCCESS";
5647
+ const startedAt = new Date(pipelineStartTime).toISOString();
5648
+ const completedAt = new Date(endTime).toISOString();
5649
+ let rows = "";
5650
+ for (let i = 0; i < results.length; i++) {
5651
+ const r = results[i];
5652
+ const status = r.exitCode === 0 ? "PASS" : "FAIL";
5653
+ const durMin = Math.floor(r.durationMs / 6e4);
5654
+ const durSec = Math.floor(r.durationMs % 6e4 / 1e3);
5655
+ const retried = r.retried ? `Yes (${r.attempts})` : "No";
5656
+ rows += `| ${i + 1} | ${r.actionId} | ${r.milestoneId} | ${status} | ${durMin}m ${durSec}s | ${retried} |
5657
+ `;
5658
+ }
5659
+ const report = `# Execution Report
5660
+
5661
+ **Status:** ${overallStatus}
5258
5662
  **Started:** ${startedAt}
5259
5663
  **Completed:** ${completedAt}
5260
5664
  **Duration:** ${totalMin}m ${totalSec}s
@@ -5278,16 +5682,18 @@ ${rows}
5278
5682
  return null;
5279
5683
  }
5280
5684
  }
5281
- function createPipelineRunner(sseClients, cwd) {
5685
+ function createPipelineRunner(sseClients, cwd, registry) {
5282
5686
  let isRunning = false;
5283
5687
  let stopRequested = false;
5284
- const activeProcesses = /* @__PURE__ */ new Map();
5688
+ const activeControllers = /* @__PURE__ */ new Map();
5285
5689
  let results = [];
5286
5690
  let pipelineState = null;
5287
5691
  let pausedOnFailure = null;
5288
5692
  let skipResolve = null;
5289
5693
  const outputBuffers = {};
5290
5694
  let totalActionCount = 0;
5695
+ let pipelineAgentId;
5696
+ const actionAgentIds = /* @__PURE__ */ new Map();
5291
5697
  function persistState() {
5292
5698
  if (!pipelineState) {
5293
5699
  try {
@@ -5304,7 +5710,7 @@ ${rows}
5304
5710
  completedActions: pipelineState.completedActions,
5305
5711
  failedActions: pipelineState.failedActions,
5306
5712
  stoppedActions: pipelineState.stoppedActions,
5307
- activeActions: [...activeProcesses.keys()],
5713
+ activeActions: [...activeControllers.keys()],
5308
5714
  outputBuffers,
5309
5715
  pausedOnFailure,
5310
5716
  timestamp: Date.now()
@@ -5359,78 +5765,75 @@ data: ${JSON.stringify(data)}
5359
5765
  }
5360
5766
  }
5361
5767
  function executeAction(actionId, milestoneId) {
5362
- return new Promise((resolve) => {
5363
- const startedAt = (/* @__PURE__ */ new Date()).toISOString();
5364
- const startTime = Date.now();
5365
- const prompt = `Run /declare:execute ${milestoneId} for action ${actionId} only. Do not ask questions, execute autonomously.`;
5366
- const spawnEnv = { ...process.env, FORCE_COLOR: "0" };
5367
- delete spawnEnv.CLAUDECODE;
5368
- const proc = spawn("claude", ["-p", prompt], {
5369
- cwd,
5370
- env: spawnEnv
5371
- });
5372
- activeProcesses.set(actionId, proc);
5373
- const planningDir = path.join(cwd, ".planning");
5374
- const milestoneFolder = findMilestoneFolder(planningDir, milestoneId);
5375
- const logPath = milestoneFolder ? path.join(milestoneFolder, "execution.log") : void 0;
5376
- appendLog(logPath, `
5768
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
5769
+ const startTime = Date.now();
5770
+ if (registry) {
5771
+ const agent = registry.spawn("execution", actionId, milestoneId);
5772
+ actionAgentIds.set(actionId, agent.id);
5773
+ }
5774
+ const prompt = `Run /declare:execute ${milestoneId} for action ${actionId} only. Do not ask questions, execute autonomously.`;
5775
+ const abortController = new AbortController();
5776
+ activeControllers.set(actionId, abortController);
5777
+ const planningDir = path.join(cwd, ".planning");
5778
+ const milestoneFolder = findMilestoneFolder(planningDir, milestoneId);
5779
+ const logPath = milestoneFolder ? path.join(milestoneFolder, "execution.log") : void 0;
5780
+ appendLog(logPath, `
5377
5781
  === START ${actionId} (pipeline) @ ${startedAt} ===`);
5378
- let stdoutBuf = "";
5379
- let stderrBuf = "";
5380
- let stderrFull = "";
5381
- if (proc.stdout) {
5382
- proc.stdout.on("data", (chunk) => {
5383
- stdoutBuf += chunk.toString();
5384
- const lines = stdoutBuf.split("\n");
5385
- stdoutBuf = lines.pop() || "";
5386
- for (const line of lines) {
5387
- broadcast("action-output", { actionId, text: line, stream: "stdout" });
5388
- appendLog(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] [${actionId}] [stdout] ${line}`);
5389
- if (!outputBuffers[actionId]) outputBuffers[actionId] = "";
5390
- outputBuffers[actionId] += line + "\n";
5391
- if (outputBuffers[actionId].length > OUTPUT_BUFFER_MAX) {
5392
- outputBuffers[actionId] = outputBuffers[actionId].slice(-OUTPUT_BUFFER_MAX);
5393
- }
5394
- }
5395
- });
5782
+ return runAI(prompt, {
5783
+ cwd,
5784
+ model: "sonnet",
5785
+ withTools: true,
5786
+ maxTurns: 10,
5787
+ abortController,
5788
+ onText: (chunk) => {
5789
+ broadcast("action-output", { actionId, text: chunk, stream: "stdout" });
5790
+ appendLog(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] [${actionId}] [stdout] ${chunk}`);
5791
+ if (!outputBuffers[actionId]) outputBuffers[actionId] = "";
5792
+ outputBuffers[actionId] += chunk;
5793
+ if (outputBuffers[actionId].length > OUTPUT_BUFFER_MAX) {
5794
+ outputBuffers[actionId] = outputBuffers[actionId].slice(-OUTPUT_BUFFER_MAX);
5795
+ }
5396
5796
  }
5397
- if (proc.stderr) {
5398
- proc.stderr.on("data", (chunk) => {
5399
- const text = chunk.toString();
5400
- stderrFull += text;
5401
- stderrBuf += text;
5402
- const lines = stderrBuf.split("\n");
5403
- stderrBuf = lines.pop() || "";
5404
- for (const line of lines) {
5405
- broadcast("action-output", { actionId, text: line, stream: "stderr" });
5406
- appendLog(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] [${actionId}] [stderr] ${line}`);
5407
- if (!outputBuffers[actionId]) outputBuffers[actionId] = "";
5408
- outputBuffers[actionId] += line + "\n";
5409
- if (outputBuffers[actionId].length > OUTPUT_BUFFER_MAX) {
5410
- outputBuffers[actionId] = outputBuffers[actionId].slice(-OUTPUT_BUFFER_MAX);
5411
- }
5797
+ }).then(({ text, error }) => {
5798
+ const exitCode = error ? 1 : 0;
5799
+ const completedAt = (/* @__PURE__ */ new Date()).toISOString();
5800
+ const durationMs = Date.now() - startTime;
5801
+ appendLog(logPath, `=== END ${actionId} (pipeline) @ ${completedAt} exit=${exitCode} duration=${durationMs}ms ===
5802
+ `);
5803
+ activeControllers.delete(actionId);
5804
+ broadcast("action-complete", { actionId, exitCode, durationMs });
5805
+ if (registry) {
5806
+ const aId = actionAgentIds.get(actionId);
5807
+ if (aId) {
5808
+ if (exitCode === 0) {
5809
+ registry.complete(aId, {
5810
+ actionId,
5811
+ milestoneId,
5812
+ logPath: logPath || null,
5813
+ durationMs
5814
+ });
5815
+ } else {
5816
+ registry.fail(aId, exitCode, error || "execution failed");
5412
5817
  }
5413
- });
5818
+ actionAgentIds.delete(actionId);
5819
+ }
5414
5820
  }
5415
- proc.on("close", (exitCode) => {
5416
- const code = exitCode ?? -1;
5417
- const completedAt = (/* @__PURE__ */ new Date()).toISOString();
5418
- const durationMs = Date.now() - startTime;
5419
- appendLog(logPath, `=== END ${actionId} (pipeline) @ ${completedAt} exit=${code} duration=${durationMs}ms ===
5420
- `);
5421
- activeProcesses.delete(actionId);
5422
- broadcast("action-complete", { actionId, exitCode: code, durationMs });
5423
- resolve({ actionId, milestoneId, exitCode: code, stderrOutput: stderrFull, durationMs, startedAt, completedAt, retried: false, attempts: 1 });
5424
- });
5425
- proc.on("error", (_err) => {
5426
- const completedAt = (/* @__PURE__ */ new Date()).toISOString();
5427
- const durationMs = Date.now() - startTime;
5428
- appendLog(logPath, `=== ERROR ${actionId} (pipeline) @ ${completedAt} ===
5821
+ return { actionId, milestoneId, exitCode, errorOutput: error || "", durationMs, startedAt, completedAt, retried: false, attempts: 1 };
5822
+ }).catch((err) => {
5823
+ const completedAt = (/* @__PURE__ */ new Date()).toISOString();
5824
+ const durationMs = Date.now() - startTime;
5825
+ appendLog(logPath, `=== ERROR ${actionId} (pipeline) @ ${completedAt} ===
5429
5826
  `);
5430
- activeProcesses.delete(actionId);
5431
- broadcast("action-complete", { actionId, exitCode: -1, durationMs });
5432
- resolve({ actionId, milestoneId, exitCode: -1, stderrOutput: stderrFull, durationMs, startedAt, completedAt, retried: false, attempts: 1 });
5433
- });
5827
+ activeControllers.delete(actionId);
5828
+ broadcast("action-complete", { actionId, exitCode: -1, durationMs });
5829
+ if (registry) {
5830
+ const aId = actionAgentIds.get(actionId);
5831
+ if (aId) {
5832
+ registry.fail(aId, -1, String(err.message || err));
5833
+ actionAgentIds.delete(actionId);
5834
+ }
5835
+ }
5836
+ return { actionId, milestoneId, exitCode: -1, errorOutput: String(err.message || err), durationMs, startedAt, completedAt, retried: false, attempts: 1 };
5434
5837
  });
5435
5838
  }
5436
5839
  function start() {
@@ -5445,6 +5848,10 @@ data: ${JSON.stringify(data)}
5445
5848
  isRunning = true;
5446
5849
  stopRequested = false;
5447
5850
  results = [];
5851
+ if (registry) {
5852
+ const agent = registry.spawn("pipeline", "manifest", "");
5853
+ pipelineAgentId = agent.id;
5854
+ }
5448
5855
  pipelineState = {
5449
5856
  currentWave: 0,
5450
5857
  totalWaves: waves.length,
@@ -5497,7 +5904,7 @@ data: ${JSON.stringify(data)}
5497
5904
  const waveResults = await Promise.all(promises);
5498
5905
  for (let ri = 0; ri < waveResults.length; ri++) {
5499
5906
  const r = waveResults[ri];
5500
- if (r.exitCode !== 0 && !stopRequested && isTransientFailure(r.exitCode, r.stderrOutput)) {
5907
+ if (r.exitCode !== 0 && !stopRequested && isTransientFailure(r.exitCode, r.errorOutput)) {
5501
5908
  broadcast("action-retry", { actionId: r.actionId, milestoneId: r.milestoneId, attempt: 2, reason: "transient failure detected" });
5502
5909
  appendLog(
5503
5910
  findMilestoneFolder(path.join(cwd, ".planning"), r.milestoneId) ? path.join(findMilestoneFolder(path.join(cwd, ".planning"), r.milestoneId), "execution.log") : void 0,
@@ -5557,7 +5964,7 @@ data: ${JSON.stringify(data)}
5557
5964
  const finalResults = [...results];
5558
5965
  isRunning = false;
5559
5966
  if (stopRequested) {
5560
- for (const actionId of activeProcesses.keys()) {
5967
+ for (const actionId of activeControllers.keys()) {
5561
5968
  finalState.stoppedActions.push(actionId);
5562
5969
  }
5563
5970
  }
@@ -5572,6 +5979,19 @@ data: ${JSON.stringify(data)}
5572
5979
  results: finalResults,
5573
5980
  reportPath: reportPath ? ".planning/execution-report.md" : null
5574
5981
  });
5982
+ if (registry && pipelineAgentId) {
5983
+ const failed = finalState.failedActions.length;
5984
+ if (failed === 0 && !stopRequested) {
5985
+ registry.complete(pipelineAgentId, {
5986
+ completed: finalState.completedActions.length,
5987
+ failed: 0,
5988
+ reportPath: reportPath ? ".planning/execution-report.md" : null
5989
+ });
5990
+ } else {
5991
+ registry.fail(pipelineAgentId, 1, stopRequested ? "stopped by user" : `${failed} action(s) failed`);
5992
+ }
5993
+ pipelineAgentId = void 0;
5994
+ }
5575
5995
  stopRequested = false;
5576
5996
  pausedOnFailure = null;
5577
5997
  pipelineState = null;
@@ -5581,6 +6001,10 @@ data: ${JSON.stringify(data)}
5581
6001
  pausedOnFailure = null;
5582
6002
  pipelineState = null;
5583
6003
  persistState();
6004
+ if (registry && pipelineAgentId) {
6005
+ registry.fail(pipelineAgentId, -1, String(_err));
6006
+ pipelineAgentId = void 0;
6007
+ }
5584
6008
  broadcast("pipeline-complete", {
5585
6009
  completed: [],
5586
6010
  failed: [],
@@ -5600,11 +6024,18 @@ data: ${JSON.stringify(data)}
5600
6024
  skipResolve("stop");
5601
6025
  skipResolve = null;
5602
6026
  }
5603
- for (const [, proc] of activeProcesses) {
6027
+ for (const [actionId, controller] of activeControllers) {
5604
6028
  try {
5605
- proc.kill("SIGTERM");
6029
+ controller.abort();
5606
6030
  } catch (_) {
5607
6031
  }
6032
+ if (registry) {
6033
+ const aId = actionAgentIds.get(actionId);
6034
+ if (aId) {
6035
+ registry.fail(aId, -1, "stopped by user");
6036
+ actionAgentIds.delete(actionId);
6037
+ }
6038
+ }
5608
6039
  }
5609
6040
  persistState();
5610
6041
  return { ok: true };
@@ -5618,7 +6049,7 @@ data: ${JSON.stringify(data)}
5618
6049
  running: isRunning,
5619
6050
  currentWave: pipelineState.currentWave,
5620
6051
  totalWaves: pipelineState.totalWaves,
5621
- activeActions: [...activeProcesses.keys()],
6052
+ activeActions: [...activeControllers.keys()],
5622
6053
  completedActions: pipelineState.completedActions,
5623
6054
  failedActions: pipelineState.failedActions,
5624
6055
  results
@@ -5646,7 +6077,7 @@ data: ${JSON.stringify(data)}
5646
6077
  totalActions: totalActionCount,
5647
6078
  completedActions: pipelineState ? pipelineState.completedActions : [],
5648
6079
  failedActions: pipelineState ? pipelineState.failedActions : [],
5649
- activeActions: [...activeProcesses.keys()],
6080
+ activeActions: [...activeControllers.keys()],
5650
6081
  outputBuffers,
5651
6082
  pausedOnFailure
5652
6083
  };
@@ -5657,6 +6088,201 @@ data: ${JSON.stringify(data)}
5657
6088
  }
5658
6089
  });
5659
6090
 
6091
+ // src/server/agent-registry.js
6092
+ var require_agent_registry = __commonJS({
6093
+ "src/server/agent-registry.js"(exports2, module2) {
6094
+ "use strict";
6095
+ var fs = require("node:fs");
6096
+ var path = require("node:path");
6097
+ var RECENT_MAX = 50;
6098
+ var RECENT_MAX_AGE_MS = 30 * 60 * 1e3;
6099
+ function createAgentRegistry(cwd, broadcastFn) {
6100
+ const agents = /* @__PURE__ */ new Map();
6101
+ let recentAgents = [];
6102
+ const statePath = path.join(cwd, ".planning", "agent-state.json");
6103
+ function pruneRecent() {
6104
+ const cutoff = Date.now() - RECENT_MAX_AGE_MS;
6105
+ recentAgents = recentAgents.filter((a) => {
6106
+ const ts = a.completedAt || a.updatedAt;
6107
+ return new Date(ts).getTime() > cutoff;
6108
+ });
6109
+ }
6110
+ function moveToRecent(agentId) {
6111
+ const record = agents.get(agentId);
6112
+ if (!record) return;
6113
+ agents.delete(agentId);
6114
+ recentAgents.push(record);
6115
+ if (recentAgents.length > RECENT_MAX) {
6116
+ recentAgents = recentAgents.slice(recentAgents.length - RECENT_MAX);
6117
+ }
6118
+ }
6119
+ function persistState() {
6120
+ try {
6121
+ const state = {
6122
+ agents: Object.fromEntries(agents),
6123
+ recentAgents,
6124
+ persistedAt: (/* @__PURE__ */ new Date()).toISOString()
6125
+ };
6126
+ fs.writeFileSync(statePath, JSON.stringify(state, null, 2), "utf-8");
6127
+ } catch (_) {
6128
+ }
6129
+ }
6130
+ function spawn(type, target, milestoneId) {
6131
+ const now = (/* @__PURE__ */ new Date()).toISOString();
6132
+ const id = `${type.slice(0, 4)}-${target}-${Date.now()}`;
6133
+ const record = {
6134
+ id,
6135
+ type,
6136
+ target,
6137
+ milestoneId: milestoneId || "",
6138
+ status: "running",
6139
+ startedAt: now,
6140
+ updatedAt: now,
6141
+ completedAt: null,
6142
+ exitCode: null,
6143
+ error: null,
6144
+ result: null
6145
+ };
6146
+ agents.set(id, record);
6147
+ broadcastFn("agent-start", record);
6148
+ persistState();
6149
+ return record;
6150
+ }
6151
+ function update(agentId, patch) {
6152
+ const record = agents.get(agentId);
6153
+ if (!record) return null;
6154
+ Object.assign(record, patch, { updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
6155
+ broadcastFn("agent-update", record);
6156
+ persistState();
6157
+ return record;
6158
+ }
6159
+ function complete(agentId, result) {
6160
+ const record = agents.get(agentId);
6161
+ if (!record) return null;
6162
+ const now = (/* @__PURE__ */ new Date()).toISOString();
6163
+ record.status = "complete";
6164
+ record.completedAt = now;
6165
+ record.updatedAt = now;
6166
+ record.exitCode = 0;
6167
+ record.result = result;
6168
+ moveToRecent(agentId);
6169
+ broadcastFn("agent-complete", record);
6170
+ persistState();
6171
+ return record;
6172
+ }
6173
+ function fail(agentId, exitCode, errorMessage) {
6174
+ const record = agents.get(agentId);
6175
+ if (!record) return null;
6176
+ const now = (/* @__PURE__ */ new Date()).toISOString();
6177
+ record.status = "failed";
6178
+ record.completedAt = now;
6179
+ record.updatedAt = now;
6180
+ record.exitCode = exitCode;
6181
+ record.error = errorMessage;
6182
+ moveToRecent(agentId);
6183
+ broadcastFn("agent-complete", record);
6184
+ persistState();
6185
+ return record;
6186
+ }
6187
+ function get(agentId) {
6188
+ const active = agents.get(agentId);
6189
+ if (active) return active;
6190
+ return recentAgents.find((a) => a.id === agentId) || null;
6191
+ }
6192
+ function getActive() {
6193
+ return [...agents.values()];
6194
+ }
6195
+ function getRecent(limit = 20) {
6196
+ pruneRecent();
6197
+ return recentAgents.slice(-limit);
6198
+ }
6199
+ function getAll() {
6200
+ return { active: getActive(), recent: getRecent() };
6201
+ }
6202
+ function markInterrupted(agentIds) {
6203
+ for (const id of agentIds) {
6204
+ const record = agents.get(id);
6205
+ if (!record) continue;
6206
+ const now = (/* @__PURE__ */ new Date()).toISOString();
6207
+ record.status = "interrupted";
6208
+ record.completedAt = now;
6209
+ record.updatedAt = now;
6210
+ moveToRecent(id);
6211
+ }
6212
+ persistState();
6213
+ }
6214
+ function loadFromDisk() {
6215
+ try {
6216
+ const raw = fs.readFileSync(statePath, "utf-8");
6217
+ return JSON.parse(raw);
6218
+ } catch (_) {
6219
+ return null;
6220
+ }
6221
+ }
6222
+ function restoreFromDisk() {
6223
+ const persisted = loadFromDisk();
6224
+ if (!persisted) return { restored: 0, interrupted: 0 };
6225
+ const now = (/* @__PURE__ */ new Date()).toISOString();
6226
+ let interruptedCount = 0;
6227
+ const seenIds = new Set(recentAgents.map((a) => a.id));
6228
+ const persistedAgents = persisted.agents || {};
6229
+ for (const [id, record] of Object.entries(persistedAgents)) {
6230
+ if (seenIds.has(id)) continue;
6231
+ record.status = "interrupted";
6232
+ record.completedAt = now;
6233
+ record.updatedAt = now;
6234
+ record.exitCode = -1;
6235
+ record.error = "server restarted";
6236
+ recentAgents.push(record);
6237
+ seenIds.add(id);
6238
+ interruptedCount++;
6239
+ }
6240
+ const cutoff = Date.now() - RECENT_MAX_AGE_MS;
6241
+ const persistedRecent = persisted.recentAgents || [];
6242
+ for (const record of persistedRecent) {
6243
+ if (seenIds.has(record.id)) continue;
6244
+ const ts = record.completedAt || record.updatedAt;
6245
+ if (new Date(ts).getTime() <= cutoff) continue;
6246
+ recentAgents.push(record);
6247
+ seenIds.add(record.id);
6248
+ }
6249
+ if (recentAgents.length > RECENT_MAX) {
6250
+ recentAgents = recentAgents.slice(recentAgents.length - RECENT_MAX);
6251
+ }
6252
+ persistState();
6253
+ return { restored: recentAgents.length, interrupted: interruptedCount };
6254
+ }
6255
+ return {
6256
+ spawn,
6257
+ update,
6258
+ complete,
6259
+ fail,
6260
+ get,
6261
+ getActive,
6262
+ getRecent,
6263
+ getAll,
6264
+ markInterrupted,
6265
+ loadFromDisk,
6266
+ restoreFromDisk
6267
+ };
6268
+ }
6269
+ if (require.main === module2) {
6270
+ const reg = createAgentRegistry(".", () => {
6271
+ });
6272
+ const a = reg.spawn("execution", "A-01", "M-01");
6273
+ console.log("spawned:", a.id, a.status);
6274
+ reg.complete(a.id, { path: "test.md" });
6275
+ console.log("completed:", reg.get(a.id).status);
6276
+ console.log("active:", reg.getActive().length);
6277
+ console.log("recent:", reg.getRecent().length);
6278
+ const restored = reg.restoreFromDisk();
6279
+ console.log("restored:", restored.restored, "interrupted:", restored.interrupted);
6280
+ console.log("OK");
6281
+ }
6282
+ module2.exports = { createAgentRegistry };
6283
+ }
6284
+ });
6285
+
5660
6286
  // src/server/index.js
5661
6287
  var require_server = __commonJS({
5662
6288
  "src/server/index.js"(exports2, module2) {
@@ -5685,7 +6311,9 @@ var require_server = __commonJS({
5685
6311
  var { computeWorkflowState } = require_workflow_state();
5686
6312
  var { createPlayRunner } = require_play();
5687
6313
  var { computeReadiness } = require_readiness();
6314
+ var { computeLifecycleStages } = require_lifecycle_stages();
5688
6315
  var { createPipelineRunner } = require_pipeline_runner();
6316
+ var { createAgentRegistry } = require_agent_registry();
5689
6317
  var MIME_TYPES = {
5690
6318
  ".html": "text/html; charset=utf-8",
5691
6319
  ".js": "application/javascript; charset=utf-8",
@@ -5867,6 +6495,21 @@ var require_server = __commonJS({
5867
6495
  sendJson(res, 500, { error: String(err) });
5868
6496
  }
5869
6497
  }
6498
+ function handleLifecycle(res, cwd) {
6499
+ try {
6500
+ const graph = runLoadGraph2(cwd);
6501
+ if ("error" in graph) {
6502
+ sendJson(res, 500, { error: graph.error });
6503
+ return;
6504
+ }
6505
+ const pm = getProcessManager(cwd);
6506
+ const runningIds = new Set(pm.running());
6507
+ const result = computeLifecycleStages(graph, runningIds);
6508
+ sendJson(res, 200, result);
6509
+ } catch (err) {
6510
+ sendJson(res, 500, { error: String(err) });
6511
+ }
6512
+ }
5870
6513
  async function handleSaveManifest(req, res, cwd) {
5871
6514
  try {
5872
6515
  const body = await readJsonBody(req);
@@ -5977,25 +6620,38 @@ var require_server = __commonJS({
5977
6620
  }
5978
6621
  function handleExecuteAction(res, cwd, actionId) {
5979
6622
  try {
5980
- const result = runGetExecPlan2(cwd, ["--action", actionId]);
5981
- if (result.error || !result.execPlan) {
5982
- sendJson(res, 400, { error: "Action not found or no exec-plan" });
6623
+ const graph = runLoadGraph2(cwd);
6624
+ if ("error" in graph) {
6625
+ sendJson(res, 500, { error: graph.error });
5983
6626
  return;
5984
6627
  }
5985
- const graph = runLoadGraph2(cwd);
5986
- if (!("error" in graph)) {
5987
- const normalizedId = actionId.toUpperCase();
5988
- const action = graph.actions.find((a) => a.id.toUpperCase() === normalizedId);
5989
- if (action && action.reviewState !== "approved") {
5990
- sendJson(res, 403, {
5991
- error: "Action not approved for execution",
5992
- unapproved: [{ id: action.id, title: action.title, reviewState: action.reviewState || "draft" }]
5993
- });
5994
- return;
5995
- }
6628
+ const normalizedId = actionId.toUpperCase();
6629
+ const action = graph.actions.find((a) => a.id.toUpperCase() === normalizedId);
6630
+ if (!action) {
6631
+ sendJson(res, 404, { error: "Action not found" });
6632
+ return;
6633
+ }
6634
+ if (action.reviewState !== "approved") {
6635
+ sendJson(res, 403, {
6636
+ error: "Action not approved for execution",
6637
+ unapproved: [{ id: action.id, title: action.title, reviewState: action.reviewState || "draft" }]
6638
+ });
6639
+ return;
5996
6640
  }
6641
+ const milestoneId = (action.causes || []).find((c) => c.startsWith("M-")) || (action.causes || [])[0] || actionId;
6642
+ const milestone = graph.milestones.find((m) => m.id === milestoneId);
6643
+ const declaration = milestone ? (graph.declarations || []).find((d) => (milestone.realizes || []).includes(d.id)) : null;
6644
+ const siblingActions = graph.actions.filter(
6645
+ (a) => a.id !== action.id && (a.causes || []).includes(milestoneId)
6646
+ );
6647
+ const actionContext = {
6648
+ action: { id: action.id, title: action.title, produces: action.produces || "", status: action.status },
6649
+ milestone: milestone ? { id: milestone.id, title: milestone.title, description: milestone.description || "" } : null,
6650
+ declaration: declaration ? { id: declaration.id, statement: declaration.statement || declaration.title || "" } : null,
6651
+ siblingActions: siblingActions.map((a) => ({ id: a.id, title: a.title, produces: a.produces || "", status: a.status }))
6652
+ };
5997
6653
  const pm = getProcessManager(cwd);
5998
- const execResult = pm.execute(actionId, result.milestoneId);
6654
+ const execResult = pm.execute(actionId, milestoneId, actionContext);
5999
6655
  if (execResult.error) {
6000
6656
  sendJson(res, execResult.status || 500, { error: execResult.error });
6001
6657
  return;
@@ -6019,24 +6675,35 @@ var require_server = __commonJS({
6019
6675
  milestones: d.milestones || []
6020
6676
  }));
6021
6677
  const dr = getDerivationRunner(cwd);
6022
- const result = dr.derive(body.declarationId || null, declarations);
6678
+ const declarationId = body.declarationId || null;
6679
+ const result = dr.derive(declarationId, declarations);
6023
6680
  if (result.error) {
6024
6681
  sendJson(res, result.status || 500, { error: result.error });
6025
6682
  return;
6026
6683
  }
6027
- sendJson(res, 202, { ok: true, sessionId: result.sessionId });
6684
+ sendJson(res, 202, { ok: true, sessionId: result.sessionId, declarationId });
6028
6685
  } catch (err) {
6029
6686
  sendJson(res, 400, { error: String(err) });
6030
6687
  }
6031
6688
  }
6032
- function handleDeriveStop(res, cwd) {
6689
+ function handleDeriveStop(req, res, cwd) {
6033
6690
  const dr = getDerivationRunner(cwd);
6034
- const result = dr.stop();
6035
- if (result.error) {
6036
- sendJson(res, result.status || 500, { error: result.error });
6037
- } else {
6038
- sendJson(res, 200, { ok: true });
6039
- }
6691
+ let sessionId;
6692
+ const chunks = [];
6693
+ req.on("data", (c) => chunks.push(c));
6694
+ req.on("end", () => {
6695
+ try {
6696
+ const body = JSON.parse(Buffer.concat(chunks).toString("utf-8"));
6697
+ sessionId = body.sessionId;
6698
+ } catch (_) {
6699
+ }
6700
+ const result = dr.stop(sessionId);
6701
+ if (result.error) {
6702
+ sendJson(res, result.status || 500, { error: result.error });
6703
+ } else {
6704
+ sendJson(res, 200, { ok: true });
6705
+ }
6706
+ });
6040
6707
  }
6041
6708
  async function handleDeriveAccept(req, res, cwd) {
6042
6709
  try {
@@ -6056,6 +6723,36 @@ var require_server = __commonJS({
6056
6723
  sendJson(res, 400, { error: String(err) });
6057
6724
  }
6058
6725
  }
6726
+ function handleDeriveAll(res, cwd) {
6727
+ try {
6728
+ const graph = runLoadGraph2(cwd);
6729
+ if ("error" in graph) {
6730
+ sendJson(res, 500, { error: graph.error });
6731
+ return;
6732
+ }
6733
+ const declarations = graph.declarations.map((d) => ({
6734
+ id: d.id,
6735
+ statement: d.statement,
6736
+ milestones: d.milestones || []
6737
+ }));
6738
+ const needsPlanning = declarations.filter((d) => !d.milestones || d.milestones.length === 0);
6739
+ if (needsPlanning.length === 0) {
6740
+ sendJson(res, 200, { ok: true, sessions: [], message: "All declarations already have milestones" });
6741
+ return;
6742
+ }
6743
+ const dr = getDerivationRunner(cwd);
6744
+ const results = [];
6745
+ for (const decl of needsPlanning) {
6746
+ const result = dr.derive(decl.id, declarations);
6747
+ if (result.ok) {
6748
+ results.push({ declarationId: decl.id, sessionId: result.sessionId });
6749
+ }
6750
+ }
6751
+ sendJson(res, 202, { ok: true, sessions: results });
6752
+ } catch (err) {
6753
+ sendJson(res, 500, { error: String(err) });
6754
+ }
6755
+ }
6059
6756
  function handleActionDerive(res, cwd, milestoneId) {
6060
6757
  try {
6061
6758
  const graph = runLoadGraph2(cwd);
@@ -6360,19 +7057,38 @@ var require_server = __commonJS({
6360
7057
  }
6361
7058
  }
6362
7059
  var sseClients = /* @__PURE__ */ new Set();
7060
+ var agentRegistry = null;
7061
+ function getAgentRegistry(cwd) {
7062
+ if (!agentRegistry) {
7063
+ agentRegistry = createAgentRegistry(cwd, (event, data) => {
7064
+ const payload = `event: ${event}
7065
+ data: ${JSON.stringify(data)}
7066
+
7067
+ `;
7068
+ for (const client of sseClients) {
7069
+ try {
7070
+ client.write(payload);
7071
+ } catch (_) {
7072
+ sseClients.delete(client);
7073
+ }
7074
+ }
7075
+ });
7076
+ }
7077
+ return agentRegistry;
7078
+ }
6363
7079
  var processManager = null;
6364
7080
  function getProcessManager(cwd) {
6365
- if (!processManager) processManager = createProcessManager(sseClients, cwd);
7081
+ if (!processManager) processManager = createProcessManager(sseClients, cwd, getAgentRegistry(cwd));
6366
7082
  return processManager;
6367
7083
  }
6368
7084
  var derivationRunner = null;
6369
7085
  function getDerivationRunner(cwd) {
6370
- if (!derivationRunner) derivationRunner = createDerivationRunner(sseClients, cwd);
7086
+ if (!derivationRunner) derivationRunner = createDerivationRunner(sseClients, cwd, getAgentRegistry(cwd));
6371
7087
  return derivationRunner;
6372
7088
  }
6373
7089
  var actionDerivationRunner = null;
6374
7090
  function getActionDerivationRunner(cwd) {
6375
- if (!actionDerivationRunner) actionDerivationRunner = createActionDerivationRunner(sseClients, cwd);
7091
+ if (!actionDerivationRunner) actionDerivationRunner = createActionDerivationRunner(sseClients, cwd, getAgentRegistry(cwd));
6376
7092
  return actionDerivationRunner;
6377
7093
  }
6378
7094
  var playRunner = null;
@@ -6382,7 +7098,7 @@ var require_server = __commonJS({
6382
7098
  }
6383
7099
  var pipelineRunnerInstance = null;
6384
7100
  function getPipelineRunner(cwd) {
6385
- if (!pipelineRunnerInstance) pipelineRunnerInstance = createPipelineRunner(sseClients, cwd);
7101
+ if (!pipelineRunnerInstance) pipelineRunnerInstance = createPipelineRunner(sseClients, cwd, getAgentRegistry(cwd));
6386
7102
  return pipelineRunnerInstance;
6387
7103
  }
6388
7104
  var revisionRunner = null;
@@ -6391,7 +7107,7 @@ var require_server = __commonJS({
6391
7107
  revisionRunner = createRevisionRunner(sseClients, cwd, (nodeId) => {
6392
7108
  setReviewState(cwd, nodeId, "in_review");
6393
7109
  broadcastChange();
6394
- });
7110
+ }, getAgentRegistry(cwd));
6395
7111
  }
6396
7112
  return revisionRunner;
6397
7113
  }
@@ -6546,7 +7262,7 @@ var require_server = __commonJS({
6546
7262
  sendJson(res, 200, { ok: true });
6547
7263
  }
6548
7264
  }
6549
- var refineSession = null;
7265
+ var refineSessions = /* @__PURE__ */ new Map();
6550
7266
  function buildRefinePrompt(nodeId, graph, mode, userMessage) {
6551
7267
  const id = nodeId.toUpperCase();
6552
7268
  const prefix = id.split("-")[0];
@@ -6586,99 +7302,772 @@ Produces: ${m.produces || "(none)"}`).join("\n\n");
6586
7302
  const siblingActions = graph.actions.filter((a) => a.id !== action.id && (a.causes || []).some((c) => milestoneIds.includes(c)));
6587
7303
  siblingsContent = siblingActions.map((a) => `${a.id}: ${a.title} [${a.status}]`).join("\n");
6588
7304
  }
6589
- const modeInstructions = {
6590
- write: `The user wants to refine this ${nodeType} with specific direction:
6591
-
6592
- "${userMessage || ""}"
6593
-
6594
- Apply their feedback to improve the title and statement/produces. Keep the same format and scope level.`,
6595
- outdated: `Check if this ${nodeType} is still relevant given the current state of its parent and siblings. Some siblings may already be DONE. Consider:
6596
- - Has progress on siblings made this redundant?
6597
- - Does the title/statement still accurately describe what's needed?
6598
- - Should this be reworded to reflect current reality?`,
6599
- sharpen: `Make this ${nodeType} more specific and actionable. Consider:
6600
- - Is the title vague or generic? Make it concrete.
6601
- - Does the statement/produces describe a clear, verifiable outcome?
6602
- - Could someone read this and know exactly what "done" looks like?`,
6603
- consolidate: `Check if this ${nodeType} overlaps with its siblings. Consider:
6604
- - Are any siblings covering the same ground?
6605
- - Could this be merged with a sibling for clarity?
6606
- - Is this a subset of another sibling?
6607
-
6608
- If overlap exists, suggest how to differentiate or merge. If no overlap, say so.`,
6609
- expand: `This ${nodeType} may be too broad. Consider:
6610
- - Could it be broken into 2-3 more specific items?
6611
- - Are there implicit sub-tasks hiding in the description?
6612
- - Would splitting improve clarity and trackability?
6613
-
6614
- Suggest a breakdown if warranted. If it's already well-scoped, say so.`
6615
- };
6616
- const taskBlock = modeInstructions[mode] || `Analyze this ${nodeType} in the context of its parent and siblings. Consider:
6617
- - Is it well-scoped? Too broad or too narrow compared to siblings?
6618
- - Does it overlap with any sibling?
6619
- - Does it clearly contribute to its parent's goal?
6620
- - Is the title/statement clear and specific?`;
6621
- return `You are reviewing a Declare project artifact and suggesting improvements.
6622
-
6623
- ## This ${nodeType}
6624
-
6625
- ${nodeContent}
6626
-
6627
- ## Parent context
6628
-
6629
- ${parentContent}
6630
-
6631
- ## Sibling ${nodeType === "declaration" ? "declarations" : nodeType === "milestone" ? "milestones" : "actions"}
7305
+ const modeInstructions = {
7306
+ write: `The user wants to modify this ${nodeType} with specific direction:
7307
+
7308
+ "${userMessage || ""}"
7309
+
7310
+ You have tool access \u2014 directly edit the project files to apply the requested changes. The planning files are at:
7311
+ - ${nodeType === "declaration" ? "FUTURE.md" : nodeType === "milestone" ? "MILESTONES.md" : "the milestone PLAN.md"}
7312
+ - MILESTONES.md (for milestone references)
7313
+ - FUTURE-ARCHIVE.md (if archiving)
7314
+
7315
+ Apply the changes directly. After making edits, summarize what you changed.`,
7316
+ outdated: `Check if this ${nodeType} is still relevant given the current state of its parent and siblings. Some siblings may already be DONE. Consider:
7317
+ - Has progress on siblings made this redundant?
7318
+ - Does the title/statement still accurately describe what's needed?
7319
+ - Should this be reworded to reflect current reality?`,
7320
+ sharpen: `Make this ${nodeType} more specific and actionable. Consider:
7321
+ - Is the title vague or generic? Make it concrete.
7322
+ - Does the statement/produces describe a clear, verifiable outcome?
7323
+ - Could someone read this and know exactly what "done" looks like?`,
7324
+ consolidate: `Check if this ${nodeType} overlaps with its siblings. Consider:
7325
+ - Are any siblings covering the same ground?
7326
+ - Could this be merged with a sibling for clarity?
7327
+ - Is this a subset of another sibling?
7328
+
7329
+ If overlap exists, suggest how to differentiate or merge. If no overlap, say so.`,
7330
+ expand: `This ${nodeType} may be too broad. Consider:
7331
+ - Could it be broken into 2-3 more specific items?
7332
+ - Are there implicit sub-tasks hiding in the description?
7333
+ - Would splitting improve clarity and trackability?
7334
+
7335
+ Suggest a breakdown if warranted. If it's already well-scoped, say so.`
7336
+ };
7337
+ const taskBlock = modeInstructions[mode] || `Analyze this ${nodeType} in the context of its parent and siblings. Consider:
7338
+ - Is it well-scoped? Too broad or too narrow compared to siblings?
7339
+ - Does it overlap with any sibling?
7340
+ - Does it clearly contribute to its parent's goal?
7341
+ - Is the title/statement clear and specific?`;
7342
+ return `You are reviewing a Declare project artifact and suggesting improvements.
7343
+
7344
+ ## This ${nodeType}
7345
+
7346
+ ${nodeContent}
7347
+
7348
+ ## Parent context
7349
+
7350
+ ${parentContent}
7351
+
7352
+ ## Sibling ${nodeType === "declaration" ? "declarations" : nodeType === "milestone" ? "milestones" : "actions"}
7353
+
7354
+ ${siblingsContent || "(none)"}
7355
+
7356
+ ## Task
7357
+
7358
+ ${taskBlock}
7359
+
7360
+ If improvements are possible, suggest a revised version. Output ONLY the improved text in this format:
7361
+
7362
+ **Title:** <improved title>
7363
+ **Statement:** <improved statement or produces>
7364
+ **Reason:** <one sentence explaining why this is better>
7365
+
7366
+ If the current version is already good, output exactly: LGTM \u2014 no changes needed.`;
7367
+ }
7368
+ async function handleRefine(req, res, cwd, nodeId) {
7369
+ try {
7370
+ const body = await readJsonBody(req).catch(() => ({}));
7371
+ const mode = body.mode || "general";
7372
+ const userMessage = body.message || "";
7373
+ const graph = runLoadGraph2(cwd);
7374
+ if ("error" in graph) {
7375
+ sendJson(res, 500, { error: graph.error });
7376
+ return;
7377
+ }
7378
+ const prompt = buildRefinePrompt(nodeId, graph, mode, userMessage);
7379
+ if (!prompt) {
7380
+ sendJson(res, 404, { error: "Node not found: " + nodeId });
7381
+ return;
7382
+ }
7383
+ const sessionId = `refine-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
7384
+ const { runAI } = require_ai_runner();
7385
+ const activityFile = path.join(cwd, ".planning", "activity.jsonl");
7386
+ const nId = nodeId.toUpperCase();
7387
+ let refineAgentId;
7388
+ try {
7389
+ const reg = getAgentRegistry(cwd);
7390
+ const agent = reg.spawn("refine", nId, nId);
7391
+ refineAgentId = agent.id;
7392
+ } catch (_) {
7393
+ }
7394
+ const abortController = new AbortController();
7395
+ refineSessions.set(sessionId, { sessionId, nodeId: nId, suggestion: "", abortController, agentId: refineAgentId });
7396
+ const isWriteMode = mode === "write";
7397
+ runAI(prompt, {
7398
+ cwd,
7399
+ model: "haiku",
7400
+ maxTurns: isWriteMode ? 10 : 1,
7401
+ withTools: isWriteMode,
7402
+ abortController,
7403
+ onText: (text) => {
7404
+ const session = refineSessions.get(sessionId);
7405
+ if (session) session.suggestion += text;
7406
+ const payload = `event: refine-output
7407
+ data: ${JSON.stringify({ sessionId, nodeId: nId, text })}
7408
+
7409
+ `;
7410
+ for (const client of sseClients) {
7411
+ try {
7412
+ client.write(payload);
7413
+ } catch (_) {
7414
+ sseClients.delete(client);
7415
+ }
7416
+ }
7417
+ }
7418
+ }).then(({ text, error }) => {
7419
+ const session = refineSessions.get(sessionId);
7420
+ const suggestion = text || (session ? session.suggestion : "");
7421
+ refineSessions.delete(sessionId);
7422
+ const exitCode = error ? 1 : 0;
7423
+ const payload = `event: refine-complete
7424
+ data: ${JSON.stringify({ sessionId, nodeId: nId, exitCode, suggestion, error })}
7425
+
7426
+ `;
7427
+ for (const client of sseClients) {
7428
+ try {
7429
+ client.write(payload);
7430
+ } catch (_) {
7431
+ sseClients.delete(client);
7432
+ }
7433
+ }
7434
+ if (refineAgentId) {
7435
+ try {
7436
+ const reg = getAgentRegistry(cwd);
7437
+ if (!error) reg.complete(refineAgentId, { nodeId: nId });
7438
+ else reg.fail(refineAgentId, 1, error);
7439
+ } catch (_) {
7440
+ }
7441
+ }
7442
+ const phase = error ? "error" : "done";
7443
+ const desc = error ? `Review of ${nId} failed: ${error}` : `Review of ${nId} complete` + (suggestion.includes("LGTM") ? " \u2014 no changes needed" : " \u2014 suggestion ready");
7444
+ try {
7445
+ fs.appendFileSync(activityFile, JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), tool: "Task", phase, agent: "Refine", desc }) + "\n");
7446
+ } catch (_) {
7447
+ }
7448
+ }).catch((err) => {
7449
+ refineSessions.delete(sessionId);
7450
+ const payload = `event: refine-complete
7451
+ data: ${JSON.stringify({ sessionId, nodeId: nId, exitCode: 1, suggestion: "", error: String(err) })}
7452
+
7453
+ `;
7454
+ for (const client of sseClients) {
7455
+ try {
7456
+ client.write(payload);
7457
+ } catch (_) {
7458
+ sseClients.delete(client);
7459
+ }
7460
+ }
7461
+ if (refineAgentId) {
7462
+ try {
7463
+ getAgentRegistry(cwd).fail(refineAgentId, 1, String(err));
7464
+ } catch (_) {
7465
+ }
7466
+ }
7467
+ });
7468
+ const entry = { ts: (/* @__PURE__ */ new Date()).toISOString(), tool: "Task", phase: "start", agent: "Refine", desc: `Analyzing ${nodeId.toUpperCase()} for improvements` };
7469
+ fs.appendFileSync(activityFile, JSON.stringify(entry) + "\n");
7470
+ sendJson(res, 202, { ok: true, sessionId });
7471
+ } catch (err) {
7472
+ sendJson(res, 500, { error: String(err) });
7473
+ }
7474
+ }
7475
+ function handleRefineStop(res, sessionId) {
7476
+ if (sessionId) {
7477
+ const session = refineSessions.get(sessionId);
7478
+ if (!session) {
7479
+ sendJson(res, 404, { error: "Session not found" });
7480
+ return;
7481
+ }
7482
+ try {
7483
+ session.abortController.abort();
7484
+ } catch (_) {
7485
+ }
7486
+ refineSessions.delete(sessionId);
7487
+ sendJson(res, 200, { ok: true });
7488
+ } else {
7489
+ for (const [id, session] of refineSessions) {
7490
+ try {
7491
+ session.abortController.abort();
7492
+ } catch (_) {
7493
+ }
7494
+ }
7495
+ refineSessions.clear();
7496
+ sendJson(res, 200, { ok: true });
7497
+ }
7498
+ }
7499
+ async function handleRefineAccept(req, res, cwd) {
7500
+ try {
7501
+ const body = await readJsonBody(req);
7502
+ const { nodeId, title, statement } = body;
7503
+ if (!nodeId) {
7504
+ sendJson(res, 400, { error: "Missing nodeId" });
7505
+ return;
7506
+ }
7507
+ const id = nodeId.toUpperCase();
7508
+ const prefix = id.split("-")[0];
7509
+ const planningDir = path.join(cwd, ".planning");
7510
+ if (prefix === "D") {
7511
+ const futurePath = path.join(planningDir, "FUTURE.md");
7512
+ const content = fs.readFileSync(futurePath, "utf-8");
7513
+ const declarations = parseFutureFile(content);
7514
+ const decl = declarations.find((d) => d.id === id);
7515
+ if (decl) {
7516
+ if (title) decl.title = title;
7517
+ if (statement) decl.statement = statement;
7518
+ const projectNameMatch = content.match(/^# Future:\\s*(.+)/m);
7519
+ const projectName = projectNameMatch ? projectNameMatch[1].trim() : "Project";
7520
+ fs.writeFileSync(futurePath, writeFutureFile(declarations, projectName), "utf-8");
7521
+ }
7522
+ } else if (prefix === "M") {
7523
+ const msPath = path.join(planningDir, "MILESTONES.md");
7524
+ const content = fs.readFileSync(msPath, "utf-8");
7525
+ const { milestones } = parseMilestonesFile(content);
7526
+ const mile = milestones.find((m) => m.id === id);
7527
+ if (mile && title) {
7528
+ mile.title = title;
7529
+ const projectNameMatch = content.match(/^# Milestones:\\s*(.+)/m);
7530
+ const projectName = projectNameMatch ? projectNameMatch[1].trim() : "Project";
7531
+ fs.writeFileSync(msPath, writeMilestonesFile(milestones, projectName), "utf-8");
7532
+ }
7533
+ }
7534
+ const activityFile = path.join(cwd, ".planning", "activity.jsonl");
7535
+ const entry = { ts: (/* @__PURE__ */ new Date()).toISOString(), tool: "Review", phase: "end", desc: `Refined ${id}`, nodeId: id, reviewState: "approved" };
7536
+ fs.appendFileSync(activityFile, JSON.stringify(entry) + "\n");
7537
+ sendJson(res, 200, { ok: true });
7538
+ } catch (err) {
7539
+ sendJson(res, 500, { error: String(err) });
7540
+ }
7541
+ }
7542
+ var discussSession = null;
7543
+ async function handleDiscuss(req, res, cwd, nodeId) {
7544
+ try {
7545
+ if (discussSession) {
7546
+ sendJson(res, 409, { error: "A discuss session is already running" });
7547
+ return;
7548
+ }
7549
+ const graph = runLoadGraph2(cwd);
7550
+ if ("error" in graph) {
7551
+ sendJson(res, 500, { error: graph.error });
7552
+ return;
7553
+ }
7554
+ const id = nodeId.toUpperCase();
7555
+ const prefix = id.split("-")[0];
7556
+ let nodeContext = "";
7557
+ if (prefix === "D") {
7558
+ const decl = graph.declarations.find((d) => d.id === id);
7559
+ if (!decl) {
7560
+ sendJson(res, 404, { error: "Declaration not found" });
7561
+ return;
7562
+ }
7563
+ nodeContext = `Declaration ${decl.id}: ${decl.title}
7564
+ Statement: ${decl.statement || decl.title}`;
7565
+ const myMiles = graph.milestones.filter((m) => (m.realizes || []).includes(id));
7566
+ if (myMiles.length > 0) {
7567
+ nodeContext += "\n\nExisting milestones:\n" + myMiles.map((m) => `- ${m.id}: ${m.title}`).join("\n");
7568
+ }
7569
+ } else if (prefix === "M") {
7570
+ const mile = graph.milestones.find((m) => m.id === id);
7571
+ if (!mile) {
7572
+ sendJson(res, 404, { error: "Milestone not found" });
7573
+ return;
7574
+ }
7575
+ nodeContext = `Milestone ${mile.id}: ${mile.title}`;
7576
+ const myActions = graph.actions.filter((a) => (a.causes || []).includes(id));
7577
+ if (myActions.length > 0) {
7578
+ nodeContext += "\n\nExisting actions:\n" + myActions.map((a) => `- ${a.id}: ${a.title}`).join("\n");
7579
+ }
7580
+ }
7581
+ const prompt = `You are helping a user plan their software project. You need to identify gray areas and ambiguities that should be resolved BEFORE creating detailed plans.
7582
+
7583
+ Given this project artifact:
7584
+
7585
+ ${nodeContext}
7586
+
7587
+ Identify 3-5 important questions that should be answered to avoid wasted work. Focus on:
7588
+ - Ambiguous scope (what's included vs excluded?)
7589
+ - Technical decisions that affect the plan
7590
+ - Dependencies or constraints not mentioned
7591
+ - Success criteria that need clarification
7592
+
7593
+ Output ONLY a JSON array of question objects:
7594
+ [{"question": "The question text", "context": "Why this matters", "options": ["Option A", "Option B"]}]
7595
+
7596
+ The options array is optional \u2014 include it only when there are clear alternatives to choose from.`;
7597
+ const sessionId = `discuss-${Date.now()}`;
7598
+ const { runAI } = require_ai_runner();
7599
+ const activityFile = path.join(cwd, ".planning", "activity.jsonl");
7600
+ let agentId;
7601
+ try {
7602
+ const reg = getAgentRegistry(cwd);
7603
+ const agent = reg.spawn("discuss", id, id);
7604
+ agentId = agent.id;
7605
+ } catch (_) {
7606
+ }
7607
+ const abortController = new AbortController();
7608
+ discussSession = { sessionId, nodeId: id, abortController, agentId };
7609
+ runAI(prompt, {
7610
+ cwd,
7611
+ model: "haiku",
7612
+ maxTurns: 1,
7613
+ abortController,
7614
+ onText: (text) => {
7615
+ const payload = `event: discuss-output
7616
+ data: ${JSON.stringify({ sessionId, nodeId: id, text })}
7617
+
7618
+ `;
7619
+ for (const client of sseClients) {
7620
+ try {
7621
+ client.write(payload);
7622
+ } catch (_) {
7623
+ sseClients.delete(client);
7624
+ }
7625
+ }
7626
+ }
7627
+ }).then(({ text, error }) => {
7628
+ const closingSession = discussSession;
7629
+ discussSession = null;
7630
+ let questions = null;
7631
+ if (!error && text) {
7632
+ try {
7633
+ questions = JSON.parse(text.trim());
7634
+ } catch (_) {
7635
+ const jsonMatch = text.match(/\[[\s\S]*\]/);
7636
+ if (jsonMatch) {
7637
+ try {
7638
+ questions = JSON.parse(jsonMatch[0]);
7639
+ } catch (_2) {
7640
+ }
7641
+ }
7642
+ }
7643
+ }
7644
+ const payload = `event: discuss-complete
7645
+ data: ${JSON.stringify({ sessionId, nodeId: id, questions, error })}
7646
+
7647
+ `;
7648
+ for (const client of sseClients) {
7649
+ try {
7650
+ client.write(payload);
7651
+ } catch (_) {
7652
+ sseClients.delete(client);
7653
+ }
7654
+ }
7655
+ if (closingSession && closingSession.agentId) {
7656
+ try {
7657
+ const reg = getAgentRegistry(cwd);
7658
+ if (!error) reg.complete(closingSession.agentId, { nodeId: id, questionCount: Array.isArray(questions) ? questions.length : 0 });
7659
+ else reg.fail(closingSession.agentId, 1, error);
7660
+ } catch (_) {
7661
+ }
7662
+ }
7663
+ const phase = error ? "error" : "done";
7664
+ const desc = error ? `Discuss ${id} failed: ${error}` : `Discuss ${id}: ${Array.isArray(questions) ? questions.length : 0} questions`;
7665
+ try {
7666
+ fs.appendFileSync(activityFile, JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), tool: "Task", phase, agent: "Discuss", desc }) + "\n");
7667
+ } catch (_) {
7668
+ }
7669
+ }).catch((err) => {
7670
+ discussSession = null;
7671
+ if (agentId) {
7672
+ try {
7673
+ getAgentRegistry(cwd).fail(agentId, 1, String(err));
7674
+ } catch (_) {
7675
+ }
7676
+ }
7677
+ });
7678
+ try {
7679
+ fs.appendFileSync(activityFile, JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), tool: "Task", phase: "start", agent: "Discuss", desc: `Analyzing ${id} for discussion` }) + "\n");
7680
+ } catch (_) {
7681
+ }
7682
+ sendJson(res, 202, { ok: true, sessionId });
7683
+ } catch (err) {
7684
+ sendJson(res, 500, { error: String(err) });
7685
+ }
7686
+ }
7687
+ async function handleDiscussAnswer(req, res, cwd, nodeId) {
7688
+ try {
7689
+ const body = await readJsonBody(req);
7690
+ const answers = body.answers;
7691
+ if (!Array.isArray(answers) || answers.length === 0) {
7692
+ sendJson(res, 400, { error: "Missing answers array" });
7693
+ return;
7694
+ }
7695
+ const id = nodeId.toUpperCase();
7696
+ const prefix = id.split("-")[0];
7697
+ const planningDir = path.join(cwd, ".planning");
7698
+ let contextContent = `# Context: ${id}
7699
+
7700
+ `;
7701
+ contextContent += `Generated: ${(/* @__PURE__ */ new Date()).toISOString()}
7702
+
7703
+ `;
7704
+ for (const item of answers) {
7705
+ contextContent += `## ${item.question}
7706
+
7707
+ ${item.answer}
7708
+
7709
+ `;
7710
+ }
7711
+ if (prefix === "M") {
7712
+ const { ensureMilestoneFolder } = require_milestone_folders();
7713
+ const graph = runLoadGraph2(cwd);
7714
+ if ("error" in graph) {
7715
+ sendJson(res, 500, { error: graph.error });
7716
+ return;
7717
+ }
7718
+ const milestone = graph.milestones.find((m) => m.id === id);
7719
+ if (!milestone) {
7720
+ sendJson(res, 404, { error: "Milestone not found" });
7721
+ return;
7722
+ }
7723
+ const folder = ensureMilestoneFolder(planningDir, milestone.id, milestone.title);
7724
+ const contextPath = path.join(folder, "CONTEXT.md");
7725
+ fs.writeFileSync(contextPath, contextContent, "utf-8");
7726
+ } else {
7727
+ const contextPath = path.join(planningDir, `CONTEXT-${id}.md`);
7728
+ fs.writeFileSync(contextPath, contextContent, "utf-8");
7729
+ }
7730
+ sendJson(res, 200, { ok: true });
7731
+ broadcastChange();
7732
+ } catch (err) {
7733
+ sendJson(res, 400, { error: String(err) });
7734
+ }
7735
+ }
7736
+ var commandSession = null;
7737
+ async function handleCommand(req, res, cwd) {
7738
+ try {
7739
+ if (commandSession) {
7740
+ sendJson(res, 409, { error: "A command is already running" });
7741
+ return;
7742
+ }
7743
+ const body = await readJsonBody(req).catch(() => ({}));
7744
+ const message = (body.message || "").trim();
7745
+ const context = body.context || {};
7746
+ if (!message) {
7747
+ sendJson(res, 400, { error: "No message provided" });
7748
+ return;
7749
+ }
7750
+ const sessionId = `cmd-${Date.now()}`;
7751
+ const { runAI } = require_ai_runner();
7752
+ const activityFile = path.join(cwd, ".planning", "activity.jsonl");
7753
+ const graph = runLoadGraph2(cwd);
7754
+ let contextBlock = "";
7755
+ if (!("error" in graph)) {
7756
+ if (context.nodeId) {
7757
+ const allNodes = [...graph.declarations || [], ...graph.milestones || [], ...graph.actions || []];
7758
+ const node = allNodes.find((n) => n.id === context.nodeId);
7759
+ if (node) {
7760
+ contextBlock = `
7761
+ Current view is focused on: ${node.id} \u2014 ${node.title || node.statement || ""} (${node.status || "PENDING"})`;
7762
+ }
7763
+ }
7764
+ if (context.nodeIds && context.nodeIds.length > 0) {
7765
+ const allNodes = [...graph.declarations || [], ...graph.milestones || [], ...graph.actions || []];
7766
+ const visible = context.nodeIds.map((id) => allNodes.find((n) => n.id === id)).filter(Boolean);
7767
+ contextBlock += `
7768
+ Visible nodes:
7769
+ ${visible.map((n) => `- ${n.id}: ${n.title || n.statement || ""} (${n.status || "PENDING"})`).join("\n")}`;
7770
+ }
7771
+ if (context.viewDescription) {
7772
+ contextBlock += `
7773
+ View: ${context.viewDescription}`;
7774
+ }
7775
+ }
7776
+ const prompt = `You are working on a Declare project. The project planning files are in ${path.join(cwd, ".planning")}/
7777
+
7778
+ The user is looking at the dashboard and says: "${message}"
7779
+ ${contextBlock}
7780
+
7781
+ The planning files:
7782
+ - FUTURE.md \u2014 declarations (the "what" the project declares will be true)
7783
+ - MILESTONES.md \u2014 milestones (checkpoints that realize declarations)
7784
+ - milestones/M-XX-slug/PLAN.md \u2014 action plans per milestone
7785
+
7786
+ Apply the user's request by editing the relevant files. Be concise in your response \u2014 just state what you did.`;
7787
+ let agentId;
7788
+ try {
7789
+ const reg = getAgentRegistry(cwd);
7790
+ const agent = reg.spawn("command", context.nodeId || "project", context.nodeId || "project");
7791
+ agentId = agent.id;
7792
+ } catch (_) {
7793
+ }
7794
+ const abortController = new AbortController();
7795
+ commandSession = { sessionId, abortController, agentId };
7796
+ runAI(prompt, {
7797
+ cwd,
7798
+ model: "sonnet",
7799
+ maxTurns: 15,
7800
+ withTools: true,
7801
+ abortController,
7802
+ onText: (text) => {
7803
+ const payload = `event: command-output
7804
+ data: ${JSON.stringify({ sessionId, text })}
7805
+
7806
+ `;
7807
+ for (const client of sseClients) {
7808
+ try {
7809
+ client.write(payload);
7810
+ } catch (_) {
7811
+ sseClients.delete(client);
7812
+ }
7813
+ }
7814
+ }
7815
+ }).then(({ text, error }) => {
7816
+ commandSession = null;
7817
+ const exitCode = error ? 1 : 0;
7818
+ const payload = `event: command-complete
7819
+ data: ${JSON.stringify({ sessionId, exitCode, result: text, error })}
7820
+
7821
+ `;
7822
+ for (const client of sseClients) {
7823
+ try {
7824
+ client.write(payload);
7825
+ } catch (_) {
7826
+ sseClients.delete(client);
7827
+ }
7828
+ }
7829
+ if (agentId) {
7830
+ try {
7831
+ const reg = getAgentRegistry(cwd);
7832
+ if (!error) reg.complete(agentId, { message: message.slice(0, 50) });
7833
+ else reg.fail(agentId, 1, error);
7834
+ } catch (_) {
7835
+ }
7836
+ }
7837
+ const phase = error ? "error" : "done";
7838
+ const desc = error ? `Command failed: ${error}` : `Command complete: ${text.slice(0, 80)}`;
7839
+ try {
7840
+ fs.appendFileSync(activityFile, JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), tool: "Task", phase, agent: "Command", desc }) + "\n");
7841
+ } catch (_) {
7842
+ }
7843
+ }).catch((err) => {
7844
+ commandSession = null;
7845
+ if (agentId) {
7846
+ try {
7847
+ getAgentRegistry(cwd).fail(agentId, 1, String(err));
7848
+ } catch (_) {
7849
+ }
7850
+ }
7851
+ });
7852
+ try {
7853
+ fs.appendFileSync(activityFile, JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), tool: "Task", phase: "start", agent: "Command", desc: message.slice(0, 100) }) + "\n");
7854
+ } catch (_) {
7855
+ }
7856
+ sendJson(res, 202, { ok: true, sessionId });
7857
+ } catch (err) {
7858
+ sendJson(res, 500, { error: String(err) });
7859
+ }
7860
+ }
7861
+ var onboardSession = null;
7862
+ function persistOnboardSession(cwd) {
7863
+ const filePath = path.join(cwd, ".planning", "onboard-session.json");
7864
+ if (!onboardSession) {
7865
+ try {
7866
+ fs.unlinkSync(filePath);
7867
+ } catch (_) {
7868
+ }
7869
+ return;
7870
+ }
7871
+ try {
7872
+ const data = {
7873
+ prompt: onboardSession.prompt,
7874
+ answers: onboardSession.answers,
7875
+ questions: onboardSession.questions,
7876
+ proposals: onboardSession.proposals,
7877
+ phase: onboardSession.phase,
7878
+ approveIndex: onboardSession.approveIndex || 0,
7879
+ savedAt: (/* @__PURE__ */ new Date()).toISOString()
7880
+ };
7881
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf-8");
7882
+ } catch (_) {
7883
+ }
7884
+ }
7885
+ function restoreOnboardSession(cwd) {
7886
+ const filePath = path.join(cwd, ".planning", "onboard-session.json");
7887
+ try {
7888
+ if (!fs.existsSync(filePath)) return false;
7889
+ const data = JSON.parse(fs.readFileSync(filePath, "utf-8"));
7890
+ if (data.phase === "questions" && !data.questions) return false;
7891
+ if (data.phase === "proposals" && !data.proposals) return false;
7892
+ onboardSession = {
7893
+ prompt: data.prompt,
7894
+ answers: data.answers,
7895
+ questions: data.questions,
7896
+ proposals: data.proposals,
7897
+ phase: data.phase,
7898
+ approveIndex: data.approveIndex || 0,
7899
+ abortController: null
7900
+ };
7901
+ return true;
7902
+ } catch (_) {
7903
+ return false;
7904
+ }
7905
+ }
7906
+ async function handleOnboard(req, res, cwd) {
7907
+ try {
7908
+ if (onboardSession) {
7909
+ sendJson(res, 409, { error: "An onboarding session is already running" });
7910
+ return;
7911
+ }
7912
+ const body = await readJsonBody(req);
7913
+ const message = (body.message || "").trim();
7914
+ if (!message) {
7915
+ sendJson(res, 400, { error: "No message provided" });
7916
+ return;
7917
+ }
7918
+ const { runAI } = require_ai_runner();
7919
+ const activityFile = path.join(cwd, ".planning", "activity.jsonl");
7920
+ const prompt = `You are helping a user plan their software project. They have described their vision:
6632
7921
 
6633
- ${siblingsContent || "(none)"}
7922
+ "${message}"
6634
7923
 
6635
- ## Task
7924
+ Identify 3-5 important clarification questions that should be answered before breaking this vision into concrete declarations. Focus on:
7925
+ - Scope boundaries (what's included vs excluded?)
7926
+ - Key technical decisions that affect the architecture
7927
+ - Target users and use cases
7928
+ - Priority and sequencing preferences
7929
+ - Success criteria and constraints
6636
7930
 
6637
- ${taskBlock}
7931
+ Output ONLY a JSON array of question objects:
7932
+ [{"question": "The question text", "context": "Why this matters for planning", "options": ["Option A", "Option B"]}]
6638
7933
 
6639
- If improvements are possible, suggest a revised version. Output ONLY the improved text in this format:
7934
+ The options array is optional \u2014 include it only when there are clear alternatives to choose from.`;
7935
+ let agentId;
7936
+ try {
7937
+ const reg = getAgentRegistry(cwd);
7938
+ const agent = reg.spawn("onboard", "project", "");
7939
+ agentId = agent.id;
7940
+ } catch (_) {
7941
+ }
7942
+ const abortController = new AbortController();
7943
+ onboardSession = { prompt: message, answers: null, questions: null, proposals: null, phase: "questions", approveIndex: 0, abortController, agentId };
7944
+ persistOnboardSession(cwd);
7945
+ runAI(prompt, {
7946
+ cwd,
7947
+ model: "haiku",
7948
+ maxTurns: 1,
7949
+ abortController,
7950
+ onText: (text) => {
7951
+ const payload = `event: onboard-output
7952
+ data: ${JSON.stringify({ text })}
6640
7953
 
6641
- **Title:** <improved title>
6642
- **Statement:** <improved statement or produces>
6643
- **Reason:** <one sentence explaining why this is better>
7954
+ `;
7955
+ for (const client of sseClients) {
7956
+ try {
7957
+ client.write(payload);
7958
+ } catch (_) {
7959
+ sseClients.delete(client);
7960
+ }
7961
+ }
7962
+ }
7963
+ }).then(({ text, error }) => {
7964
+ let questions = null;
7965
+ if (!error && text) {
7966
+ try {
7967
+ questions = JSON.parse(text.trim());
7968
+ } catch (_) {
7969
+ const jsonMatch = text.match(/\[[\s\S]*\]/);
7970
+ if (jsonMatch) {
7971
+ try {
7972
+ questions = JSON.parse(jsonMatch[0]);
7973
+ } catch (_2) {
7974
+ }
7975
+ }
7976
+ }
7977
+ }
7978
+ if (onboardSession && questions) {
7979
+ onboardSession.questions = questions;
7980
+ persistOnboardSession(cwd);
7981
+ }
7982
+ const payload = `event: onboard-questions-complete
7983
+ data: ${JSON.stringify({ questions, error })}
6644
7984
 
6645
- If the current version is already good, output exactly: LGTM \u2014 no changes needed.`;
7985
+ `;
7986
+ for (const client of sseClients) {
7987
+ try {
7988
+ client.write(payload);
7989
+ } catch (_) {
7990
+ sseClients.delete(client);
7991
+ }
7992
+ }
7993
+ if (agentId) {
7994
+ try {
7995
+ const reg = getAgentRegistry(cwd);
7996
+ if (!error) reg.complete(agentId, { questionCount: Array.isArray(questions) ? questions.length : 0 });
7997
+ else reg.fail(agentId, 1, error);
7998
+ } catch (_) {
7999
+ }
8000
+ }
8001
+ try {
8002
+ fs.appendFileSync(activityFile, JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), tool: "Task", phase: error ? "error" : "done", agent: "Onboard", desc: error ? `Onboard questions failed: ${error}` : `Onboard: ${Array.isArray(questions) ? questions.length : 0} questions` }) + "\n");
8003
+ } catch (_) {
8004
+ }
8005
+ }).catch((err) => {
8006
+ if (agentId) {
8007
+ try {
8008
+ getAgentRegistry(cwd).fail(agentId, 1, String(err));
8009
+ } catch (_) {
8010
+ }
8011
+ }
8012
+ });
8013
+ try {
8014
+ fs.appendFileSync(activityFile, JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), tool: "Task", phase: "start", agent: "Onboard", desc: "Generating clarification questions" }) + "\n");
8015
+ } catch (_) {
8016
+ }
8017
+ sendJson(res, 202, { ok: true });
8018
+ } catch (err) {
8019
+ sendJson(res, 500, { error: String(err) });
8020
+ }
6646
8021
  }
6647
- async function handleRefine(req, res, cwd, nodeId) {
8022
+ async function handleOnboardAnswer(req, res, cwd) {
6648
8023
  try {
6649
- if (refineSession) {
6650
- sendJson(res, 409, { error: "A refine session is already running" });
8024
+ if (!onboardSession) {
8025
+ sendJson(res, 400, { error: "No onboarding session active" });
6651
8026
  return;
6652
8027
  }
6653
- const body = await readJsonBody(req).catch(() => ({}));
6654
- const mode = body.mode || "general";
6655
- const userMessage = body.message || "";
6656
- const graph = runLoadGraph2(cwd);
6657
- if ("error" in graph) {
6658
- sendJson(res, 500, { error: graph.error });
6659
- return;
8028
+ const body = await readJsonBody(req);
8029
+ const answers = body.answers || [];
8030
+ onboardSession.answers = answers;
8031
+ onboardSession.phase = "proposals";
8032
+ persistOnboardSession(cwd);
8033
+ const { runAI } = require_ai_runner();
8034
+ const activityFile = path.join(cwd, ".planning", "activity.jsonl");
8035
+ let answersBlock = "";
8036
+ if (answers.length > 0) {
8037
+ answersBlock = "\n\nThe user answered these clarification questions:\n" + answers.map((a) => `Q: ${a.question}
8038
+ A: ${a.answer}`).join("\n\n");
6660
8039
  }
6661
- const prompt = buildRefinePrompt(nodeId, graph, mode, userMessage);
6662
- if (!prompt) {
6663
- sendJson(res, 404, { error: "Node not found: " + nodeId });
6664
- return;
8040
+ const prompt = `You are helping a user break down their software project vision into concrete declarations.
8041
+
8042
+ The user's vision: "${onboardSession.prompt}"${answersBlock}
8043
+
8044
+ In the Declare framework, a "declaration" is a statement about what WILL be true when the project succeeds. Each declaration should be:
8045
+ - A concrete, verifiable outcome (not a task or feature)
8046
+ - Scoped to be achievable independently
8047
+ - Written as a present-tense statement of truth (e.g., "Users can sign in with Google OAuth")
8048
+
8049
+ Propose 4-6 declarations that together cover the user's vision. Each should be distinct and non-overlapping.
8050
+
8051
+ Output ONLY a JSON array:
8052
+ [{"title": "Short declaration title", "statement": "The full declaration statement written as a present-tense truth", "reasoning": "Brief explanation of why this declaration matters"}]`;
8053
+ let agentId;
8054
+ try {
8055
+ const reg = getAgentRegistry(cwd);
8056
+ const agent = reg.spawn("onboard-propose", "project", "");
8057
+ agentId = agent.id;
8058
+ } catch (_) {
6665
8059
  }
6666
- const sessionId = `refine-${Date.now()}`;
6667
- const { spawn } = require("node:child_process");
6668
- const spawnEnv = { ...process.env, FORCE_COLOR: "0" };
6669
- delete spawnEnv.CLAUDECODE;
6670
- const proc = spawn("claude", ["-p", prompt, "--output-format", "text"], {
8060
+ const abortController = new AbortController();
8061
+ onboardSession.abortController = abortController;
8062
+ if (agentId) onboardSession.agentId = agentId;
8063
+ runAI(prompt, {
6671
8064
  cwd,
6672
- env: spawnEnv
6673
- });
6674
- const activityFile = path.join(cwd, ".planning", "activity.jsonl");
6675
- refineSession = { sessionId, proc, nodeId: nodeId.toUpperCase(), suggestion: "", stderr: "" };
6676
- if (proc.stdout) {
6677
- proc.stdout.on("data", (chunk) => {
6678
- const text = chunk.toString();
6679
- if (refineSession) refineSession.suggestion += text;
6680
- const payload = `event: refine-output
6681
- data: ${JSON.stringify({ sessionId, nodeId: nodeId.toUpperCase(), text })}
8065
+ model: "sonnet",
8066
+ maxTurns: 1,
8067
+ abortController,
8068
+ onText: (text) => {
8069
+ const payload = `event: onboard-output
8070
+ data: ${JSON.stringify({ text })}
6682
8071
 
6683
8072
  `;
6684
8073
  for (const client of sseClients) {
@@ -6688,21 +8077,28 @@ data: ${JSON.stringify({ sessionId, nodeId: nodeId.toUpperCase(), text })}
6688
8077
  sseClients.delete(client);
6689
8078
  }
6690
8079
  }
6691
- });
6692
- }
6693
- if (proc.stderr) {
6694
- proc.stderr.on("data", (chunk) => {
6695
- if (refineSession) refineSession.stderr += chunk.toString();
6696
- });
6697
- }
6698
- proc.on("close", (exitCode) => {
6699
- const suggestion = refineSession ? refineSession.suggestion : "";
6700
- const stderrText = refineSession ? refineSession.stderr : "";
6701
- const nId = refineSession ? refineSession.nodeId : nodeId.toUpperCase();
6702
- refineSession = null;
6703
- if (stderrText) console.error(`[refine ${nId}] stderr:`, stderrText.slice(0, 500));
6704
- const payload = `event: refine-complete
6705
- data: ${JSON.stringify({ sessionId, nodeId: nId, exitCode, suggestion, error: exitCode !== 0 ? stderrText.slice(0, 500) : void 0 })}
8080
+ }
8081
+ }).then(({ text, error }) => {
8082
+ let proposals = null;
8083
+ if (!error && text) {
8084
+ try {
8085
+ proposals = JSON.parse(text.trim());
8086
+ } catch (_) {
8087
+ const jsonMatch = text.match(/\[[\s\S]*\]/);
8088
+ if (jsonMatch) {
8089
+ try {
8090
+ proposals = JSON.parse(jsonMatch[0]);
8091
+ } catch (_2) {
8092
+ }
8093
+ }
8094
+ }
8095
+ }
8096
+ if (onboardSession && proposals) {
8097
+ onboardSession.proposals = proposals;
8098
+ persistOnboardSession(cwd);
8099
+ }
8100
+ const payload = `event: onboard-proposals-complete
8101
+ data: ${JSON.stringify({ proposals, error })}
6706
8102
 
6707
8103
  `;
6708
8104
  for (const client of sseClients) {
@@ -6712,76 +8108,81 @@ data: ${JSON.stringify({ sessionId, nodeId: nId, exitCode, suggestion, error: ex
6712
8108
  sseClients.delete(client);
6713
8109
  }
6714
8110
  }
6715
- const phase = exitCode === 0 ? "done" : "error";
6716
- const desc = exitCode === 0 ? `Review of ${nId} complete` + (suggestion.includes("LGTM") ? " \u2014 no changes needed" : " \u2014 suggestion ready") : `Review of ${nId} failed (exit ${exitCode})`;
6717
- const doneEntry = { ts: (/* @__PURE__ */ new Date()).toISOString(), tool: "Task", phase, agent: "Refine", desc };
8111
+ if (agentId) {
8112
+ try {
8113
+ const reg = getAgentRegistry(cwd);
8114
+ if (!error) reg.complete(agentId, { proposalCount: Array.isArray(proposals) ? proposals.length : 0 });
8115
+ else reg.fail(agentId, 1, error);
8116
+ } catch (_) {
8117
+ }
8118
+ }
6718
8119
  try {
6719
- fs.appendFileSync(activityFile, JSON.stringify(doneEntry) + "\n");
8120
+ fs.appendFileSync(activityFile, JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), tool: "Task", phase: error ? "error" : "done", agent: "Onboard", desc: error ? `Onboard proposals failed: ${error}` : `Onboard: ${Array.isArray(proposals) ? proposals.length : 0} proposals` }) + "\n");
6720
8121
  } catch (_) {
6721
8122
  }
8123
+ }).catch((err) => {
8124
+ if (agentId) {
8125
+ try {
8126
+ getAgentRegistry(cwd).fail(agentId, 1, String(err));
8127
+ } catch (_) {
8128
+ }
8129
+ }
6722
8130
  });
6723
- const entry = { ts: (/* @__PURE__ */ new Date()).toISOString(), tool: "Task", phase: "start", agent: "Refine", desc: `Analyzing ${nodeId.toUpperCase()} for improvements` };
6724
- fs.appendFileSync(activityFile, JSON.stringify(entry) + "\n");
6725
- sendJson(res, 202, { ok: true, sessionId });
8131
+ try {
8132
+ fs.appendFileSync(activityFile, JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), tool: "Task", phase: "start", agent: "Onboard", desc: "Generating declaration proposals" }) + "\n");
8133
+ } catch (_) {
8134
+ }
8135
+ sendJson(res, 202, { ok: true });
6726
8136
  } catch (err) {
6727
8137
  sendJson(res, 500, { error: String(err) });
6728
8138
  }
6729
8139
  }
6730
- function handleRefineStop(res) {
6731
- if (!refineSession) {
6732
- sendJson(res, 404, { error: "No refine session running" });
6733
- return;
6734
- }
6735
- try {
6736
- refineSession.proc.kill();
6737
- } catch (_) {
6738
- }
6739
- refineSession = null;
6740
- sendJson(res, 200, { ok: true });
6741
- }
6742
- async function handleRefineAccept(req, res, cwd) {
8140
+ async function handleOnboardApprove(req, res, cwd) {
6743
8141
  try {
6744
8142
  const body = await readJsonBody(req);
6745
- const { nodeId, title, statement } = body;
6746
- if (!nodeId) {
6747
- sendJson(res, 400, { error: "Missing nodeId" });
8143
+ const { title, statement } = body;
8144
+ if (!title || !statement) {
8145
+ sendJson(res, 400, { error: "Missing title or statement" });
6748
8146
  return;
6749
8147
  }
6750
- const id = nodeId.toUpperCase();
6751
- const prefix = id.split("-")[0];
6752
- const planningDir = path.join(cwd, ".planning");
6753
- if (prefix === "D") {
6754
- const futurePath = path.join(planningDir, "FUTURE.md");
6755
- const content = fs.readFileSync(futurePath, "utf-8");
6756
- const declarations = parseFutureFile(content);
6757
- const decl = declarations.find((d) => d.id === id);
6758
- if (decl) {
6759
- if (title) decl.title = title;
6760
- if (statement) decl.statement = statement;
6761
- const projectNameMatch = content.match(/^# Future:\\s*(.+)/m);
6762
- const projectName = projectNameMatch ? projectNameMatch[1].trim() : "Project";
6763
- fs.writeFileSync(futurePath, writeFutureFile(declarations, projectName), "utf-8");
6764
- }
6765
- } else if (prefix === "M") {
6766
- const msPath = path.join(planningDir, "MILESTONES.md");
6767
- const content = fs.readFileSync(msPath, "utf-8");
6768
- const { milestones } = parseMilestonesFile(content);
6769
- const mile = milestones.find((m) => m.id === id);
6770
- if (mile && title) {
6771
- mile.title = title;
6772
- const projectNameMatch = content.match(/^# Milestones:\\s*(.+)/m);
6773
- const projectName = projectNameMatch ? projectNameMatch[1].trim() : "Project";
6774
- fs.writeFileSync(msPath, writeMilestonesFile(milestones, projectName), "utf-8");
8148
+ const result = runAddDeclaration2(cwd, ["--title", title, "--statement", statement]);
8149
+ if ("error" in result) {
8150
+ sendJson(res, 400, { error: result.error });
8151
+ return;
8152
+ }
8153
+ setReviewState(cwd, result.id, "approved");
8154
+ broadcastChange();
8155
+ if (onboardSession) {
8156
+ onboardSession.phase = "approving";
8157
+ onboardSession.approveIndex = (onboardSession.approveIndex || 0) + 1;
8158
+ persistOnboardSession(cwd);
8159
+ }
8160
+ try {
8161
+ const graph = runLoadGraph2(cwd);
8162
+ if (!("error" in graph)) {
8163
+ const declarations = graph.declarations.map((d) => ({
8164
+ id: d.id,
8165
+ statement: d.statement,
8166
+ milestones: d.milestones || []
8167
+ }));
8168
+ const dr = getDerivationRunner(cwd);
8169
+ dr.derive(result.id, declarations);
6775
8170
  }
8171
+ } catch (_) {
6776
8172
  }
6777
- const activityFile = path.join(cwd, ".planning", "activity.jsonl");
6778
- const entry = { ts: (/* @__PURE__ */ new Date()).toISOString(), tool: "Review", phase: "end", desc: `Refined ${id}`, nodeId: id, reviewState: "approved" };
6779
- fs.appendFileSync(activityFile, JSON.stringify(entry) + "\n");
6780
- sendJson(res, 200, { ok: true });
8173
+ sendJson(res, 201, result);
6781
8174
  } catch (err) {
6782
8175
  sendJson(res, 500, { error: String(err) });
6783
8176
  }
6784
8177
  }
8178
+ function handleOnboardCancel(req, res, cwd) {
8179
+ if (onboardSession && onboardSession.abortController) {
8180
+ onboardSession.abortController.abort();
8181
+ }
8182
+ onboardSession = null;
8183
+ persistOnboardSession(cwd);
8184
+ sendJson(res, 200, { ok: true });
8185
+ }
6785
8186
  function handleArchiveNode(res, cwd, nodeId) {
6786
8187
  const id = nodeId.toUpperCase();
6787
8188
  const prefix = id.split("-")[0];
@@ -7149,13 +8550,17 @@ data: ${JSON.stringify({ reason: "delete", nodeId: id })}
7149
8550
  return;
7150
8551
  }
7151
8552
  if (urlPath === "/api/milestones/derive/stop") {
7152
- handleDeriveStop(res, cwd);
8553
+ handleDeriveStop(req, res, cwd);
7153
8554
  return;
7154
8555
  }
7155
8556
  if (urlPath === "/api/milestones/derive/accept") {
7156
8557
  handleDeriveAccept(req, res, cwd);
7157
8558
  return;
7158
8559
  }
8560
+ if (urlPath === "/api/declarations/derive-all") {
8561
+ handleDeriveAll(res, cwd);
8562
+ return;
8563
+ }
7159
8564
  const actionDeriveMatch = urlPath.match(/^\/api\/milestones\/([^/]+)\/actions\/derive$/);
7160
8565
  if (actionDeriveMatch) {
7161
8566
  handleActionDerive(res, cwd, actionDeriveMatch[1]);
@@ -7263,6 +8668,42 @@ data: ${JSON.stringify({ reason: "delete", nodeId: id })}
7263
8668
  handleRefineAccept(req, res, cwd);
7264
8669
  return;
7265
8670
  }
8671
+ const discussMatch = urlPath.match(/^\/api\/node\/([^/]+)\/discuss$/);
8672
+ if (discussMatch) {
8673
+ handleDiscuss(req, res, cwd, discussMatch[1]);
8674
+ return;
8675
+ }
8676
+ const discussAnswerMatch = urlPath.match(/^\/api\/node\/([^/]+)\/discuss\/answer$/);
8677
+ if (discussAnswerMatch) {
8678
+ handleDiscussAnswer(req, res, cwd, discussAnswerMatch[1]);
8679
+ return;
8680
+ }
8681
+ if (urlPath === "/api/command") {
8682
+ handleCommand(req, res, cwd);
8683
+ return;
8684
+ }
8685
+ if (urlPath === "/api/onboard/complete") {
8686
+ onboardSession = null;
8687
+ persistOnboardSession(cwd);
8688
+ sendJson(res, 200, { ok: true });
8689
+ return;
8690
+ }
8691
+ if (urlPath === "/api/onboard") {
8692
+ handleOnboard(req, res, cwd);
8693
+ return;
8694
+ }
8695
+ if (urlPath === "/api/onboard/answer") {
8696
+ handleOnboardAnswer(req, res, cwd);
8697
+ return;
8698
+ }
8699
+ if (urlPath === "/api/onboard/approve") {
8700
+ handleOnboardApprove(req, res, cwd);
8701
+ return;
8702
+ }
8703
+ if (urlPath === "/api/onboard/cancel") {
8704
+ handleOnboardCancel(req, res, cwd);
8705
+ return;
8706
+ }
7266
8707
  if (urlPath === "/api/execution-manifest") {
7267
8708
  handleSaveManifest(req, res, cwd);
7268
8709
  return;
@@ -7332,6 +8773,22 @@ data: ${JSON.stringify({ reason: "delete", nodeId: id })}
7332
8773
  sendJson(res, 200, { running: pm.running() });
7333
8774
  return;
7334
8775
  }
8776
+ if (urlPath === "/api/agents") {
8777
+ const reg = getAgentRegistry(cwd);
8778
+ sendJson(res, 200, reg.getAll());
8779
+ return;
8780
+ }
8781
+ const agentMatch = urlPath.match(/^\/api\/agents\/([^/]+)$/);
8782
+ if (agentMatch) {
8783
+ const reg = getAgentRegistry(cwd);
8784
+ const agent = reg.get(decodeURIComponent(agentMatch[1]));
8785
+ if (agent) {
8786
+ sendJson(res, 200, agent);
8787
+ } else {
8788
+ sendJson(res, 404, { error: "Agent not found" });
8789
+ }
8790
+ return;
8791
+ }
7335
8792
  if (urlPath === "/api/play/status") {
7336
8793
  const pr = getPlayRunner(cwd);
7337
8794
  const plr = getPipelineRunner(cwd);
@@ -7350,7 +8807,28 @@ data: ${JSON.stringify({ reason: "delete", nodeId: id })}
7350
8807
  }
7351
8808
  if (urlPath === "/api/derivation/running") {
7352
8809
  const dr = getDerivationRunner(cwd);
7353
- sendJson(res, 200, { running: dr.running() });
8810
+ const runningSessions = dr.running();
8811
+ sendJson(res, 200, {
8812
+ running: runningSessions ? runningSessions[0].sessionId : null,
8813
+ sessions: runningSessions
8814
+ });
8815
+ return;
8816
+ }
8817
+ if (urlPath === "/api/onboard/state") {
8818
+ if (!onboardSession) restoreOnboardSession(cwd);
8819
+ if (onboardSession) {
8820
+ sendJson(res, 200, {
8821
+ active: true,
8822
+ prompt: onboardSession.prompt,
8823
+ phase: onboardSession.phase,
8824
+ questions: onboardSession.questions,
8825
+ proposals: onboardSession.proposals,
8826
+ answers: onboardSession.answers,
8827
+ approveIndex: onboardSession.approveIndex || 0
8828
+ });
8829
+ } else {
8830
+ sendJson(res, 200, { active: false });
8831
+ }
7354
8832
  return;
7355
8833
  }
7356
8834
  const actionDeriveRunningMatch = urlPath.match(/^\/api\/milestones\/([^/]+)\/actions\/derive\/running$/);
@@ -7359,6 +8837,10 @@ data: ${JSON.stringify({ reason: "delete", nodeId: id })}
7359
8837
  sendJson(res, 200, { running: adr.running() });
7360
8838
  return;
7361
8839
  }
8840
+ if (urlPath === "/api/lifecycle") {
8841
+ handleLifecycle(res, cwd);
8842
+ return;
8843
+ }
7362
8844
  if (urlPath === "/api/workflow/state") {
7363
8845
  handleWorkflowState(res, cwd);
7364
8846
  return;
@@ -7432,6 +8914,22 @@ data: ${JSON.stringify({ reason: "delete", nodeId: id })}
7432
8914
  resolve(void 0);
7433
8915
  });
7434
8916
  });
8917
+ const portFilePath = require("path").join(cwd, ".planning", "server.port");
8918
+ require("fs").writeFileSync(portFilePath, String(resolvedPort), "utf-8");
8919
+ const deletePortFile = () => {
8920
+ try {
8921
+ require("fs").unlinkSync(portFilePath);
8922
+ } catch (_) {
8923
+ }
8924
+ };
8925
+ server.on("close", deletePortFile);
8926
+ process.on("exit", deletePortFile);
8927
+ const reg = getAgentRegistry(cwd);
8928
+ const restored = reg.restoreFromDisk();
8929
+ if (restored.interrupted > 0) {
8930
+ process.stderr.write(`[declare] Restored agent state: ${restored.interrupted} agent(s) marked as interrupted from previous run
8931
+ `);
8932
+ }
7435
8933
  const url = `http://localhost:${resolvedPort}`;
7436
8934
  return { server, port: resolvedPort, url };
7437
8935
  }
@@ -7443,6 +8941,7 @@ data: ${JSON.stringify({ reason: "delete", nodeId: id })}
7443
8941
  var require_serve = __commonJS({
7444
8942
  "src/commands/serve.js"(exports2, module2) {
7445
8943
  "use strict";
8944
+ var { spawn } = require("child_process");
7446
8945
  var { startServer } = require_server();
7447
8946
  function parsePortFlag(args) {
7448
8947
  const idx = args.indexOf("--port");
@@ -7453,6 +8952,11 @@ var require_serve = __commonJS({
7453
8952
  async function runServe2(cwd, args) {
7454
8953
  const port = parsePortFlag(args) || parseInt(process.env.PORT || "", 10) || 3847;
7455
8954
  const { server, port: resolvedPort, url } = await startServer(cwd, port);
8955
+ const noOpen = args.includes("--no-open") || process.env.DECLARE_NO_OPEN;
8956
+ if (!noOpen) {
8957
+ const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
8958
+ spawn(opener, [url], { stdio: "ignore", detached: true }).unref();
8959
+ }
7456
8960
  process.on("SIGINT", () => {
7457
8961
  server.close(() => process.exit(0));
7458
8962
  });
@@ -7473,6 +8977,7 @@ var require_open = __commonJS({
7473
8977
  var path = require("path");
7474
8978
  var http = require("http");
7475
8979
  var { spawn } = require("child_process");
8980
+ var { runInit: runInit2 } = require_init();
7476
8981
  function checkServer(port) {
7477
8982
  return new Promise((resolve) => {
7478
8983
  const req = http.get(`http://localhost:${port}/api/graph`, (res) => {
@@ -7496,16 +9001,11 @@ var require_open = __commonJS({
7496
9001
  async function runOpen2(cwd, args) {
7497
9002
  const planningDir = path.join(cwd, ".planning");
7498
9003
  if (!fs.existsSync(planningDir)) {
7499
- console.log("");
7500
- console.log(" No .planning/ directory found in: " + cwd);
7501
- console.log("");
7502
- console.log(" To initialize this project with Declare, run:");
7503
- console.log(" npx declare-cc");
7504
- console.log("");
7505
- console.log(" Or, if declare-cc is already installed globally:");
7506
- console.log(" declare-cc");
7507
- console.log("");
7508
- process.exit(0);
9004
+ console.log("Initializing Declare project in: " + cwd);
9005
+ const result = runInit2(cwd, []);
9006
+ if (result.created && result.created.length > 0) {
9007
+ console.log("Created: " + result.created.join(", "));
9008
+ }
7509
9009
  }
7510
9010
  const portFile = path.join(cwd, ".planning", "server.port");
7511
9011
  const port = fs.existsSync(portFile) ? parseInt(fs.readFileSync(portFile, "utf8").trim(), 10) : 3847;