declare-cc 0.5.9 → 0.6.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.
@@ -215,7 +215,19 @@ var require_future = __commonJS({
215
215
  const status = rawStatus.toUpperCase().trim();
216
216
  const rawMilestones = extractField(lines, "Milestones");
217
217
  const milestones = rawMilestones ? rawMilestones.split(",").map((s) => s.trim()).filter(Boolean) : [];
218
- declarations.push({ id, title: title.trim(), statement, status, milestones });
218
+ const reviewState = extractField(lines, "Review") || "draft";
219
+ const rawRef = extractField(lines, "Ref");
220
+ let ref;
221
+ if (rawRef) {
222
+ ref = {};
223
+ const urlMatch = rawRef.match(/url=(\S+)/);
224
+ const pathMatch = rawRef.match(/path=(\S+)/);
225
+ if (urlMatch) ref.url = urlMatch[1];
226
+ if (pathMatch) ref.path = pathMatch[1];
227
+ }
228
+ const decl = { id, title: title.trim(), statement, status, milestones, reviewState };
229
+ if (ref) decl.ref = ref;
230
+ declarations.push(decl);
219
231
  }
220
232
  return declarations;
221
233
  }
@@ -225,7 +237,14 @@ var require_future = __commonJS({
225
237
  lines.push(`## ${d.id}: ${d.title}`);
226
238
  lines.push(`**Statement:** ${d.statement}`);
227
239
  lines.push(`**Status:** ${d.status}`);
240
+ lines.push(`**Review:** ${d.reviewState || "draft"}`);
228
241
  lines.push(`**Milestones:** ${d.milestones.join(", ")}`);
242
+ if (d.ref && (d.ref.url || d.ref.path)) {
243
+ const parts = [];
244
+ if (d.ref.url) parts.push(`url=${d.ref.url}`);
245
+ if (d.ref.path) parts.push(`path=${d.ref.path}`);
246
+ lines.push(`**Ref:** ${parts.join(" ")}`);
247
+ }
229
248
  lines.push("");
230
249
  }
231
250
  return lines.join("\n");
@@ -302,9 +321,13 @@ var require_milestones = __commonJS({
302
321
  const milestones = milestoneRows.map((row) => ({
303
322
  id: (row["ID"] || "").trim(),
304
323
  title: (row["Title"] || "").trim(),
324
+ description: (row["Description"] || "").trim(),
305
325
  status: (row["Status"] || "PENDING").trim().toUpperCase(),
306
326
  realizes: splitMultiValue(row["Realizes"] || ""),
307
- hasPlan: (row["Plan"] || "").trim().toUpperCase() === "YES"
327
+ hasPlan: (row["Plan"] || "").trim().toUpperCase() === "YES",
328
+ classification: (row["Classification"] || "agent").trim().toLowerCase() === "human" ? "human" : "agent",
329
+ dependsOn: splitMultiValue(row["Depends On"] || ""),
330
+ reviewState: (row["Review"] || "draft").trim() || "draft"
308
331
  })).filter((m) => m.id);
309
332
  return { milestones };
310
333
  }
@@ -315,14 +338,22 @@ var require_milestones = __commonJS({
315
338
  const projectName = maybeProjectName || (typeof projectNameOrActions === "string" ? projectNameOrActions : "Project");
316
339
  const lines = [`# Milestones: ${projectName}`, ""];
317
340
  lines.push("## Milestones", "");
318
- const mHeaders = ["ID", "Title", "Status", "Realizes", "Plan"];
319
- const mRows = milestones.map((m) => [
320
- m.id,
321
- m.title,
322
- m.status,
323
- m.realizes.join(", "),
324
- m.hasPlan ? "YES" : "NO"
325
- ]);
341
+ const hasDescriptions = milestones.some((m) => m.description);
342
+ const hasClassification = milestones.some((m) => m.classification && m.classification !== "agent");
343
+ const hasDependsOn = milestones.some((m) => m.dependsOn && m.dependsOn.length > 0);
344
+ const mHeaders = ["ID", "Title"];
345
+ if (hasDescriptions) mHeaders.push("Description");
346
+ mHeaders.push("Status", "Realizes", "Plan", "Review");
347
+ if (hasClassification) mHeaders.push("Classification");
348
+ if (hasDependsOn) mHeaders.push("Depends On");
349
+ const mRows = milestones.map((m) => {
350
+ const row = [m.id, m.title];
351
+ if (hasDescriptions) row.push(m.description || "");
352
+ row.push(m.status, m.realizes.join(", "), m.hasPlan ? "YES" : "NO", m.reviewState || "draft");
353
+ if (hasClassification) row.push(m.classification || "agent");
354
+ if (hasDependsOn) row.push((m.dependsOn || []).join(", "));
355
+ return row;
356
+ });
326
357
  lines.push(...formatTable(mHeaders, mRows));
327
358
  lines.push("");
328
359
  return lines.join("\n");
@@ -427,7 +458,7 @@ var require_plan = __commonJS({
427
458
  }
428
459
  function parsePlanFile(content) {
429
460
  if (!content || !content.trim()) {
430
- return { milestone: null, realizes: [], status: "PENDING", derived: "", actions: [] };
461
+ return { milestone: null, realizes: [], status: "PENDING", derived: "", produces: "", actions: [] };
431
462
  }
432
463
  const headerMatch = content.match(/^# Plan:\s*(M-\d+)/m);
433
464
  const milestone = headerMatch ? headerMatch[1] : null;
@@ -437,6 +468,7 @@ var require_plan = __commonJS({
437
468
  const realizes = realizesRaw ? realizesRaw.split(",").map((s) => s.trim()).filter(Boolean) : [];
438
469
  const status = (extractField(headerLines, "Status") || "PENDING").toUpperCase();
439
470
  const derived = extractField(headerLines, "Derived") || "";
471
+ const produces = extractField(headerLines, "Produces") || "";
440
472
  const actionSections = content.split(/^### /m).slice(1);
441
473
  const actions = actionSections.map((section) => {
442
474
  const lines = section.trim().split("\n");
@@ -444,31 +476,35 @@ var require_plan = __commonJS({
444
476
  if (!actionHeaderMatch) return null;
445
477
  const [, id, title] = actionHeaderMatch;
446
478
  const actionStatus = (extractField(lines, "Status") || "PENDING").toUpperCase();
447
- const produces = extractField(lines, "Produces") || "";
479
+ const produces2 = extractField(lines, "Produces") || "";
480
+ const reviewState = extractField(lines, "Review") || "draft";
448
481
  const description = lines.slice(1).filter((l) => !l.trim().startsWith("**")).map((l) => l.trim()).filter(Boolean).join("\n");
449
- return { id, title: title.trim(), status: actionStatus, produces, description };
482
+ return { id, title: title.trim(), status: actionStatus, produces: produces2, description, reviewState };
450
483
  }).filter(Boolean);
451
- return { milestone, realizes, status, derived, actions };
484
+ return { milestone, realizes, status, derived, produces, actions };
452
485
  }
453
- function writePlanFile(milestoneId, milestoneTitle, realizes, actions) {
486
+ function writePlanFile(milestoneId, milestoneTitle, realizes, actions, options) {
454
487
  const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
488
+ const milestoneProduces = options && options.produces || "";
455
489
  const lines = [
456
490
  `# Plan: ${milestoneId} -- ${milestoneTitle}`,
457
491
  "",
458
492
  `**Milestone:** ${milestoneId}`,
459
493
  `**Realizes:** ${realizes.join(", ")}`,
460
494
  `**Status:** PENDING`,
461
- `**Derived:** ${today}`,
462
- "",
463
- "## Actions",
464
- ""
495
+ `**Derived:** ${today}`
465
496
  ];
497
+ if (milestoneProduces) {
498
+ lines.push(`**Produces:** ${milestoneProduces}`);
499
+ }
500
+ lines.push("", "## Actions", "");
466
501
  for (const action of actions) {
467
502
  const id = action.id || "A-XX";
468
503
  const status = action.status || "PENDING";
469
504
  const produces = action.produces || "";
470
505
  lines.push(`### ${id}: ${action.title}`);
471
506
  lines.push(`**Status:** ${status}`);
507
+ lines.push(`**Review:** ${action.reviewState || "draft"}`);
472
508
  if (produces) {
473
509
  lines.push(`**Produces:** ${produces}`);
474
510
  }
@@ -560,6 +596,7 @@ var require_engine = __commonJS({
560
596
  return COMPLETED_STATUSES.has(status);
561
597
  }
562
598
  var VALID_TYPES = /* @__PURE__ */ new Set(["declaration", "milestone", "action"]);
599
+ var VALID_REVIEW_STATES = /* @__PURE__ */ new Set(["draft", "in_review", "revision_needed", "approved"]);
563
600
  var VALID_EDGE_DIRECTIONS = {
564
601
  action: "milestone",
565
602
  milestone: "declaration"
@@ -896,6 +933,71 @@ var require_engine = __commonJS({
896
933
  get size() {
897
934
  return this.nodes.size;
898
935
  }
936
+ // ---------------------------------------------------------------------------
937
+ // Wholeness computation (bottom-up integrity model)
938
+ // ---------------------------------------------------------------------------
939
+ /**
940
+ * Compute wholeness state for every node in the graph.
941
+ *
942
+ * Bottom-up, three-pass computation:
943
+ * 1. Actions: whole if isCompleted(status), broken otherwise
944
+ * 2. Milestones: whole if ALL child actions whole, partial if SOME, broken if NONE (or no children)
945
+ * 3. Declarations: whole if ALL child milestones whole, partial if SOME, broken if NONE (or no children)
946
+ *
947
+ * Milestone wholeness is computed from action completion, NOT from the milestone's own status field.
948
+ * Declaration wholeness is computed from milestone wholeness results.
949
+ *
950
+ * @returns {Map<string, string>} Map of node ID to wholeness state ("whole" | "partial" | "broken")
951
+ */
952
+ computeWholeness() {
953
+ const wholeness = /* @__PURE__ */ new Map();
954
+ for (const [id, node] of this.nodes) {
955
+ if (node.type === "action") {
956
+ wholeness.set(id, isCompleted(node.status) ? "whole" : "broken");
957
+ }
958
+ }
959
+ for (const [id, node] of this.nodes) {
960
+ if (node.type === "milestone") {
961
+ const children = this.downEdges.get(id);
962
+ if (!children || children.size === 0) {
963
+ wholeness.set(id, "broken");
964
+ continue;
965
+ }
966
+ let wholeCount = 0;
967
+ for (const childId of children) {
968
+ if (wholeness.get(childId) === "whole") wholeCount++;
969
+ }
970
+ if (wholeCount === children.size) {
971
+ wholeness.set(id, "whole");
972
+ } else if (wholeCount > 0) {
973
+ wholeness.set(id, "partial");
974
+ } else {
975
+ wholeness.set(id, "broken");
976
+ }
977
+ }
978
+ }
979
+ for (const [id, node] of this.nodes) {
980
+ if (node.type === "declaration") {
981
+ const children = this.downEdges.get(id);
982
+ if (!children || children.size === 0) {
983
+ wholeness.set(id, "broken");
984
+ continue;
985
+ }
986
+ let wholeCount = 0;
987
+ for (const childId of children) {
988
+ if (wholeness.get(childId) === "whole") wholeCount++;
989
+ }
990
+ if (wholeCount === children.size) {
991
+ wholeness.set(id, "whole");
992
+ } else if (wholeCount > 0) {
993
+ wholeness.set(id, "partial");
994
+ } else {
995
+ wholeness.set(id, "broken");
996
+ }
997
+ }
998
+ }
999
+ return wholeness;
1000
+ }
899
1001
  /**
900
1002
  * Get graph statistics.
901
1003
  * @returns {{ declarations: number, milestones: number, actions: number, edges: number, byStatus: {PENDING: number, ACTIVE: number, DONE: number} }}
@@ -926,7 +1028,85 @@ var require_engine = __commonJS({
926
1028
  return node ? { id: node.id, type: node.type, title: node.title, status: node.status } : { id: e.node, type: "unknown", title: "", status: "" };
927
1029
  });
928
1030
  }
929
- module2.exports = { DeclareDag, COMPLETED_STATUSES, isCompleted, findOrphans };
1031
+ function computeWholeness(dag) {
1032
+ return dag.computeWholeness();
1033
+ }
1034
+ function computeWorkabilityPath(dag, nodeId) {
1035
+ if (!dag.getNode(nodeId)) {
1036
+ throw new Error(`Node not found: ${nodeId}`);
1037
+ }
1038
+ const wholenessMap = dag.computeWholeness();
1039
+ const nodeWholeness = wholenessMap.get(nodeId);
1040
+ if (nodeWholeness === "whole") {
1041
+ return { nodeId, wholeness: "whole", steps: [] };
1042
+ }
1043
+ const brokenActions = [];
1044
+ const visited = /* @__PURE__ */ new Set();
1045
+ function walkDown(id) {
1046
+ if (visited.has(id)) return;
1047
+ visited.add(id);
1048
+ const node = dag.getNode(id);
1049
+ if (!node) return;
1050
+ if (node.type === "action" && wholenessMap.get(id) === "broken") {
1051
+ brokenActions.push(node);
1052
+ return;
1053
+ }
1054
+ const children = dag.downEdges.get(id);
1055
+ if (!children) return;
1056
+ for (const childId of children) {
1057
+ if (wholenessMap.get(childId) !== "whole") {
1058
+ walkDown(childId);
1059
+ }
1060
+ }
1061
+ }
1062
+ walkDown(nodeId);
1063
+ const steps = brokenActions.map((action) => {
1064
+ const ancestors = /* @__PURE__ */ new Set();
1065
+ const ancestorVisited = /* @__PURE__ */ new Set();
1066
+ function walkUp(id) {
1067
+ if (ancestorVisited.has(id)) return;
1068
+ ancestorVisited.add(id);
1069
+ const upTargets = dag.upEdges.get(id);
1070
+ if (!upTargets) return;
1071
+ for (const targetId of upTargets) {
1072
+ ancestors.add(targetId);
1073
+ walkUp(targetId);
1074
+ }
1075
+ }
1076
+ walkUp(action.id);
1077
+ let nonWholeCount = 0;
1078
+ for (const ancestorId of ancestors) {
1079
+ if (wholenessMap.get(ancestorId) !== "whole") {
1080
+ nonWholeCount++;
1081
+ }
1082
+ }
1083
+ let impact;
1084
+ if (nonWholeCount >= 3) {
1085
+ impact = "high";
1086
+ } else if (nonWholeCount >= 1) {
1087
+ impact = "medium";
1088
+ } else {
1089
+ impact = "low";
1090
+ }
1091
+ const upstream = dag.getUpstream(action.id);
1092
+ const milestone = upstream.find((n) => n.type === "milestone");
1093
+ const milestoneId = milestone ? milestone.id : "";
1094
+ return {
1095
+ actionId: action.id,
1096
+ title: action.title,
1097
+ milestoneId,
1098
+ impact
1099
+ };
1100
+ });
1101
+ const impactOrder = { high: 0, medium: 1, low: 2 };
1102
+ steps.sort((a, b) => {
1103
+ const impactDiff = impactOrder[a.impact] - impactOrder[b.impact];
1104
+ if (impactDiff !== 0) return impactDiff;
1105
+ return a.actionId.localeCompare(b.actionId);
1106
+ });
1107
+ return { nodeId, wholeness: nodeWholeness, steps };
1108
+ }
1109
+ module2.exports = { DeclareDag, VALID_REVIEW_STATES, COMPLETED_STATUSES, isCompleted, findOrphans, computeWholeness, computeWorkabilityPath };
930
1110
  }
931
1111
  });
932
1112
 
@@ -942,26 +1122,31 @@ var require_build_dag = __commonJS({
942
1122
  var { DeclareDag } = require_engine();
943
1123
  function loadActionsFromFolders(planningDir) {
944
1124
  const milestonesDir = join(planningDir, "milestones");
945
- if (!existsSync(milestonesDir)) return [];
1125
+ if (!existsSync(milestonesDir)) return { actions: [], milestoneProduces: {} };
946
1126
  const allActions = [];
1127
+ const milestoneProduces = {};
947
1128
  const entries = readdirSync(milestonesDir, { withFileTypes: true });
948
1129
  for (const entry of entries) {
949
1130
  if (!entry.isDirectory() || entry.name.startsWith("_")) continue;
950
1131
  const planPath = join(milestonesDir, entry.name, "PLAN.md");
951
1132
  if (!existsSync(planPath)) continue;
952
1133
  const content = readFileSync(planPath, "utf-8");
953
- const { milestone, actions } = parsePlanFile(content);
1134
+ const { milestone, actions, produces } = parsePlanFile(content);
1135
+ if (milestone && produces) {
1136
+ milestoneProduces[milestone] = produces;
1137
+ }
954
1138
  for (const action of actions) {
955
1139
  allActions.push({
956
1140
  id: action.id,
957
1141
  title: action.title,
958
1142
  status: action.status,
959
1143
  produces: action.produces,
1144
+ reviewState: action.reviewState || "draft",
960
1145
  causes: milestone ? [milestone] : []
961
1146
  });
962
1147
  }
963
1148
  }
964
- return allActions;
1149
+ return { actions: allActions, milestoneProduces };
965
1150
  }
966
1151
  function buildDagFromDisk(cwd) {
967
1152
  const planningDir = join(cwd, ".planning");
@@ -974,16 +1159,33 @@ var require_build_dag = __commonJS({
974
1159
  const milestonesContent = existsSync(milestonesPath) ? readFileSync(milestonesPath, "utf-8") : "";
975
1160
  const declarations = parseFutureFile(futureContent);
976
1161
  const { milestones } = parseMilestonesFile(milestonesContent);
977
- const actions = loadActionsFromFolders(planningDir);
1162
+ const { actions: allLoadedActions, milestoneProduces } = loadActionsFromFolders(planningDir);
1163
+ const milestoneIds = new Set(milestones.map((m) => m.id.toUpperCase()));
1164
+ const actions = allLoadedActions.filter(
1165
+ (a) => a.causes.some((c) => milestoneIds.has(c.toUpperCase()))
1166
+ );
1167
+ for (const m of milestones) {
1168
+ if (milestoneProduces[m.id]) {
1169
+ m.produces = milestoneProduces[m.id];
1170
+ }
1171
+ }
978
1172
  const dag = new DeclareDag();
979
1173
  for (const d of declarations) {
980
- dag.addNode(d.id, "declaration", d.title, d.status || "PENDING");
1174
+ const meta = { reviewState: d.reviewState || "draft" };
1175
+ if (d.ref) meta.ref = d.ref;
1176
+ dag.addNode(d.id, "declaration", d.title, d.status || "PENDING", meta);
981
1177
  }
982
1178
  for (const m of milestones) {
983
- dag.addNode(m.id, "milestone", m.title, m.status || "PENDING");
1179
+ dag.addNode(m.id, "milestone", m.title, m.status || "PENDING", {
1180
+ description: m.description || "",
1181
+ produces: m.produces || "",
1182
+ classification: m.classification || "agent",
1183
+ dependsOn: m.dependsOn || [],
1184
+ reviewState: m.reviewState || "draft"
1185
+ });
984
1186
  }
985
1187
  for (const a of actions) {
986
- dag.addNode(a.id, "action", a.title, a.status || "PENDING");
1188
+ dag.addNode(a.id, "action", a.title, a.status || "PENDING", { reviewState: a.reviewState || "draft" });
987
1189
  }
988
1190
  for (const m of milestones) {
989
1191
  for (const declId of m.realizes) {
@@ -1409,6 +1611,113 @@ var require_add_declaration = __commonJS({
1409
1611
  }
1410
1612
  });
1411
1613
 
1614
+ // src/commands/update-declaration.js
1615
+ var require_update_declaration = __commonJS({
1616
+ "src/commands/update-declaration.js"(exports2, module2) {
1617
+ "use strict";
1618
+ var { existsSync, readFileSync, writeFileSync } = require("node:fs");
1619
+ var { join } = require("node:path");
1620
+ var { parseFutureFile, writeFutureFile } = require_future();
1621
+ var { commitPlanningDocs: commitPlanningDocs2, loadConfig } = require_commit();
1622
+ var { parseFlag } = require_parse_args();
1623
+ function runUpdateDeclaration2(cwd, args) {
1624
+ const id = parseFlag(args, "id");
1625
+ const title = parseFlag(args, "title");
1626
+ const statement = parseFlag(args, "statement");
1627
+ const status = parseFlag(args, "status");
1628
+ if (!id) {
1629
+ return { error: "Missing required flag: --id" };
1630
+ }
1631
+ if (!title && !statement && !status) {
1632
+ return { error: "At least one of --title, --statement, or --status must be provided" };
1633
+ }
1634
+ const planningDir = join(cwd, ".planning");
1635
+ const futurePath = join(planningDir, "FUTURE.md");
1636
+ if (!existsSync(futurePath)) {
1637
+ return { error: "FUTURE.md not found" };
1638
+ }
1639
+ const futureContent = readFileSync(futurePath, "utf-8");
1640
+ const declarations = parseFutureFile(futureContent);
1641
+ const decl = declarations.find((d) => d.id === id);
1642
+ if (!decl) {
1643
+ return { error: `Declaration not found: ${id}` };
1644
+ }
1645
+ if (title) decl.title = title;
1646
+ if (statement) decl.statement = statement;
1647
+ if (status) decl.status = status.toUpperCase();
1648
+ const headerMatch = futureContent.match(/^# Future: (.+)/m);
1649
+ const projectName = headerMatch ? headerMatch[1].trim() : "Project";
1650
+ const content = writeFutureFile(declarations, projectName);
1651
+ writeFileSync(futurePath, content, "utf-8");
1652
+ const config = loadConfig(cwd);
1653
+ let committed = false;
1654
+ let hash;
1655
+ if (config.commit_docs !== false) {
1656
+ const result = commitPlanningDocs2(
1657
+ cwd,
1658
+ `declare: update ${id} "${decl.title}"`,
1659
+ [".planning/FUTURE.md"]
1660
+ );
1661
+ committed = result.committed;
1662
+ hash = result.hash;
1663
+ }
1664
+ return { id, title: decl.title, statement: decl.statement, status: decl.status, committed, hash };
1665
+ }
1666
+ module2.exports = { runUpdateDeclaration: runUpdateDeclaration2 };
1667
+ }
1668
+ });
1669
+
1670
+ // src/commands/delete-declaration.js
1671
+ var require_delete_declaration = __commonJS({
1672
+ "src/commands/delete-declaration.js"(exports2, module2) {
1673
+ "use strict";
1674
+ var { existsSync, readFileSync, writeFileSync } = require("node:fs");
1675
+ var { join } = require("node:path");
1676
+ var { parseFutureFile, writeFutureFile } = require_future();
1677
+ var { commitPlanningDocs: commitPlanningDocs2, loadConfig } = require_commit();
1678
+ var { parseFlag } = require_parse_args();
1679
+ function runDeleteDeclaration2(cwd, args) {
1680
+ const id = parseFlag(args, "id");
1681
+ if (!id) {
1682
+ return { error: "Missing required flag: --id" };
1683
+ }
1684
+ const planningDir = join(cwd, ".planning");
1685
+ const futurePath = join(planningDir, "FUTURE.md");
1686
+ if (!existsSync(futurePath)) {
1687
+ return { error: "FUTURE.md not found" };
1688
+ }
1689
+ const futureContent = readFileSync(futurePath, "utf-8");
1690
+ const declarations = parseFutureFile(futureContent);
1691
+ const decl = declarations.find((d) => d.id === id);
1692
+ if (!decl) {
1693
+ return { error: `Declaration not found: ${id}` };
1694
+ }
1695
+ if (decl.milestones && decl.milestones.length > 0) {
1696
+ return { error: "Cannot delete declaration with linked milestones. Renegotiate instead." };
1697
+ }
1698
+ const filtered = declarations.filter((d) => d.id !== id);
1699
+ const headerMatch = futureContent.match(/^# Future: (.+)/m);
1700
+ const projectName = headerMatch ? headerMatch[1].trim() : "Project";
1701
+ const content = writeFutureFile(filtered, projectName);
1702
+ writeFileSync(futurePath, content, "utf-8");
1703
+ const config = loadConfig(cwd);
1704
+ let committed = false;
1705
+ let hash;
1706
+ if (config.commit_docs !== false) {
1707
+ const result = commitPlanningDocs2(
1708
+ cwd,
1709
+ `declare: delete ${id} "${decl.title}"`,
1710
+ [".planning/FUTURE.md"]
1711
+ );
1712
+ committed = result.committed;
1713
+ hash = result.hash;
1714
+ }
1715
+ return { id, title: decl.title, deleted: true, committed, hash };
1716
+ }
1717
+ module2.exports = { runDeleteDeclaration: runDeleteDeclaration2 };
1718
+ }
1719
+ });
1720
+
1412
1721
  // src/commands/add-milestone.js
1413
1722
  var require_add_milestone = __commonJS({
1414
1723
  "src/commands/add-milestone.js"(exports2, module2) {
@@ -1601,19 +1910,91 @@ var require_add_action = __commonJS({
1601
1910
  }
1602
1911
  });
1603
1912
 
1913
+ // src/commands/readiness.js
1914
+ var require_readiness = __commonJS({
1915
+ "src/commands/readiness.js"(exports2, module2) {
1916
+ "use strict";
1917
+ var COMPLETED = /* @__PURE__ */ new Set(["DONE", "KEPT", "HONORED"]);
1918
+ function computeReadiness(graph) {
1919
+ const { milestones, actions } = graph;
1920
+ const milestoneStatus = {};
1921
+ for (const m of milestones) {
1922
+ milestoneStatus[m.id] = (m.status || "PENDING").toUpperCase();
1923
+ }
1924
+ const actionCounts = {};
1925
+ for (const m of milestones) {
1926
+ actionCounts[m.id] = { done: 0, total: 0 };
1927
+ }
1928
+ for (const a of actions) {
1929
+ const causes = a.causes || [];
1930
+ for (const mId of causes) {
1931
+ if (!actionCounts[mId]) continue;
1932
+ actionCounts[mId].total++;
1933
+ if (COMPLETED.has((a.status || "").toUpperCase())) {
1934
+ actionCounts[mId].done++;
1935
+ }
1936
+ }
1937
+ }
1938
+ const readiness = {};
1939
+ for (const m of milestones) {
1940
+ const status = milestoneStatus[m.id];
1941
+ const progress = actionCounts[m.id] || { done: 0, total: 0 };
1942
+ if (COMPLETED.has(status)) {
1943
+ readiness[m.id] = { state: "done", blockedBy: [], progress };
1944
+ continue;
1945
+ }
1946
+ const deps = m.dependsOn || [];
1947
+ const blockedBy = [];
1948
+ for (const depId of deps) {
1949
+ const depStatus = milestoneStatus[depId];
1950
+ if (!depStatus || !COMPLETED.has(depStatus)) {
1951
+ blockedBy.push(depId);
1952
+ }
1953
+ }
1954
+ if (blockedBy.length > 0) {
1955
+ readiness[m.id] = { state: "blocked", blockedBy, progress };
1956
+ } else if (progress.total === 0 && !m.hasPlan) {
1957
+ readiness[m.id] = { state: "no-actions", blockedBy: [], progress };
1958
+ } else {
1959
+ readiness[m.id] = { state: "ready", blockedBy: [], progress };
1960
+ }
1961
+ }
1962
+ return readiness;
1963
+ }
1964
+ module2.exports = { computeReadiness };
1965
+ }
1966
+ });
1967
+
1604
1968
  // src/commands/load-graph.js
1605
1969
  var require_load_graph = __commonJS({
1606
1970
  "src/commands/load-graph.js"(exports2, module2) {
1607
1971
  "use strict";
1608
1972
  var { buildDagFromDisk, loadActionsFromFolders } = require_build_dag();
1973
+ var { computeReadiness } = require_readiness();
1609
1974
  function runLoadGraph2(cwd) {
1610
1975
  const graphResult = buildDagFromDisk(cwd);
1611
1976
  if (graphResult.error) return graphResult;
1612
1977
  const { dag, declarations, milestones, actions } = graphResult;
1978
+ const wholeness = dag.computeWholeness();
1979
+ const enrichedMilestones = milestones.map((m) => ({
1980
+ ...m,
1981
+ classification: m.classification || "agent",
1982
+ dependsOn: m.dependsOn || [],
1983
+ wholeness: wholeness.get(m.id) || "broken"
1984
+ }));
1985
+ const enrichedActions = actions.map((a) => ({ ...a, wholeness: wholeness.get(a.id) || "broken" }));
1986
+ const readiness = computeReadiness({
1987
+ milestones: enrichedMilestones,
1988
+ actions: enrichedActions
1989
+ });
1613
1990
  return {
1614
- declarations,
1615
- milestones,
1616
- actions,
1991
+ declarations: declarations.map((d) => ({ ...d, wholeness: wholeness.get(d.id) || "broken" })),
1992
+ milestones: enrichedMilestones.map((m) => ({
1993
+ ...m,
1994
+ readiness: readiness[m.id] || { state: "blocked", blockedBy: [], progress: { done: 0, total: 0 } }
1995
+ })),
1996
+ actions: enrichedActions,
1997
+ readiness,
1617
1998
  stats: dag.stats(),
1618
1999
  validation: dag.validate()
1619
2000
  };
@@ -1676,7 +2057,7 @@ var require_create_plan = __commonJS({
1676
2057
  for (const m of milestones) {
1677
2058
  dag.addNode(m.id, "milestone", m.title, m.status || "PENDING");
1678
2059
  }
1679
- const existingActions = loadActionsFromFolders(planningDir);
2060
+ const { actions: existingActions } = loadActionsFromFolders(planningDir);
1680
2061
  for (const a of existingActions) {
1681
2062
  dag.addNode(a.id, "action", a.title, a.status || "PENDING");
1682
2063
  }
@@ -3018,7 +3399,7 @@ var require_sync_status = __commonJS({
3018
3399
  writeFileSync(planPath, planContent, "utf-8");
3019
3400
  }
3020
3401
  }
3021
- const freshActions = loadActionsFromFolders(planningDir);
3402
+ const { actions: freshActions } = loadActionsFromFolders(planningDir);
3022
3403
  const milestoneActionIds = /* @__PURE__ */ new Map();
3023
3404
  const actionStatusMap = /* @__PURE__ */ new Map();
3024
3405
  for (const a of freshActions) {
@@ -3109,6 +3490,7 @@ var require_get_exec_plan = __commonJS({
3109
3490
  "use strict";
3110
3491
  var { existsSync, readFileSync, readdirSync } = require("node:fs");
3111
3492
  var { join } = require("node:path");
3493
+ var { execSync } = require("node:child_process");
3112
3494
  var { parseFlag } = require_parse_args();
3113
3495
  var { buildDagFromDisk } = require_build_dag();
3114
3496
  var { findMilestoneFolder } = require_milestone_folders();
@@ -3197,6 +3579,48 @@ var require_get_exec_plan = __commonJS({
3197
3579
  );
3198
3580
  return match ? join(milestoneFolder, match) : null;
3199
3581
  }
3582
+ function resolveActionModel(cwd, summaryContent) {
3583
+ try {
3584
+ if (summaryContent) {
3585
+ const fmMatch = summaryContent.match(/^---\n([\s\S]*?)\n---/);
3586
+ if (fmMatch) {
3587
+ const fm = parseFrontmatter(fmMatch[1]);
3588
+ if (fm.model) return String(fm.model);
3589
+ }
3590
+ }
3591
+ const configPath = join(cwd, ".planning", "config.json");
3592
+ if (existsSync(configPath)) {
3593
+ const config = JSON.parse(readFileSync(configPath, "utf-8"));
3594
+ return config.modelAssignment?.executor ?? null;
3595
+ }
3596
+ return null;
3597
+ } catch {
3598
+ return null;
3599
+ }
3600
+ }
3601
+ function getActionCommits(cwd, actionId, milestoneId) {
3602
+ try {
3603
+ const milestonePrefix = milestoneId.match(/^(M-\d+)/);
3604
+ if (!milestonePrefix) return [];
3605
+ const grepPattern = `(${milestonePrefix[1]}-${actionId})`;
3606
+ const output = execSync(
3607
+ `git log --all --oneline --format="%H|%s|%ai" --extended-regexp --grep="${grepPattern}"`,
3608
+ { cwd, encoding: "utf-8", timeout: 5e3 }
3609
+ );
3610
+ const lines = output.trim().split("\n").filter(Boolean);
3611
+ return lines.map((line) => {
3612
+ const [sha, message, date] = line.split("|");
3613
+ return {
3614
+ sha: sha || "",
3615
+ shortSha: (sha || "").slice(0, 7),
3616
+ message: message || "",
3617
+ date: date || ""
3618
+ };
3619
+ });
3620
+ } catch {
3621
+ return [];
3622
+ }
3623
+ }
3200
3624
  function runGetExecPlan2(cwd, args) {
3201
3625
  const actionId = parseFlag(args, "action");
3202
3626
  if (!actionId) {
@@ -3215,6 +3639,7 @@ var require_get_exec_plan = __commonJS({
3215
3639
  if (!milestoneFolder) {
3216
3640
  return { error: `Milestone folder not found for ${milestone.id}` };
3217
3641
  }
3642
+ const commits = getActionCommits(cwd, actionId, milestone.id);
3218
3643
  const execPlanPath = findExecPlan(milestoneFolder, actionId);
3219
3644
  if (!execPlanPath) {
3220
3645
  return {
@@ -3224,7 +3649,9 @@ var require_get_exec_plan = __commonJS({
3224
3649
  milestoneId: milestone.id,
3225
3650
  milestoneTitle: milestone.title,
3226
3651
  execPlan: null,
3227
- summaryExists: false
3652
+ summaryExists: false,
3653
+ model: resolveActionModel(cwd, null),
3654
+ commits
3228
3655
  };
3229
3656
  }
3230
3657
  const raw = readFileSync(execPlanPath, "utf-8");
@@ -3238,12 +3665,14 @@ var require_get_exec_plan = __commonJS({
3238
3665
  const summaryPath = join(milestoneFolder, `${actionId}-SUMMARY.md`);
3239
3666
  const summaryExists = existsSync(summaryPath);
3240
3667
  const summaryContent = summaryExists ? readFileSync(summaryPath, "utf-8") : null;
3668
+ const model = resolveActionModel(cwd, summaryContent);
3241
3669
  return {
3242
3670
  actionId,
3243
3671
  actionTitle: action.title,
3244
3672
  status: action.status,
3245
3673
  milestoneId: milestone.id,
3246
3674
  milestoneTitle: milestone.title,
3675
+ model,
3247
3676
  execPlan: {
3248
3677
  wave: frontmatter.wave ? Number(frontmatter.wave) : null,
3249
3678
  autonomous: frontmatter.autonomous === "true" || frontmatter.autonomous === true,
@@ -3257,7 +3686,8 @@ var require_get_exec_plan = __commonJS({
3257
3686
  verification
3258
3687
  },
3259
3688
  summaryExists,
3260
- summaryContent
3689
+ summaryContent,
3690
+ commits
3261
3691
  };
3262
3692
  }
3263
3693
  module2.exports = { runGetExecPlan: runGetExecPlan2 };
@@ -3825,92 +4255,1815 @@ var require_health_check = __commonJS({
3825
4255
  }
3826
4256
  });
3827
4257
 
3828
- // src/server/index.js
3829
- var require_server = __commonJS({
3830
- "src/server/index.js"(exports2, module2) {
4258
+ // src/server/process-manager.js
4259
+ var require_process_manager = __commonJS({
4260
+ "src/server/process-manager.js"(exports2, module2) {
3831
4261
  "use strict";
3832
- var http = require("node:http");
4262
+ var { spawn } = require("node:child_process");
3833
4263
  var fs = require("node:fs");
3834
- var net = require("node:net");
3835
4264
  var path = require("node:path");
3836
- var { runLoadGraph: runLoadGraph2 } = require_load_graph();
3837
- var { runStatus: runStatus2 } = require_status();
3838
- var { runGetExecPlan: runGetExecPlan2 } = require_get_exec_plan();
3839
- var MIME_TYPES = {
3840
- ".html": "text/html; charset=utf-8",
3841
- ".js": "application/javascript; charset=utf-8",
3842
- ".css": "text/css; charset=utf-8",
3843
- ".json": "application/json; charset=utf-8",
3844
- ".svg": "image/svg+xml",
3845
- ".png": "image/png",
3846
- ".ico": "image/x-icon"
3847
- };
3848
- function getPublicDir(cwd) {
3849
- const installed = path.join(cwd, ".claude", "server", "public");
3850
- if (fs.existsSync(installed)) return installed;
3851
- const bundled = path.join(__dirname, "public");
3852
- if (fs.existsSync(bundled)) return bundled;
3853
- return path.join(cwd, "src", "server", "public");
3854
- }
3855
- function sendJson(res, statusCode, data) {
3856
- const body = JSON.stringify(data, null, 2);
3857
- res.writeHead(statusCode, {
3858
- "Content-Type": "application/json; charset=utf-8",
3859
- "Content-Length": Buffer.byteLength(body),
3860
- "Access-Control-Allow-Origin": "*",
3861
- "Access-Control-Allow-Methods": "GET, OPTIONS",
3862
- "Access-Control-Allow-Headers": "Content-Type"
3863
- });
3864
- res.end(body);
3865
- }
3866
- function sendFile(res, filePath) {
3867
- const ext = path.extname(filePath).toLowerCase();
3868
- const contentType = MIME_TYPES[ext] || "application/octet-stream";
3869
- fs.readFile(filePath, (err, data) => {
3870
- if (err) {
3871
- res.writeHead(404, { "Content-Type": "text/plain" });
3872
- res.end("Not Found");
3873
- return;
3874
- }
3875
- res.writeHead(200, {
3876
- "Content-Type": contentType,
3877
- "Content-Length": data.length
3878
- });
3879
- res.end(data);
3880
- });
3881
- }
3882
- function handleGraph(res, cwd) {
4265
+ var { findMilestoneFolder } = require_milestone_folders();
4266
+ function appendLog(logPath, line) {
4267
+ if (!logPath) return;
3883
4268
  try {
3884
- const graph = runLoadGraph2(cwd);
3885
- if ("error" in graph) {
3886
- sendJson(res, 500, { error: graph.error });
3887
- return;
3888
- }
3889
- sendJson(res, 200, graph);
3890
- } catch (err) {
3891
- sendJson(res, 500, { error: String(err) });
4269
+ fs.appendFileSync(logPath, line + "\n", "utf-8");
4270
+ } catch (_) {
3892
4271
  }
3893
4272
  }
3894
- function handleStatus(res, cwd) {
3895
- try {
3896
- const status = runStatus2(cwd);
3897
- if ("error" in status) {
3898
- sendJson(res, 500, { error: status.error });
3899
- return;
4273
+ function createProcessManager(sseClients, cwd) {
4274
+ const processes = /* @__PURE__ */ new Map();
4275
+ function broadcast(event, data) {
4276
+ const payload = `event: ${event}
4277
+ data: ${JSON.stringify(data)}
4278
+
4279
+ `;
4280
+ for (const client of sseClients) {
4281
+ try {
4282
+ client.write(payload);
4283
+ } catch (_) {
4284
+ sseClients.delete(client);
4285
+ }
3900
4286
  }
3901
- sendJson(res, 200, status);
3902
- } catch (err) {
3903
- sendJson(res, 500, { error: String(err) });
3904
4287
  }
3905
- }
3906
- function handleMilestone(res, cwd, milestoneId) {
3907
- try {
3908
- const graph = runLoadGraph2(cwd);
3909
- if ("error" in graph) {
3910
- sendJson(res, 500, { error: graph.error });
3911
- return;
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) {
4301
+ if (processes.size > 0) {
4302
+ return { error: "busy", status: 409 };
3912
4303
  }
3913
- const normalizedId = milestoneId.toUpperCase();
4304
+ if (processes.has(actionId)) {
4305
+ return { error: "already_running", status: 409 };
4306
+ }
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
+ });
4314
+ const planningDir = path.join(cwd, ".planning");
4315
+ const milestoneFolder = findMilestoneFolder(planningDir, milestoneId);
4316
+ let logPath;
4317
+ if (milestoneFolder) {
4318
+ logPath = path.join(milestoneFolder, "execution.log");
4319
+ } else {
4320
+ process.stderr.write(`[declare] Warning: milestone folder not found for ${milestoneId}, skipping execution log
4321
+ `);
4322
+ }
4323
+ processes.set(actionId, { proc, milestoneId, logPath });
4324
+ appendLog(logPath, `
4325
+ === 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) => {
4333
+ const entry = processes.get(actionId);
4334
+ appendLog(entry?.logPath, `=== END ${actionId} @ ${(/* @__PURE__ */ new Date()).toISOString()} exit=${exitCode ?? -1} ===
4335
+ `);
4336
+ processes.delete(actionId);
4337
+ broadcast("action-complete", { actionId, exitCode: exitCode ?? -1 });
4338
+ });
4339
+ proc.on("error", (_err) => {
4340
+ const entry = processes.get(actionId);
4341
+ appendLog(entry?.logPath, `=== ERROR ${actionId} @ ${(/* @__PURE__ */ new Date()).toISOString()} ===
4342
+ `);
4343
+ processes.delete(actionId);
4344
+ broadcast("action-complete", { actionId, exitCode: -1 });
4345
+ });
4346
+ return { ok: true };
4347
+ }
4348
+ function stop(actionId) {
4349
+ const entry = processes.get(actionId);
4350
+ if (!entry) {
4351
+ return { error: "not_running", status: 404 };
4352
+ }
4353
+ entry.proc.kill("SIGTERM");
4354
+ return { ok: true };
4355
+ }
4356
+ function running() {
4357
+ return [...processes.keys()];
4358
+ }
4359
+ return { execute, stop, running };
4360
+ }
4361
+ module2.exports = { createProcessManager };
4362
+ }
4363
+ });
4364
+
4365
+ // src/server/derivation-runner.js
4366
+ var require_derivation_runner = __commonJS({
4367
+ "src/server/derivation-runner.js"(exports2, module2) {
4368
+ "use strict";
4369
+ var { spawn } = require("node:child_process");
4370
+ function buildPrompt(declarationId, declarations) {
4371
+ let targets;
4372
+ if (declarationId) {
4373
+ targets = declarations.filter((d) => d.id === declarationId);
4374
+ } else {
4375
+ targets = declarations.filter(
4376
+ (d) => !d.milestones || d.milestones.length === 0
4377
+ );
4378
+ }
4379
+ 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;
4381
+ }
4382
+ function createDerivationRunner(sseClients, cwd) {
4383
+ let current = null;
4384
+ function broadcast(event, data) {
4385
+ const payload = `event: ${event}
4386
+ data: ${JSON.stringify(data)}
4387
+
4388
+ `;
4389
+ for (const client of sseClients) {
4390
+ try {
4391
+ client.write(payload);
4392
+ } catch (_) {
4393
+ sseClients.delete(client);
4394
+ }
4395
+ }
4396
+ }
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) {
4408
+ broadcast("derivation-output", {
4409
+ sessionId,
4410
+ text: line,
4411
+ stream: streamName
4412
+ });
4413
+ }
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) => {
4437
+ let milestones = null;
4438
+ if (exitCode === 0) {
4439
+ try {
4440
+ milestones = JSON.parse(stdout.text.trim());
4441
+ } catch (_) {
4442
+ }
4443
+ }
4444
+ current = null;
4445
+ broadcast("derivation-complete", {
4446
+ sessionId,
4447
+ exitCode: exitCode ?? -1,
4448
+ milestones
4449
+ });
4450
+ });
4451
+ proc.on("error", (_err) => {
4452
+ current = null;
4453
+ broadcast("derivation-complete", {
4454
+ sessionId,
4455
+ exitCode: -1,
4456
+ milestones: null
4457
+ });
4458
+ });
4459
+ return { ok: true, sessionId };
4460
+ }
4461
+ function stop() {
4462
+ if (!current) {
4463
+ return { error: "not_running", status: 404 };
4464
+ }
4465
+ current.proc.kill("SIGTERM");
4466
+ return { ok: true };
4467
+ }
4468
+ function running() {
4469
+ return current ? current.sessionId : null;
4470
+ }
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");
4479
+ }
4480
+ module2.exports = { createDerivationRunner };
4481
+ }
4482
+ });
4483
+
4484
+ // src/server/action-derivation-runner.js
4485
+ var require_action_derivation_runner = __commonJS({
4486
+ "src/server/action-derivation-runner.js"(exports2, module2) {
4487
+ "use strict";
4488
+ var { spawn } = require("node:child_process");
4489
+ function buildActionPrompt(milestone, existingActions) {
4490
+ 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
+
4492
+ Milestone:
4493
+ - ${milestone.id}: ${milestone.title} (realizes: ${milestone.realizes.join(", ")})`;
4494
+ if (existingActions.length > 0) {
4495
+ prompt += "\n\nExisting actions for this milestone (do NOT duplicate these):\n";
4496
+ for (const a of existingActions) {
4497
+ prompt += `- ${a.id}: ${a.title}`;
4498
+ if (a.produces) prompt += ` (produces: ${a.produces})`;
4499
+ prompt += "\n";
4500
+ }
4501
+ }
4502
+ return prompt;
4503
+ }
4504
+ function createActionDerivationRunner(sseClients, cwd) {
4505
+ let current = null;
4506
+ function broadcast(event, data) {
4507
+ const payload = `event: ${event}
4508
+ data: ${JSON.stringify(data)}
4509
+
4510
+ `;
4511
+ for (const client of sseClients) {
4512
+ try {
4513
+ client.write(payload);
4514
+ } catch (_) {
4515
+ sseClients.delete(client);
4516
+ }
4517
+ }
4518
+ }
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
+ function derive(milestone, existingActions) {
4539
+ if (current) {
4540
+ return { error: "busy", status: 409 };
4541
+ }
4542
+ const sessionId = `action-deriv-${Date.now()}`;
4543
+ 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"], {
4547
+ 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) => {
4559
+ let actions = null;
4560
+ if (exitCode === 0) {
4561
+ try {
4562
+ actions = JSON.parse(stdout.text.trim());
4563
+ } catch (_) {
4564
+ }
4565
+ }
4566
+ current = null;
4567
+ broadcast("action-derivation-complete", {
4568
+ sessionId,
4569
+ exitCode: exitCode ?? -1,
4570
+ actions
4571
+ });
4572
+ });
4573
+ proc.on("error", (_err) => {
4574
+ current = null;
4575
+ broadcast("action-derivation-complete", {
4576
+ sessionId,
4577
+ exitCode: -1,
4578
+ actions: null
4579
+ });
4580
+ });
4581
+ return { ok: true, sessionId };
4582
+ }
4583
+ function stop() {
4584
+ if (!current) {
4585
+ return { error: "not_running", status: 404 };
4586
+ }
4587
+ current.proc.kill("SIGTERM");
4588
+ return { ok: true };
4589
+ }
4590
+ function running() {
4591
+ return current ? current.sessionId : null;
4592
+ }
4593
+ return { derive, stop, running };
4594
+ }
4595
+ if (require.main === module2) {
4596
+ const runner = createActionDerivationRunner(/* @__PURE__ */ new Set(), ".");
4597
+ console.log("derive:", typeof runner.derive);
4598
+ console.log("stop:", typeof runner.stop);
4599
+ console.log("running:", typeof runner.running);
4600
+ console.log("OK");
4601
+ }
4602
+ module2.exports = { createActionDerivationRunner };
4603
+ }
4604
+ });
4605
+
4606
+ // src/server/revision-runner.js
4607
+ var require_revision_runner = __commonJS({
4608
+ "src/server/revision-runner.js"(exports2, module2) {
4609
+ "use strict";
4610
+ var { spawn } = require("node:child_process");
4611
+ var fs = require("node:fs");
4612
+ var path = require("node:path");
4613
+ function buildRevisionPrompt(artifactContent, annotations) {
4614
+ 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.";
4616
+ }
4617
+ function createRevisionRunner(sseClients, cwd, onComplete) {
4618
+ let current = null;
4619
+ function broadcast(event, data) {
4620
+ const payload = `event: ${event}
4621
+ data: ${JSON.stringify(data)}
4622
+
4623
+ `;
4624
+ for (const client of sseClients) {
4625
+ try {
4626
+ client.write(payload);
4627
+ } catch (_) {
4628
+ sseClients.delete(client);
4629
+ }
4630
+ }
4631
+ }
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
+ function stripMarkdownFencing(text) {
4653
+ const trimmed = text.trim();
4654
+ const fenceMatch = trimmed.match(/^```(?:markdown)?\s*\n([\s\S]*?)\n```\s*$/);
4655
+ if (fenceMatch) {
4656
+ return fenceMatch[1];
4657
+ }
4658
+ return trimmed;
4659
+ }
4660
+ function revise(nodeId, artifactPath, artifactContent, annotations) {
4661
+ if (current) {
4662
+ return { error: "busy", status: 409 };
4663
+ }
4664
+ const sessionId = `revision-${Date.now()}`;
4665
+ const prompt = buildRevisionPrompt(artifactContent, annotations);
4666
+ try {
4667
+ const annPath = path.join(cwd, ".planning", "annotations", nodeId.toUpperCase() + ".json");
4668
+ let round = 0;
4669
+ if (fs.existsSync(annPath)) {
4670
+ try {
4671
+ const annData = JSON.parse(fs.readFileSync(annPath, "utf-8"));
4672
+ round = annData.revisionRound || 0;
4673
+ } catch (_) {
4674
+ }
4675
+ }
4676
+ const versionedPath = artifactPath.replace(".md", "") + ".v" + round + ".md";
4677
+ fs.copyFileSync(artifactPath, versionedPath);
4678
+ } catch (_) {
4679
+ }
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));
4693
+ }
4694
+ proc.on("close", (exitCode) => {
4695
+ const completedNodeId = current ? current.nodeId : nodeId;
4696
+ 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
+ }
4708
+ }
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
+ }
4725
+ }
4726
+ } catch (err) {
4727
+ broadcast("revision-complete", {
4728
+ sessionId,
4729
+ nodeId: completedNodeId,
4730
+ exitCode: -1,
4731
+ error: true
4732
+ });
4733
+ }
4734
+ } else {
4735
+ broadcast("revision-complete", {
4736
+ sessionId,
4737
+ nodeId: completedNodeId,
4738
+ exitCode: exitCode ?? -1,
4739
+ error: true
4740
+ });
4741
+ }
4742
+ });
4743
+ proc.on("error", (_err) => {
4744
+ current = null;
4745
+ broadcast("revision-complete", {
4746
+ sessionId,
4747
+ nodeId,
4748
+ exitCode: -1,
4749
+ error: true
4750
+ });
4751
+ });
4752
+ return { ok: true, sessionId };
4753
+ }
4754
+ function stop() {
4755
+ if (!current) {
4756
+ return { error: "not_running", status: 404 };
4757
+ }
4758
+ current.proc.kill("SIGTERM");
4759
+ return { ok: true };
4760
+ }
4761
+ function running() {
4762
+ return current ? current.sessionId : null;
4763
+ }
4764
+ return { revise, stop, running };
4765
+ }
4766
+ if (require.main === module2) {
4767
+ const runner = createRevisionRunner(/* @__PURE__ */ new Set(), ".", () => {
4768
+ });
4769
+ console.log("revise:", typeof runner.revise);
4770
+ console.log("stop:", typeof runner.stop);
4771
+ console.log("running:", typeof runner.running);
4772
+ console.log("OK");
4773
+ }
4774
+ module2.exports = { createRevisionRunner };
4775
+ }
4776
+ });
4777
+
4778
+ // src/commands/workflow-state.js
4779
+ var require_workflow_state = __commonJS({
4780
+ "src/commands/workflow-state.js"(exports2, module2) {
4781
+ "use strict";
4782
+ var DONE_STATUSES = /* @__PURE__ */ new Set(["DONE", "KEPT", "HONORED"]);
4783
+ var EXECUTING_STATUSES = /* @__PURE__ */ new Set(["EXECUTING", "IN_PROGRESS", "RUNNING"]);
4784
+ function computeWorkflowState(graph, runningActionIds) {
4785
+ const declarations = graph.declarations || [];
4786
+ const milestones = graph.milestones || [];
4787
+ const actions = graph.actions || [];
4788
+ const running = runningActionIds || /* @__PURE__ */ new Set();
4789
+ const actionsDone = actions.filter((a) => DONE_STATUSES.has((a.status || "").toUpperCase())).length;
4790
+ const actionsExecuting = actions.filter(
4791
+ (a) => EXECUTING_STATUSES.has((a.status || "").toUpperCase()) || running.has(a.id)
4792
+ ).length;
4793
+ const totalActions = actions.length;
4794
+ const percentage = totalActions > 0 ? Math.round(actionsDone / totalActions * 100) : 0;
4795
+ const progress = {
4796
+ declarations: declarations.length,
4797
+ milestones: milestones.length,
4798
+ actions: totalActions,
4799
+ actionsDone,
4800
+ actionsExecuting,
4801
+ percentage
4802
+ };
4803
+ if (declarations.length === 0) {
4804
+ return {
4805
+ state: "empty",
4806
+ nextStep: {
4807
+ label: "Create your first declaration",
4808
+ action: "create-declaration"
4809
+ },
4810
+ progress
4811
+ };
4812
+ }
4813
+ if (milestones.length === 0) {
4814
+ return {
4815
+ state: "declarations_only",
4816
+ nextStep: {
4817
+ label: "Derive milestones from declarations",
4818
+ action: "derive-milestones"
4819
+ },
4820
+ progress
4821
+ };
4822
+ }
4823
+ const milestonesWithActions = /* @__PURE__ */ new Set();
4824
+ for (const a of actions) {
4825
+ if (Array.isArray(a.causes)) {
4826
+ for (const mId of a.causes) {
4827
+ milestonesWithActions.add(mId.toUpperCase());
4828
+ }
4829
+ }
4830
+ }
4831
+ const pendingMilestones = milestones.filter((m) => {
4832
+ const mStatus = (m.status || "").toUpperCase();
4833
+ if (DONE_STATUSES.has(mStatus)) return false;
4834
+ return !milestonesWithActions.has(m.id.toUpperCase());
4835
+ });
4836
+ if (pendingMilestones.length > 0) {
4837
+ const target = pendingMilestones[0];
4838
+ return {
4839
+ state: "milestones_pending",
4840
+ nextStep: {
4841
+ label: `Derive actions for ${target.id}`,
4842
+ action: "derive-actions",
4843
+ targetId: target.id
4844
+ },
4845
+ progress
4846
+ };
4847
+ }
4848
+ if (actionsExecuting > 0) {
4849
+ const executingAction = actions.find(
4850
+ (a) => EXECUTING_STATUSES.has((a.status || "").toUpperCase()) || running.has(a.id)
4851
+ );
4852
+ return {
4853
+ state: "executing",
4854
+ nextStep: {
4855
+ label: executingAction ? `Executing ${executingAction.id}` : "Execution in progress",
4856
+ action: "view-execution",
4857
+ targetId: executingAction ? executingAction.id : void 0
4858
+ },
4859
+ progress
4860
+ };
4861
+ }
4862
+ if (totalActions > 0 && actionsDone === totalActions) {
4863
+ return {
4864
+ state: "complete",
4865
+ nextStep: {
4866
+ label: "All actions complete",
4867
+ action: "view-summary"
4868
+ },
4869
+ progress
4870
+ };
4871
+ }
4872
+ const pendingActions = actions.filter((a) => {
4873
+ const s = (a.status || "").toUpperCase();
4874
+ return !DONE_STATUSES.has(s) && !EXECUTING_STATUSES.has(s);
4875
+ });
4876
+ const nextAction = pendingActions[0];
4877
+ return {
4878
+ state: "actions_pending",
4879
+ nextStep: {
4880
+ label: nextAction ? `Execute ${nextAction.id}` : "Plan next actions",
4881
+ action: nextAction ? "execute-action" : "plan-actions",
4882
+ targetId: nextAction ? nextAction.id : void 0
4883
+ },
4884
+ progress
4885
+ };
4886
+ }
4887
+ module2.exports = { computeWorkflowState };
4888
+ }
4889
+ });
4890
+
4891
+ // src/commands/play.js
4892
+ var require_play = __commonJS({
4893
+ "src/commands/play.js"(exports2, module2) {
4894
+ "use strict";
4895
+ var { spawn } = require("node:child_process");
4896
+ var fs = require("node:fs");
4897
+ var path = require("node:path");
4898
+ var { runLoadGraph: runLoadGraph2 } = require_load_graph();
4899
+ var { runGetExecPlan: runGetExecPlan2 } = require_get_exec_plan();
4900
+ var { findMilestoneFolder } = require_milestone_folders();
4901
+ var DONE_STATUSES = /* @__PURE__ */ new Set(["DONE", "KEPT", "HONORED", "RENEGOTIATED"]);
4902
+ function computePlayOrder(graph) {
4903
+ const milestones = graph.milestones || [];
4904
+ const actions = graph.actions || [];
4905
+ const candidates = milestones.filter(
4906
+ (m) => (m.classification || "agent") === "agent" && !DONE_STATUSES.has((m.status || "").toUpperCase())
4907
+ );
4908
+ if (candidates.length === 0) {
4909
+ return { waves: [] };
4910
+ }
4911
+ const candidateIds = new Set(candidates.map((m) => m.id.toUpperCase()));
4912
+ const deps = /* @__PURE__ */ new Map();
4913
+ for (const m of candidates) {
4914
+ const mId = m.id.toUpperCase();
4915
+ const mDeps = (m.dependsOn || []).map((d) => d.toUpperCase()).filter((d) => candidateIds.has(d));
4916
+ deps.set(mId, mDeps);
4917
+ }
4918
+ const waves = [];
4919
+ const placed = /* @__PURE__ */ new Set();
4920
+ while (placed.size < candidates.length) {
4921
+ const wave = [];
4922
+ for (const m of candidates) {
4923
+ const mId = m.id.toUpperCase();
4924
+ if (placed.has(mId)) continue;
4925
+ const mDeps = deps.get(mId) || [];
4926
+ const allDepsMet = mDeps.every(
4927
+ (d) => placed.has(d) || !candidateIds.has(d)
4928
+ );
4929
+ if (!allDepsMet) continue;
4930
+ const milestoneActions = actions.filter(
4931
+ (a) => (a.causes || []).some((c) => c.toUpperCase() === mId) && !DONE_STATUSES.has((a.status || "").toUpperCase())
4932
+ ).map((a) => a.id);
4933
+ if (milestoneActions.length > 0) {
4934
+ wave.push({ milestoneId: m.id, actions: milestoneActions });
4935
+ }
4936
+ }
4937
+ if (wave.length === 0) {
4938
+ let progress = false;
4939
+ for (const m of candidates) {
4940
+ const mId = m.id.toUpperCase();
4941
+ if (placed.has(mId)) continue;
4942
+ const mDeps = deps.get(mId) || [];
4943
+ const allDepsMet = mDeps.every(
4944
+ (d) => placed.has(d) || !candidateIds.has(d)
4945
+ );
4946
+ if (allDepsMet) {
4947
+ placed.add(mId);
4948
+ progress = true;
4949
+ }
4950
+ }
4951
+ if (!progress) break;
4952
+ continue;
4953
+ }
4954
+ waves.push(wave);
4955
+ for (const entry of wave) {
4956
+ placed.add(entry.milestoneId.toUpperCase());
4957
+ }
4958
+ for (const m of candidates) {
4959
+ const mId = m.id.toUpperCase();
4960
+ if (placed.has(mId)) continue;
4961
+ const mDeps = deps.get(mId) || [];
4962
+ if (mDeps.every((d) => placed.has(d) || !candidateIds.has(d))) {
4963
+ placed.add(mId);
4964
+ }
4965
+ }
4966
+ }
4967
+ return { waves };
4968
+ }
4969
+ function appendLog(logPath, line) {
4970
+ if (!logPath) return;
4971
+ try {
4972
+ fs.appendFileSync(logPath, line + "\n", "utf-8");
4973
+ } catch (_) {
4974
+ }
4975
+ }
4976
+ function createPlayRunner(sseClients, cwd) {
4977
+ let isRunning = false;
4978
+ let stopRequested = false;
4979
+ const activeProcesses = /* @__PURE__ */ new Map();
4980
+ let playState = null;
4981
+ function broadcast(event, data) {
4982
+ const payload = `event: ${event}
4983
+ data: ${JSON.stringify(data)}
4984
+
4985
+ `;
4986
+ for (const client of sseClients) {
4987
+ try {
4988
+ client.write(payload);
4989
+ } catch (_) {
4990
+ sseClients.delete(client);
4991
+ }
4992
+ }
4993
+ }
4994
+ function executeAction(actionId, milestoneId) {
4995
+ return new Promise((resolve) => {
4996
+ const prompt = `Run /declare:execute ${milestoneId} for action ${actionId} only. Do not ask questions, execute autonomously.`;
4997
+ const proc = spawn("claude", ["-p", prompt, "--no-input"], {
4998
+ cwd,
4999
+ env: { ...process.env, FORCE_COLOR: "0" }
5000
+ });
5001
+ activeProcesses.set(actionId, proc);
5002
+ const planningDir = path.join(cwd, ".planning");
5003
+ const milestoneFolder = findMilestoneFolder(planningDir, milestoneId);
5004
+ const logPath = milestoneFolder ? path.join(milestoneFolder, "execution.log") : void 0;
5005
+ appendLog(logPath, `
5006
+ === START ${actionId} (play) @ ${(/* @__PURE__ */ new Date()).toISOString()} ===`);
5007
+ let stdoutBuf = "";
5008
+ let stderrBuf = "";
5009
+ if (proc.stdout) {
5010
+ proc.stdout.on("data", (chunk) => {
5011
+ stdoutBuf += chunk.toString();
5012
+ const lines = stdoutBuf.split("\n");
5013
+ stdoutBuf = lines.pop() || "";
5014
+ for (const line of lines) {
5015
+ broadcast("action-output", { actionId, text: line, stream: "stdout" });
5016
+ appendLog(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] [${actionId}] [stdout] ${line}`);
5017
+ }
5018
+ });
5019
+ }
5020
+ if (proc.stderr) {
5021
+ proc.stderr.on("data", (chunk) => {
5022
+ stderrBuf += chunk.toString();
5023
+ const lines = stderrBuf.split("\n");
5024
+ stderrBuf = lines.pop() || "";
5025
+ for (const line of lines) {
5026
+ broadcast("action-output", { actionId, text: line, stream: "stderr" });
5027
+ appendLog(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] [${actionId}] [stderr] ${line}`);
5028
+ }
5029
+ });
5030
+ }
5031
+ proc.on("close", (exitCode) => {
5032
+ const code = exitCode ?? -1;
5033
+ appendLog(logPath, `=== END ${actionId} (play) @ ${(/* @__PURE__ */ new Date()).toISOString()} exit=${code} ===
5034
+ `);
5035
+ activeProcesses.delete(actionId);
5036
+ broadcast("action-complete", { actionId, exitCode: code });
5037
+ resolve({ actionId, exitCode: code });
5038
+ });
5039
+ proc.on("error", (_err) => {
5040
+ appendLog(logPath, `=== ERROR ${actionId} (play) @ ${(/* @__PURE__ */ new Date()).toISOString()} ===
5041
+ `);
5042
+ activeProcesses.delete(actionId);
5043
+ broadcast("action-complete", { actionId, exitCode: -1 });
5044
+ resolve({ actionId, exitCode: -1 });
5045
+ });
5046
+ });
5047
+ }
5048
+ function start() {
5049
+ if (isRunning) {
5050
+ return { error: "Play is already running" };
5051
+ }
5052
+ const graph = runLoadGraph2(cwd);
5053
+ if ("error" in graph) {
5054
+ return { error: graph.error };
5055
+ }
5056
+ let waves;
5057
+ const manifest = loadManifest(cwd);
5058
+ if (manifest) {
5059
+ const actionStatusMap = /* @__PURE__ */ new Map();
5060
+ for (const a of graph.actions || []) {
5061
+ actionStatusMap.set(a.id.toUpperCase(), (a.status || "").toUpperCase());
5062
+ }
5063
+ waves = manifest.waves.map((w) => {
5064
+ const milestones = w.milestones.map((m) => {
5065
+ const pendingActions = m.actions.filter((actionId) => {
5066
+ const status2 = actionStatusMap.get(actionId.toUpperCase()) || "";
5067
+ return !DONE_STATUSES.has(status2);
5068
+ });
5069
+ return pendingActions.length > 0 ? { milestoneId: m.id, actions: pendingActions } : null;
5070
+ }).filter(Boolean);
5071
+ return milestones.length > 0 ? milestones : null;
5072
+ }).filter(Boolean);
5073
+ } else {
5074
+ waves = computePlayOrder(graph).waves;
5075
+ }
5076
+ if (waves.length === 0) {
5077
+ return { error: "No ready agent milestones with pending actions" };
5078
+ }
5079
+ const allActions = [];
5080
+ for (const wave of waves) {
5081
+ for (const entry of wave) {
5082
+ for (const actionId of entry.actions) {
5083
+ const action = (graph.actions || []).find((a) => a.id.toUpperCase() === actionId.toUpperCase());
5084
+ if (action) allActions.push(action);
5085
+ }
5086
+ }
5087
+ }
5088
+ const unapprovedActions = allActions.filter((a) => a.reviewState !== "approved");
5089
+ if (unapprovedActions.length > 0) {
5090
+ return {
5091
+ error: "Cannot play: unapproved actions exist",
5092
+ unapproved: unapprovedActions.map((a) => ({ id: a.id, title: a.title, reviewState: a.reviewState || "draft" }))
5093
+ };
5094
+ }
5095
+ isRunning = true;
5096
+ stopRequested = false;
5097
+ playState = {
5098
+ currentWave: 0,
5099
+ totalWaves: waves.length,
5100
+ waveItems: [],
5101
+ completedActions: [],
5102
+ failedActions: []
5103
+ };
5104
+ broadcast("play-start", {
5105
+ totalWaves: waves.length,
5106
+ waves: waves.map((w, i) => ({
5107
+ wave: i + 1,
5108
+ milestones: w.map((e) => ({ milestoneId: e.milestoneId, actions: e.actions }))
5109
+ }))
5110
+ });
5111
+ (async () => {
5112
+ for (let wi = 0; wi < waves.length; wi++) {
5113
+ if (stopRequested) break;
5114
+ const wave = waves[wi];
5115
+ playState.currentWave = wi + 1;
5116
+ playState.waveItems = wave;
5117
+ broadcast("play-wave-start", {
5118
+ wave: wi + 1,
5119
+ totalWaves: waves.length,
5120
+ milestones: wave.map((e) => ({ milestoneId: e.milestoneId, actions: e.actions }))
5121
+ });
5122
+ const promises = [];
5123
+ for (const entry of wave) {
5124
+ for (const actionId of entry.actions) {
5125
+ if (stopRequested) break;
5126
+ promises.push(executeAction(actionId, entry.milestoneId));
5127
+ }
5128
+ if (stopRequested) break;
5129
+ }
5130
+ const results = await Promise.all(promises);
5131
+ for (const r of results) {
5132
+ if (r.exitCode === 0) {
5133
+ playState.completedActions.push(r.actionId);
5134
+ } else {
5135
+ playState.failedActions.push(r.actionId);
5136
+ }
5137
+ }
5138
+ broadcast("play-wave-complete", {
5139
+ wave: wi + 1,
5140
+ totalWaves: waves.length,
5141
+ completed: results.filter((r) => r.exitCode === 0).map((r) => r.actionId),
5142
+ failed: results.filter((r) => r.exitCode !== 0).map((r) => r.actionId)
5143
+ });
5144
+ }
5145
+ const finalState = { ...playState };
5146
+ isRunning = false;
5147
+ playState = null;
5148
+ broadcast("play-complete", {
5149
+ completed: finalState.completedActions,
5150
+ failed: finalState.failedActions,
5151
+ stopped: stopRequested
5152
+ });
5153
+ stopRequested = false;
5154
+ })().catch((_err) => {
5155
+ isRunning = false;
5156
+ playState = null;
5157
+ broadcast("play-complete", { completed: [], failed: [], stopped: false, error: String(_err) });
5158
+ });
5159
+ return { ok: true, waves: waves.length };
5160
+ }
5161
+ function stop() {
5162
+ if (!isRunning) {
5163
+ return { error: "Play is not running" };
5164
+ }
5165
+ stopRequested = true;
5166
+ for (const [, proc] of activeProcesses) {
5167
+ try {
5168
+ proc.kill("SIGTERM");
5169
+ } catch (_) {
5170
+ }
5171
+ }
5172
+ return { ok: true };
5173
+ }
5174
+ function running() {
5175
+ return isRunning;
5176
+ }
5177
+ function status() {
5178
+ if (!playState) return null;
5179
+ return {
5180
+ running: isRunning,
5181
+ currentWave: playState.currentWave,
5182
+ totalWaves: playState.totalWaves,
5183
+ activeActions: [...activeProcesses.keys()],
5184
+ completedActions: playState.completedActions,
5185
+ failedActions: playState.failedActions
5186
+ };
5187
+ }
5188
+ return { start, stop, running, status };
5189
+ }
5190
+ function loadManifest(cwd) {
5191
+ const manifestPath = path.join(cwd, ".planning", "execution-manifest.json");
5192
+ try {
5193
+ const raw = fs.readFileSync(manifestPath, "utf8");
5194
+ const data = JSON.parse(raw);
5195
+ if (data && Array.isArray(data.waves) && data.waves.length > 0) {
5196
+ return data;
5197
+ }
5198
+ return null;
5199
+ } catch (_) {
5200
+ return null;
5201
+ }
5202
+ }
5203
+ module2.exports = { computePlayOrder, createPlayRunner, loadManifest };
5204
+ }
5205
+ });
5206
+
5207
+ // src/server/pipeline-runner.js
5208
+ var require_pipeline_runner = __commonJS({
5209
+ "src/server/pipeline-runner.js"(exports2, module2) {
5210
+ "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 (_) {
5222
+ }
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 (_) {
5235
+ }
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
+ `;
5254
+ }
5255
+ const report = `# Execution Report
5256
+
5257
+ **Status:** ${overallStatus}
5258
+ **Started:** ${startedAt}
5259
+ **Completed:** ${completedAt}
5260
+ **Duration:** ${totalMin}m ${totalSec}s
5261
+ **Commits:** ${startSha}..${endSha}
5262
+
5263
+ ## Results
5264
+
5265
+ | # | Action | Milestone | Status | Duration | Retried |
5266
+ |---|--------|-----------|--------|----------|---------|
5267
+ ${rows}
5268
+ ## Summary
5269
+
5270
+ - **Passed:** ${passed}
5271
+ - **Failed:** ${failed}
5272
+ - **Total:** ${results.length}
5273
+ `;
5274
+ const reportPath = path.join(cwd, ".planning", "execution-report.md");
5275
+ fs.writeFileSync(reportPath, report, "utf8");
5276
+ return reportPath;
5277
+ } catch (_) {
5278
+ return null;
5279
+ }
5280
+ }
5281
+ function createPipelineRunner(sseClients, cwd) {
5282
+ let isRunning = false;
5283
+ let stopRequested = false;
5284
+ const activeProcesses = /* @__PURE__ */ new Map();
5285
+ let results = [];
5286
+ let pipelineState = null;
5287
+ let pausedOnFailure = null;
5288
+ let skipResolve = null;
5289
+ const outputBuffers = {};
5290
+ let totalActionCount = 0;
5291
+ function persistState() {
5292
+ if (!pipelineState) {
5293
+ try {
5294
+ fs.unlinkSync(path.join(cwd, STATE_FILE));
5295
+ } catch (_) {
5296
+ }
5297
+ return;
5298
+ }
5299
+ const state = {
5300
+ running: isRunning,
5301
+ currentWave: pipelineState.currentWave,
5302
+ totalWaves: pipelineState.totalWaves,
5303
+ totalActions: totalActionCount,
5304
+ completedActions: pipelineState.completedActions,
5305
+ failedActions: pipelineState.failedActions,
5306
+ stoppedActions: pipelineState.stoppedActions,
5307
+ activeActions: [...activeProcesses.keys()],
5308
+ outputBuffers,
5309
+ pausedOnFailure,
5310
+ timestamp: Date.now()
5311
+ };
5312
+ try {
5313
+ fs.writeFileSync(path.join(cwd, STATE_FILE), JSON.stringify(state, null, 2));
5314
+ } catch (_) {
5315
+ }
5316
+ }
5317
+ function broadcast(event, data) {
5318
+ const payload = `event: ${event}
5319
+ data: ${JSON.stringify(data)}
5320
+
5321
+ `;
5322
+ for (const client of sseClients) {
5323
+ try {
5324
+ client.write(payload);
5325
+ } catch (_) {
5326
+ sseClients.delete(client);
5327
+ }
5328
+ }
5329
+ }
5330
+ function loadManifest() {
5331
+ const manifestPath = path.join(cwd, ".planning", "execution-manifest.json");
5332
+ if (!fs.existsSync(manifestPath)) {
5333
+ return { error: "Execution manifest not found at .planning/execution-manifest.json" };
5334
+ }
5335
+ try {
5336
+ const raw = fs.readFileSync(manifestPath, "utf8");
5337
+ const data = JSON.parse(raw);
5338
+ if (!data || !Array.isArray(data.waves) || data.waves.length === 0) {
5339
+ return { error: "Execution manifest is malformed: waves must be a non-empty array" };
5340
+ }
5341
+ for (let i = 0; i < data.waves.length; i++) {
5342
+ const wave = data.waves[i];
5343
+ if (!Array.isArray(wave.milestones)) {
5344
+ return { error: `Execution manifest malformed: waves[${i}].milestones must be an array` };
5345
+ }
5346
+ for (let j = 0; j < wave.milestones.length; j++) {
5347
+ const m = wave.milestones[j];
5348
+ if (typeof m.id !== "string" || !m.id) {
5349
+ return { error: `Execution manifest malformed: waves[${i}].milestones[${j}].id must be a non-empty string` };
5350
+ }
5351
+ if (!Array.isArray(m.actions)) {
5352
+ return { error: `Execution manifest malformed: waves[${i}].milestones[${j}].actions must be an array` };
5353
+ }
5354
+ }
5355
+ }
5356
+ return { waves: data.waves };
5357
+ } catch (err) {
5358
+ return { error: `Failed to parse execution manifest: ${String(err)}` };
5359
+ }
5360
+ }
5361
+ 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, `
5377
+ === 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
+ });
5396
+ }
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
+ }
5412
+ }
5413
+ });
5414
+ }
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} ===
5429
+ `);
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
+ });
5434
+ });
5435
+ }
5436
+ function start() {
5437
+ if (isRunning) {
5438
+ return { error: "Pipeline is already running" };
5439
+ }
5440
+ const manifest = loadManifest();
5441
+ if ("error" in manifest) {
5442
+ return { error: manifest.error };
5443
+ }
5444
+ const waves = manifest.waves;
5445
+ isRunning = true;
5446
+ stopRequested = false;
5447
+ results = [];
5448
+ pipelineState = {
5449
+ currentWave: 0,
5450
+ totalWaves: waves.length,
5451
+ completedActions: [],
5452
+ failedActions: [],
5453
+ stoppedActions: [],
5454
+ skippedActions: []
5455
+ };
5456
+ let totalActions = 0;
5457
+ for (const wave of waves) {
5458
+ for (const m of wave.milestones) {
5459
+ totalActions += m.actions.length;
5460
+ }
5461
+ }
5462
+ totalActionCount = totalActions;
5463
+ persistState();
5464
+ broadcast("pipeline-start", {
5465
+ totalWaves: waves.length,
5466
+ totalActions,
5467
+ waves: waves.map((w, i) => ({
5468
+ wave: i + 1,
5469
+ milestones: w.milestones.map((m) => ({ id: m.id, actions: m.actions }))
5470
+ }))
5471
+ });
5472
+ (async () => {
5473
+ const pipelineStartTime = Date.now();
5474
+ let startSha = "unknown";
5475
+ try {
5476
+ startSha = execSync("git rev-parse --short HEAD", { cwd }).toString().trim();
5477
+ } catch (_) {
5478
+ }
5479
+ for (let wi = 0; wi < waves.length; wi++) {
5480
+ if (stopRequested) break;
5481
+ const wave = waves[wi];
5482
+ pipelineState.currentWave = wi + 1;
5483
+ persistState();
5484
+ broadcast("pipeline-wave-start", {
5485
+ wave: wi + 1,
5486
+ totalWaves: waves.length,
5487
+ milestones: wave.milestones.map((m) => ({ id: m.id, actions: m.actions }))
5488
+ });
5489
+ const promises = [];
5490
+ for (const milestone of wave.milestones) {
5491
+ for (const actionId of milestone.actions) {
5492
+ if (stopRequested) break;
5493
+ promises.push(executeAction(actionId, milestone.id));
5494
+ }
5495
+ if (stopRequested) break;
5496
+ }
5497
+ const waveResults = await Promise.all(promises);
5498
+ for (let ri = 0; ri < waveResults.length; ri++) {
5499
+ const r = waveResults[ri];
5500
+ if (r.exitCode !== 0 && !stopRequested && isTransientFailure(r.exitCode, r.stderrOutput)) {
5501
+ broadcast("action-retry", { actionId: r.actionId, milestoneId: r.milestoneId, attempt: 2, reason: "transient failure detected" });
5502
+ appendLog(
5503
+ findMilestoneFolder(path.join(cwd, ".planning"), r.milestoneId) ? path.join(findMilestoneFolder(path.join(cwd, ".planning"), r.milestoneId), "execution.log") : void 0,
5504
+ `
5505
+ === RETRY ${r.actionId} (attempt 2) ===`
5506
+ );
5507
+ const retryResult = await executeAction(r.actionId, r.milestoneId);
5508
+ retryResult.retried = true;
5509
+ retryResult.attempts = 2;
5510
+ waveResults[ri] = retryResult;
5511
+ }
5512
+ }
5513
+ results.push(...waveResults);
5514
+ for (const r of waveResults) {
5515
+ if (r.exitCode === 0) {
5516
+ pipelineState.completedActions.push(r.actionId);
5517
+ } else {
5518
+ pipelineState.failedActions.push(r.actionId);
5519
+ }
5520
+ }
5521
+ persistState();
5522
+ broadcast("pipeline-wave-complete", {
5523
+ wave: wi + 1,
5524
+ totalWaves: waves.length,
5525
+ completed: waveResults.filter((r) => r.exitCode === 0).map((r) => r.actionId),
5526
+ failed: waveResults.filter((r) => r.exitCode !== 0).map((r) => r.actionId)
5527
+ });
5528
+ const failedInWave = waveResults.filter((r) => r.exitCode !== 0);
5529
+ if (failedInWave.length > 0 && !stopRequested) {
5530
+ const firstFailed = failedInWave[0];
5531
+ pausedOnFailure = { actionId: firstFailed.actionId, exitCode: firstFailed.exitCode, waveIndex: wi };
5532
+ persistState();
5533
+ broadcast("pipeline-paused", {
5534
+ actionId: firstFailed.actionId,
5535
+ exitCode: firstFailed.exitCode,
5536
+ wave: wi + 1,
5537
+ totalWaves: waves.length,
5538
+ failedActions: failedInWave.map((r) => r.actionId)
5539
+ });
5540
+ const decision = await new Promise((resolve) => {
5541
+ skipResolve = resolve;
5542
+ });
5543
+ skipResolve = null;
5544
+ if (decision === "stop") {
5545
+ stopRequested = true;
5546
+ pausedOnFailure = null;
5547
+ break;
5548
+ }
5549
+ for (const f of failedInWave) {
5550
+ pipelineState.skippedActions.push(f.actionId);
5551
+ }
5552
+ pausedOnFailure = null;
5553
+ broadcast("pipeline-resumed", { wave: wi + 1, skipped: failedInWave.map((r) => r.actionId) });
5554
+ }
5555
+ }
5556
+ const finalState = { ...pipelineState };
5557
+ const finalResults = [...results];
5558
+ isRunning = false;
5559
+ if (stopRequested) {
5560
+ for (const actionId of activeProcesses.keys()) {
5561
+ finalState.stoppedActions.push(actionId);
5562
+ }
5563
+ }
5564
+ const reportPath = generateExecutionReport(cwd, finalResults, pipelineStartTime, stopRequested, startSha);
5565
+ if (reportPath) {
5566
+ broadcast("pipeline-report", { path: ".planning/execution-report.md" });
5567
+ }
5568
+ broadcast("pipeline-complete", {
5569
+ completed: finalState.completedActions,
5570
+ failed: finalState.failedActions,
5571
+ stopped: stopRequested ? finalState.stoppedActions : [],
5572
+ results: finalResults,
5573
+ reportPath: reportPath ? ".planning/execution-report.md" : null
5574
+ });
5575
+ stopRequested = false;
5576
+ pausedOnFailure = null;
5577
+ pipelineState = null;
5578
+ persistState();
5579
+ })().catch((_err) => {
5580
+ isRunning = false;
5581
+ pausedOnFailure = null;
5582
+ pipelineState = null;
5583
+ persistState();
5584
+ broadcast("pipeline-complete", {
5585
+ completed: [],
5586
+ failed: [],
5587
+ stopped: [],
5588
+ error: String(_err),
5589
+ results
5590
+ });
5591
+ });
5592
+ return { ok: true, waves: waves.length };
5593
+ }
5594
+ function stop() {
5595
+ if (!isRunning) {
5596
+ return { error: "Pipeline is not running" };
5597
+ }
5598
+ stopRequested = true;
5599
+ if (pausedOnFailure && skipResolve) {
5600
+ skipResolve("stop");
5601
+ skipResolve = null;
5602
+ }
5603
+ for (const [, proc] of activeProcesses) {
5604
+ try {
5605
+ proc.kill("SIGTERM");
5606
+ } catch (_) {
5607
+ }
5608
+ }
5609
+ persistState();
5610
+ return { ok: true };
5611
+ }
5612
+ function running() {
5613
+ return isRunning;
5614
+ }
5615
+ function status() {
5616
+ if (!pipelineState) return null;
5617
+ return {
5618
+ running: isRunning,
5619
+ currentWave: pipelineState.currentWave,
5620
+ totalWaves: pipelineState.totalWaves,
5621
+ activeActions: [...activeProcesses.keys()],
5622
+ completedActions: pipelineState.completedActions,
5623
+ failedActions: pipelineState.failedActions,
5624
+ results
5625
+ };
5626
+ }
5627
+ function skip() {
5628
+ if (!pausedOnFailure) {
5629
+ return { error: "Pipeline is not paused" };
5630
+ }
5631
+ if (skipResolve) {
5632
+ skipResolve("skip");
5633
+ skipResolve = null;
5634
+ }
5635
+ return { ok: true };
5636
+ }
5637
+ function paused() {
5638
+ return pausedOnFailure;
5639
+ }
5640
+ function getFullState() {
5641
+ if (!isRunning && !pausedOnFailure) return null;
5642
+ return {
5643
+ running: isRunning,
5644
+ currentWave: pipelineState ? pipelineState.currentWave : 0,
5645
+ totalWaves: pipelineState ? pipelineState.totalWaves : 0,
5646
+ totalActions: totalActionCount,
5647
+ completedActions: pipelineState ? pipelineState.completedActions : [],
5648
+ failedActions: pipelineState ? pipelineState.failedActions : [],
5649
+ activeActions: [...activeProcesses.keys()],
5650
+ outputBuffers,
5651
+ pausedOnFailure
5652
+ };
5653
+ }
5654
+ return { start, stop, skip, running, paused, status, getFullState };
5655
+ }
5656
+ module2.exports = { createPipelineRunner };
5657
+ }
5658
+ });
5659
+
5660
+ // src/server/index.js
5661
+ var require_server = __commonJS({
5662
+ "src/server/index.js"(exports2, module2) {
5663
+ "use strict";
5664
+ var http = require("node:http");
5665
+ var fs = require("node:fs");
5666
+ var net = require("node:net");
5667
+ var path = require("node:path");
5668
+ var { runLoadGraph: runLoadGraph2 } = require_load_graph();
5669
+ var { runStatus: runStatus2 } = require_status();
5670
+ var { runGetExecPlan: runGetExecPlan2 } = require_get_exec_plan();
5671
+ var { runAddDeclaration: runAddDeclaration2 } = require_add_declaration();
5672
+ var { runUpdateDeclaration: runUpdateDeclaration2 } = require_update_declaration();
5673
+ var { runDeleteDeclaration: runDeleteDeclaration2 } = require_delete_declaration();
5674
+ var { createProcessManager } = require_process_manager();
5675
+ var { createDerivationRunner } = require_derivation_runner();
5676
+ var { createActionDerivationRunner } = require_action_derivation_runner();
5677
+ var { createRevisionRunner } = require_revision_runner();
5678
+ var { runAddMilestonesBatch: runAddMilestonesBatch2 } = require_add_milestones_batch();
5679
+ var { buildDagFromDisk } = require_build_dag();
5680
+ var { computeWorkabilityPath, VALID_REVIEW_STATES } = require_engine();
5681
+ var { findMilestoneFolder } = require_milestone_folders();
5682
+ var { parseFutureFile, writeFutureFile } = require_future();
5683
+ var { parsePlanFile, writePlanFile, updateActionStatus } = require_plan();
5684
+ var { parseMilestonesFile, writeMilestonesFile } = require_milestones();
5685
+ var { computeWorkflowState } = require_workflow_state();
5686
+ var { createPlayRunner } = require_play();
5687
+ var { computeReadiness } = require_readiness();
5688
+ var { createPipelineRunner } = require_pipeline_runner();
5689
+ var MIME_TYPES = {
5690
+ ".html": "text/html; charset=utf-8",
5691
+ ".js": "application/javascript; charset=utf-8",
5692
+ ".css": "text/css; charset=utf-8",
5693
+ ".json": "application/json; charset=utf-8",
5694
+ ".svg": "image/svg+xml",
5695
+ ".png": "image/png",
5696
+ ".ico": "image/x-icon"
5697
+ };
5698
+ function getPublicDir(cwd) {
5699
+ const installed = path.join(cwd, ".claude", "server", "public");
5700
+ if (fs.existsSync(installed)) return installed;
5701
+ const bundled = path.join(__dirname, "public");
5702
+ if (fs.existsSync(bundled)) return bundled;
5703
+ return path.join(cwd, "src", "server", "public");
5704
+ }
5705
+ function sendJson(res, statusCode, data) {
5706
+ const body = JSON.stringify(data, null, 2);
5707
+ res.writeHead(statusCode, {
5708
+ "Content-Type": "application/json; charset=utf-8",
5709
+ "Content-Length": Buffer.byteLength(body),
5710
+ "Access-Control-Allow-Origin": "*",
5711
+ "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
5712
+ "Access-Control-Allow-Headers": "Content-Type"
5713
+ });
5714
+ res.end(body);
5715
+ }
5716
+ function readJsonBody(req) {
5717
+ return new Promise((resolve, reject) => {
5718
+ const chunks = [];
5719
+ let size = 0;
5720
+ const MAX_SIZE = 64 * 1024;
5721
+ req.on("data", (chunk) => {
5722
+ size += chunk.length;
5723
+ if (size > MAX_SIZE) {
5724
+ reject(new Error("Request body too large (max 64KB)"));
5725
+ req.destroy();
5726
+ return;
5727
+ }
5728
+ chunks.push(chunk);
5729
+ });
5730
+ req.on("end", () => {
5731
+ try {
5732
+ const raw = Buffer.concat(chunks).toString("utf-8");
5733
+ resolve(JSON.parse(raw));
5734
+ } catch (err) {
5735
+ reject(new Error("Invalid JSON body"));
5736
+ }
5737
+ });
5738
+ req.on("error", reject);
5739
+ });
5740
+ }
5741
+ function sendFile(res, filePath) {
5742
+ const ext = path.extname(filePath).toLowerCase();
5743
+ const contentType = MIME_TYPES[ext] || "application/octet-stream";
5744
+ fs.readFile(filePath, (err, data) => {
5745
+ if (err) {
5746
+ res.writeHead(404, { "Content-Type": "text/plain" });
5747
+ res.end("Not Found");
5748
+ return;
5749
+ }
5750
+ res.writeHead(200, {
5751
+ "Content-Type": contentType,
5752
+ "Content-Length": data.length,
5753
+ "Cache-Control": "no-cache, no-store, must-revalidate"
5754
+ });
5755
+ res.end(data);
5756
+ });
5757
+ }
5758
+ function handleGraph(res, cwd) {
5759
+ try {
5760
+ const graph = runLoadGraph2(cwd);
5761
+ if ("error" in graph) {
5762
+ sendJson(res, 500, { error: graph.error });
5763
+ return;
5764
+ }
5765
+ sendJson(res, 200, graph);
5766
+ } catch (err) {
5767
+ sendJson(res, 500, { error: String(err) });
5768
+ }
5769
+ }
5770
+ function handleStatus(res, cwd) {
5771
+ try {
5772
+ const status = runStatus2(cwd);
5773
+ if ("error" in status) {
5774
+ sendJson(res, 500, { error: status.error });
5775
+ return;
5776
+ }
5777
+ sendJson(res, 200, status);
5778
+ } catch (err) {
5779
+ sendJson(res, 500, { error: String(err) });
5780
+ }
5781
+ }
5782
+ function handleMilestone(res, cwd, milestoneId) {
5783
+ try {
5784
+ const graph = runLoadGraph2(cwd);
5785
+ if ("error" in graph) {
5786
+ sendJson(res, 500, { error: graph.error });
5787
+ return;
5788
+ }
5789
+ const normalizedId = milestoneId.toUpperCase();
5790
+ const milestone = graph.milestones.find(
5791
+ (m) => m.id.toUpperCase() === normalizedId
5792
+ );
5793
+ if (!milestone) {
5794
+ sendJson(res, 404, { error: `Milestone '${milestoneId}' not found` });
5795
+ return;
5796
+ }
5797
+ const milestoneActions = graph.actions.filter((a) => {
5798
+ if (Array.isArray(a.causes)) {
5799
+ return a.causes.some((c) => c.toUpperCase() === normalizedId);
5800
+ }
5801
+ return false;
5802
+ });
5803
+ sendJson(res, 200, {
5804
+ milestone,
5805
+ actions: milestoneActions
5806
+ });
5807
+ } catch (err) {
5808
+ sendJson(res, 500, { error: String(err) });
5809
+ }
5810
+ }
5811
+ function handleMilestoneLog(res, cwd, milestoneId) {
5812
+ const planningDir = path.join(cwd, ".planning");
5813
+ const milestoneFolder = findMilestoneFolder(planningDir, milestoneId);
5814
+ if (!milestoneFolder) {
5815
+ sendJson(res, 404, { error: "Milestone folder not found" });
5816
+ return;
5817
+ }
5818
+ const logPath = path.join(milestoneFolder, "execution.log");
5819
+ fs.readFile(logPath, "utf-8", (err, data) => {
5820
+ if (err) {
5821
+ res.writeHead(200, {
5822
+ "Content-Type": "text/plain; charset=utf-8",
5823
+ "Access-Control-Allow-Origin": "*"
5824
+ });
5825
+ res.end("");
5826
+ return;
5827
+ }
5828
+ res.writeHead(200, {
5829
+ "Content-Type": "text/plain; charset=utf-8",
5830
+ "Content-Length": Buffer.byteLength(data),
5831
+ "Access-Control-Allow-Origin": "*"
5832
+ });
5833
+ res.end(data);
5834
+ });
5835
+ }
5836
+ function handleWorkability(res, cwd, nodeId) {
5837
+ try {
5838
+ const result = buildDagFromDisk(cwd);
5839
+ if ("error" in result && !("dag" in result)) {
5840
+ sendJson(res, 500, { error: result.error });
5841
+ return;
5842
+ }
5843
+ const { dag } = result;
5844
+ const normalizedId = nodeId.toUpperCase();
5845
+ if (!dag.getNode(normalizedId)) {
5846
+ sendJson(res, 404, { error: `Node '${nodeId}' not found` });
5847
+ return;
5848
+ }
5849
+ const path2 = computeWorkabilityPath(dag, normalizedId);
5850
+ sendJson(res, 200, path2);
5851
+ } catch (err) {
5852
+ sendJson(res, 500, { error: String(err) });
5853
+ }
5854
+ }
5855
+ function handleWorkflowState(res, cwd) {
5856
+ try {
5857
+ const graph = runLoadGraph2(cwd);
5858
+ if ("error" in graph) {
5859
+ sendJson(res, 500, { error: graph.error });
5860
+ return;
5861
+ }
5862
+ const pm = getProcessManager(cwd);
5863
+ const runningIds = new Set(pm.running());
5864
+ const result = computeWorkflowState(graph, runningIds);
5865
+ sendJson(res, 200, result);
5866
+ } catch (err) {
5867
+ sendJson(res, 500, { error: String(err) });
5868
+ }
5869
+ }
5870
+ async function handleSaveManifest(req, res, cwd) {
5871
+ try {
5872
+ const body = await readJsonBody(req);
5873
+ if (!body || !Array.isArray(body.waves) || body.waves.length === 0) {
5874
+ sendJson(res, 400, { error: "waves must be a non-empty array" });
5875
+ return;
5876
+ }
5877
+ for (let i = 0; i < body.waves.length; i++) {
5878
+ const wave = body.waves[i];
5879
+ if (!Array.isArray(wave.milestones)) {
5880
+ sendJson(res, 400, { error: `waves[${i}].milestones must be an array` });
5881
+ return;
5882
+ }
5883
+ for (let j = 0; j < wave.milestones.length; j++) {
5884
+ const m = wave.milestones[j];
5885
+ if (typeof m.id !== "string" || !m.id) {
5886
+ sendJson(res, 400, { error: `waves[${i}].milestones[${j}].id must be a non-empty string` });
5887
+ return;
5888
+ }
5889
+ if (!Array.isArray(m.actions)) {
5890
+ sendJson(res, 400, { error: `waves[${i}].milestones[${j}].actions must be an array` });
5891
+ return;
5892
+ }
5893
+ }
5894
+ }
5895
+ const manifest = {
5896
+ waves: body.waves,
5897
+ confirmedAt: (/* @__PURE__ */ new Date()).toISOString()
5898
+ };
5899
+ const manifestPath = path.join(cwd, ".planning", "execution-manifest.json");
5900
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), "utf-8");
5901
+ sendJson(res, 200, { ok: true });
5902
+ } catch (err) {
5903
+ sendJson(res, 400, { error: String(err) });
5904
+ }
5905
+ }
5906
+ function handleGetManifest(res, cwd) {
5907
+ try {
5908
+ const manifestPath = path.join(cwd, ".planning", "execution-manifest.json");
5909
+ if (!fs.existsSync(manifestPath)) {
5910
+ sendJson(res, 404, { error: "No execution manifest found" });
5911
+ return;
5912
+ }
5913
+ const data = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
5914
+ sendJson(res, 200, data);
5915
+ } catch (err) {
5916
+ sendJson(res, 500, { error: String(err) });
5917
+ }
5918
+ }
5919
+ function handleReadiness(res, cwd) {
5920
+ try {
5921
+ const graph = runLoadGraph2(cwd);
5922
+ if ("error" in graph) {
5923
+ sendJson(res, 500, { error: graph.error });
5924
+ return;
5925
+ }
5926
+ sendJson(res, 200, graph.readiness || {});
5927
+ } catch (err) {
5928
+ sendJson(res, 500, { error: String(err) });
5929
+ }
5930
+ }
5931
+ function handleActivity(res, cwd) {
5932
+ const activityFile = path.join(cwd, ".planning", "activity.jsonl");
5933
+ if (!fs.existsSync(activityFile)) {
5934
+ sendJson(res, 200, { events: [] });
5935
+ return;
5936
+ }
5937
+ try {
5938
+ const lines = fs.readFileSync(activityFile, "utf-8").split("\n").filter(Boolean).slice(-100);
5939
+ const events = lines.map((l) => {
5940
+ try {
5941
+ return JSON.parse(l);
5942
+ } catch {
5943
+ return null;
5944
+ }
5945
+ }).filter(Boolean).reverse();
5946
+ sendJson(res, 200, { events });
5947
+ } catch (err) {
5948
+ sendJson(res, 500, { error: String(err) });
5949
+ }
5950
+ }
5951
+ function handleFileContent(req, res, cwd) {
5952
+ try {
5953
+ const parsedUrl = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`);
5954
+ const requestedPath = parsedUrl.searchParams.get("path");
5955
+ if (!requestedPath) {
5956
+ sendJson(res, 400, { error: "Missing 'path' query parameter" });
5957
+ return;
5958
+ }
5959
+ const resolvedPath = path.resolve(cwd, requestedPath);
5960
+ if (resolvedPath !== cwd && !resolvedPath.startsWith(cwd + path.sep)) {
5961
+ sendJson(res, 403, { error: "Forbidden" });
5962
+ return;
5963
+ }
5964
+ if (!fs.existsSync(resolvedPath)) {
5965
+ sendJson(res, 404, { error: "File not found" });
5966
+ return;
5967
+ }
5968
+ if (fs.statSync(resolvedPath).isDirectory()) {
5969
+ sendJson(res, 400, { error: "Path is a directory" });
5970
+ return;
5971
+ }
5972
+ const fileContent = fs.readFileSync(resolvedPath, "utf-8");
5973
+ sendJson(res, 200, { path: requestedPath, content: fileContent });
5974
+ } catch (err) {
5975
+ sendJson(res, 500, { error: String(err) });
5976
+ }
5977
+ }
5978
+ function handleExecuteAction(res, cwd, actionId) {
5979
+ 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" });
5983
+ return;
5984
+ }
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
+ }
5996
+ }
5997
+ const pm = getProcessManager(cwd);
5998
+ const execResult = pm.execute(actionId, result.milestoneId);
5999
+ if (execResult.error) {
6000
+ sendJson(res, execResult.status || 500, { error: execResult.error });
6001
+ return;
6002
+ }
6003
+ sendJson(res, 202, { ok: true, actionId });
6004
+ } catch (err) {
6005
+ sendJson(res, 500, { error: String(err) });
6006
+ }
6007
+ }
6008
+ async function handleDerive(req, res, cwd) {
6009
+ try {
6010
+ const body = await readJsonBody(req);
6011
+ const graph = runLoadGraph2(cwd);
6012
+ if ("error" in graph) {
6013
+ sendJson(res, 500, { error: graph.error });
6014
+ return;
6015
+ }
6016
+ const declarations = graph.declarations.map((d) => ({
6017
+ id: d.id,
6018
+ statement: d.statement,
6019
+ milestones: d.milestones || []
6020
+ }));
6021
+ const dr = getDerivationRunner(cwd);
6022
+ const result = dr.derive(body.declarationId || null, declarations);
6023
+ if (result.error) {
6024
+ sendJson(res, result.status || 500, { error: result.error });
6025
+ return;
6026
+ }
6027
+ sendJson(res, 202, { ok: true, sessionId: result.sessionId });
6028
+ } catch (err) {
6029
+ sendJson(res, 400, { error: String(err) });
6030
+ }
6031
+ }
6032
+ function handleDeriveStop(res, cwd) {
6033
+ 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
+ }
6040
+ }
6041
+ async function handleDeriveAccept(req, res, cwd) {
6042
+ try {
6043
+ const body = await readJsonBody(req);
6044
+ if (!body.milestones || !Array.isArray(body.milestones) || body.milestones.length === 0) {
6045
+ sendJson(res, 400, { error: "Missing or empty milestones array" });
6046
+ return;
6047
+ }
6048
+ const result = runAddMilestonesBatch2(cwd, ["--json", JSON.stringify(body.milestones)]);
6049
+ if ("error" in result) {
6050
+ sendJson(res, 400, { error: result.error });
6051
+ return;
6052
+ }
6053
+ sendJson(res, 200, result);
6054
+ broadcastChange();
6055
+ } catch (err) {
6056
+ sendJson(res, 400, { error: String(err) });
6057
+ }
6058
+ }
6059
+ function handleActionDerive(res, cwd, milestoneId) {
6060
+ try {
6061
+ const graph = runLoadGraph2(cwd);
6062
+ if ("error" in graph) {
6063
+ sendJson(res, 500, { error: graph.error });
6064
+ return;
6065
+ }
6066
+ const normalizedId = milestoneId.toUpperCase();
3914
6067
  const milestone = graph.milestones.find(
3915
6068
  (m) => m.id.toUpperCase() === normalizedId
3916
6069
  );
@@ -3918,79 +6071,850 @@ var require_server = __commonJS({
3918
6071
  sendJson(res, 404, { error: `Milestone '${milestoneId}' not found` });
3919
6072
  return;
3920
6073
  }
3921
- const milestoneActions = graph.actions.filter((a) => {
3922
- if (Array.isArray(a.causes)) {
3923
- return a.causes.some((c) => c.toUpperCase() === normalizedId);
6074
+ const existingActions = graph.actions.filter(
6075
+ (a) => (a.causes || []).some((c) => c.toUpperCase() === normalizedId)
6076
+ );
6077
+ const adr = getActionDerivationRunner(cwd);
6078
+ const result = adr.derive(
6079
+ { id: milestone.id, title: milestone.title, status: milestone.status, realizes: milestone.realizes || [] },
6080
+ existingActions.map((a) => ({ id: a.id, title: a.title, status: a.status, produces: a.produces || "" }))
6081
+ );
6082
+ if (result.error) {
6083
+ sendJson(res, result.status || 500, { error: result.error });
6084
+ return;
6085
+ }
6086
+ sendJson(res, 202, { ok: true, sessionId: result.sessionId });
6087
+ } catch (err) {
6088
+ sendJson(res, 500, { error: String(err) });
6089
+ }
6090
+ }
6091
+ function handleActionDeriveStop(res, cwd) {
6092
+ const adr = getActionDerivationRunner(cwd);
6093
+ const result = adr.stop();
6094
+ if (result.error) {
6095
+ sendJson(res, result.status || 500, { error: result.error });
6096
+ } else {
6097
+ sendJson(res, 200, { ok: true });
6098
+ }
6099
+ }
6100
+ async function handleActionDeriveAccept(req, res, cwd, milestoneId) {
6101
+ try {
6102
+ const body = await readJsonBody(req);
6103
+ if (!body.actions || !Array.isArray(body.actions) || body.actions.length === 0) {
6104
+ sendJson(res, 400, { error: "Missing or empty actions array" });
6105
+ return;
6106
+ }
6107
+ const graph = runLoadGraph2(cwd);
6108
+ if ("error" in graph) {
6109
+ sendJson(res, 500, { error: graph.error });
6110
+ return;
6111
+ }
6112
+ const normalizedId = milestoneId.toUpperCase();
6113
+ const milestone = graph.milestones.find(
6114
+ (m) => m.id.toUpperCase() === normalizedId
6115
+ );
6116
+ if (!milestone) {
6117
+ sendJson(res, 404, { error: `Milestone '${milestoneId}' not found` });
6118
+ return;
6119
+ }
6120
+ const planningDir = path.join(cwd, ".planning");
6121
+ const milestoneFolder = findMilestoneFolder(planningDir, milestone.id);
6122
+ let existingActions = [];
6123
+ let planContent = "";
6124
+ const planPath = milestoneFolder ? path.join(milestoneFolder, "PLAN.md") : null;
6125
+ if (planPath && fs.existsSync(planPath)) {
6126
+ planContent = fs.readFileSync(planPath, "utf-8");
6127
+ const parsed = parsePlanFile(planContent);
6128
+ existingActions = parsed.actions || [];
6129
+ }
6130
+ let maxActionNum = 0;
6131
+ for (const a of graph.actions) {
6132
+ const num = parseInt(a.id.split("-")[1], 10);
6133
+ if (!isNaN(num) && num > maxActionNum) maxActionNum = num;
6134
+ }
6135
+ const newActions = [];
6136
+ for (const input of body.actions) {
6137
+ maxActionNum++;
6138
+ const id = `A-${maxActionNum < 10 ? "0" + maxActionNum : maxActionNum}`;
6139
+ newActions.push({
6140
+ id,
6141
+ title: input.title,
6142
+ status: "PENDING",
6143
+ produces: input.produces || ""
6144
+ });
6145
+ }
6146
+ const allActions = [...existingActions, ...newActions];
6147
+ const { ensureMilestoneFolder } = require_milestone_folders();
6148
+ const folder = ensureMilestoneFolder(planningDir, milestone.id, milestone.title);
6149
+ const targetPlanPath = path.join(folder, "PLAN.md");
6150
+ const realizes = milestone.realizes || [];
6151
+ const output = writePlanFile(milestone.id, milestone.title, realizes, allActions);
6152
+ fs.writeFileSync(targetPlanPath, output, "utf-8");
6153
+ const milestonesPath = path.join(planningDir, "MILESTONES.md");
6154
+ if (fs.existsSync(milestonesPath)) {
6155
+ let milestonesContent = fs.readFileSync(milestonesPath, "utf-8");
6156
+ const rowPattern = new RegExp(`(\\|\\s*${milestone.id}\\s*\\|.*?)\\s*NO\\s*\\|\\s*$`, "m");
6157
+ if (rowPattern.test(milestonesContent)) {
6158
+ milestonesContent = milestonesContent.replace(rowPattern, "$1 YES |");
6159
+ fs.writeFileSync(milestonesPath, milestonesContent, "utf-8");
6160
+ }
6161
+ }
6162
+ sendJson(res, 200, {
6163
+ actions: newActions,
6164
+ milestoneId: milestone.id
6165
+ });
6166
+ broadcastChange();
6167
+ } catch (err) {
6168
+ sendJson(res, 400, { error: String(err) });
6169
+ }
6170
+ }
6171
+ function setReviewState(cwd, nodeId, reviewState) {
6172
+ if (!reviewState || !VALID_REVIEW_STATES.has(reviewState)) {
6173
+ return { error: `Invalid reviewState. Must be one of: ${[...VALID_REVIEW_STATES].join(", ")}`, status: 400 };
6174
+ }
6175
+ const id = nodeId.toUpperCase();
6176
+ const prefix = id.split("-")[0];
6177
+ const planningDir = path.join(cwd, ".planning");
6178
+ if (prefix === "D") {
6179
+ const futurePath = path.join(planningDir, "FUTURE.md");
6180
+ if (!fs.existsSync(futurePath)) return { error: "FUTURE.md not found", status: 404 };
6181
+ const content = fs.readFileSync(futurePath, "utf-8");
6182
+ const declarations = parseFutureFile(content);
6183
+ const decl = declarations.find((d) => d.id === id);
6184
+ if (!decl) return { error: `Declaration ${id} not found`, status: 404 };
6185
+ decl.reviewState = reviewState;
6186
+ const headerMatch = content.match(/^# Future: (.+)/m);
6187
+ const projectName = headerMatch ? headerMatch[1].trim() : "Project";
6188
+ fs.writeFileSync(futurePath, writeFutureFile(declarations, projectName), "utf-8");
6189
+ } else if (prefix === "M") {
6190
+ const milestonesPath = path.join(planningDir, "MILESTONES.md");
6191
+ if (!fs.existsSync(milestonesPath)) return { error: "MILESTONES.md not found", status: 404 };
6192
+ const content = fs.readFileSync(milestonesPath, "utf-8");
6193
+ const { milestones } = parseMilestonesFile(content);
6194
+ const mile = milestones.find((m) => m.id === id);
6195
+ if (!mile) return { error: `Milestone ${id} not found`, status: 404 };
6196
+ mile.reviewState = reviewState;
6197
+ const nameMatch = content.match(/^# Milestones:\s*(.+)/m);
6198
+ const pName = nameMatch ? nameMatch[1].trim() : "Project";
6199
+ fs.writeFileSync(milestonesPath, writeMilestonesFile(milestones, pName), "utf-8");
6200
+ } else if (prefix === "A") {
6201
+ const milestonesDir = path.join(planningDir, "milestones");
6202
+ if (!fs.existsSync(milestonesDir)) return { error: "No milestones directory", status: 404 };
6203
+ let found = false;
6204
+ const entries = fs.readdirSync(milestonesDir, { withFileTypes: true });
6205
+ for (const entry of entries) {
6206
+ if (!entry.isDirectory() || entry.name.startsWith("_")) continue;
6207
+ const planPath = path.join(milestonesDir, entry.name, "PLAN.md");
6208
+ if (!fs.existsSync(planPath)) continue;
6209
+ const content = fs.readFileSync(planPath, "utf-8");
6210
+ const parsed = parsePlanFile(content);
6211
+ const action = parsed.actions.find((a) => a.id === id);
6212
+ if (!action) continue;
6213
+ const lines = content.split("\n");
6214
+ let inSection = false;
6215
+ let patched = false;
6216
+ for (let i = 0; i < lines.length; i++) {
6217
+ if (lines[i].startsWith("### ")) {
6218
+ inSection = lines[i].startsWith(`### ${id}:`);
6219
+ }
6220
+ if (inSection && !patched && /^\*\*Review:\*\*/i.test(lines[i].trim())) {
6221
+ lines[i] = `**Review:** ${reviewState}`;
6222
+ patched = true;
6223
+ break;
6224
+ }
6225
+ if (inSection && !patched && /^\*\*Status:\*\*/i.test(lines[i].trim())) {
6226
+ if (i + 1 < lines.length && /^\*\*Review:\*\*/i.test(lines[i + 1].trim())) {
6227
+ lines[i + 1] = `**Review:** ${reviewState}`;
6228
+ patched = true;
6229
+ } else {
6230
+ lines.splice(i + 1, 0, `**Review:** ${reviewState}`);
6231
+ patched = true;
6232
+ }
6233
+ break;
6234
+ }
6235
+ }
6236
+ if (patched) {
6237
+ fs.writeFileSync(planPath, lines.join("\n"), "utf-8");
6238
+ found = true;
6239
+ }
6240
+ break;
6241
+ }
6242
+ if (!found) return { error: `Action ${id} not found in any PLAN.md`, status: 404 };
6243
+ } else {
6244
+ return { error: `Unknown node type prefix: ${prefix}`, status: 400 };
6245
+ }
6246
+ return { ok: true, id, reviewState };
6247
+ }
6248
+ async function handleUpdateReviewState(req, res, cwd, nodeId) {
6249
+ try {
6250
+ const body = await readJsonBody(req);
6251
+ const result = setReviewState(cwd, nodeId, body.reviewState);
6252
+ if ("error" in result) {
6253
+ sendJson(res, result.status || 500, { error: result.error });
6254
+ return;
6255
+ }
6256
+ try {
6257
+ const activityFile = path.join(cwd, ".planning", "activity.jsonl");
6258
+ const entry = {
6259
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
6260
+ tool: "Review",
6261
+ phase: "end",
6262
+ desc: body.reviewState === "approved" ? `Approved ${nodeId}` : `Revision needed: ${nodeId}`,
6263
+ nodeId,
6264
+ reviewState: body.reviewState
6265
+ };
6266
+ fs.appendFileSync(activityFile, JSON.stringify(entry) + "\n");
6267
+ } catch (_) {
6268
+ }
6269
+ sendJson(res, 200, result);
6270
+ broadcastChange();
6271
+ } catch (err) {
6272
+ sendJson(res, 500, { error: String(err) });
6273
+ }
6274
+ }
6275
+ function getAnnotationsPath(cwd, nodeId) {
6276
+ return path.join(cwd, ".planning", "annotations", `${nodeId.toUpperCase()}.json`);
6277
+ }
6278
+ function readAnnotations(cwd, nodeId) {
6279
+ const filePath = getAnnotationsPath(cwd, nodeId);
6280
+ if (!fs.existsSync(filePath)) {
6281
+ return { nodeId: nodeId.toUpperCase(), annotations: [], revisionRound: 0 };
6282
+ }
6283
+ try {
6284
+ const data = JSON.parse(fs.readFileSync(filePath, "utf-8"));
6285
+ data.revisionRound = data.revisionRound || 0;
6286
+ return data;
6287
+ } catch (_) {
6288
+ return { nodeId: nodeId.toUpperCase(), annotations: [], revisionRound: 0 };
6289
+ }
6290
+ }
6291
+ function writeAnnotations(cwd, nodeId, data) {
6292
+ const filePath = getAnnotationsPath(cwd, nodeId);
6293
+ const dir = path.dirname(filePath);
6294
+ fs.mkdirSync(dir, { recursive: true });
6295
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf-8");
6296
+ }
6297
+ function handleGetAnnotations(res, cwd, nodeId) {
6298
+ try {
6299
+ const data = readAnnotations(cwd, nodeId);
6300
+ sendJson(res, 200, data);
6301
+ } catch (err) {
6302
+ sendJson(res, 500, { error: String(err) });
6303
+ }
6304
+ }
6305
+ async function handleAddAnnotation(req, res, cwd, nodeId) {
6306
+ try {
6307
+ const body = await readJsonBody(req);
6308
+ const { line, text } = body;
6309
+ if (typeof line !== "number" || line < 1) {
6310
+ sendJson(res, 400, { error: "line must be a number >= 1" });
6311
+ return;
6312
+ }
6313
+ if (typeof text !== "string" || text.trim().length === 0) {
6314
+ sendJson(res, 400, { error: "text must be a non-empty string" });
6315
+ return;
6316
+ }
6317
+ const id = "ann-" + Date.now() + "-" + Math.random().toString(36).slice(2, 8);
6318
+ const annotation = {
6319
+ id,
6320
+ line,
6321
+ text: text.trim(),
6322
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
6323
+ resolved: false
6324
+ };
6325
+ const data = readAnnotations(cwd, nodeId);
6326
+ data.annotations.push(annotation);
6327
+ writeAnnotations(cwd, nodeId, data);
6328
+ setReviewState(cwd, nodeId, "revision_needed");
6329
+ broadcastChange();
6330
+ sendJson(res, 201, annotation);
6331
+ } catch (err) {
6332
+ sendJson(res, 400, { error: String(err) });
6333
+ }
6334
+ }
6335
+ function handleDeleteAnnotation(res, cwd, nodeId, annotationId) {
6336
+ try {
6337
+ const data = readAnnotations(cwd, nodeId);
6338
+ const before = data.annotations.length;
6339
+ data.annotations = data.annotations.filter((a) => a.id !== annotationId);
6340
+ if (data.annotations.length === before) {
6341
+ sendJson(res, 404, { error: "Annotation not found" });
6342
+ return;
6343
+ }
6344
+ writeAnnotations(cwd, nodeId, data);
6345
+ broadcastChange();
6346
+ sendJson(res, 200, { ok: true, id: annotationId });
6347
+ } catch (err) {
6348
+ sendJson(res, 500, { error: String(err) });
6349
+ }
6350
+ }
6351
+ function handleIncrementRevisionRound(res, cwd, nodeId) {
6352
+ try {
6353
+ const data = readAnnotations(cwd, nodeId);
6354
+ data.revisionRound = (data.revisionRound || 0) + 1;
6355
+ writeAnnotations(cwd, nodeId, data);
6356
+ broadcastChange();
6357
+ sendJson(res, 200, { ok: true, revisionRound: data.revisionRound });
6358
+ } catch (err) {
6359
+ sendJson(res, 500, { error: String(err) });
6360
+ }
6361
+ }
6362
+ var sseClients = /* @__PURE__ */ new Set();
6363
+ var processManager = null;
6364
+ function getProcessManager(cwd) {
6365
+ if (!processManager) processManager = createProcessManager(sseClients, cwd);
6366
+ return processManager;
6367
+ }
6368
+ var derivationRunner = null;
6369
+ function getDerivationRunner(cwd) {
6370
+ if (!derivationRunner) derivationRunner = createDerivationRunner(sseClients, cwd);
6371
+ return derivationRunner;
6372
+ }
6373
+ var actionDerivationRunner = null;
6374
+ function getActionDerivationRunner(cwd) {
6375
+ if (!actionDerivationRunner) actionDerivationRunner = createActionDerivationRunner(sseClients, cwd);
6376
+ return actionDerivationRunner;
6377
+ }
6378
+ var playRunner = null;
6379
+ function getPlayRunner(cwd) {
6380
+ if (!playRunner) playRunner = createPlayRunner(sseClients, cwd);
6381
+ return playRunner;
6382
+ }
6383
+ var pipelineRunnerInstance = null;
6384
+ function getPipelineRunner(cwd) {
6385
+ if (!pipelineRunnerInstance) pipelineRunnerInstance = createPipelineRunner(sseClients, cwd);
6386
+ return pipelineRunnerInstance;
6387
+ }
6388
+ var revisionRunner = null;
6389
+ function getRevisionRunner(cwd) {
6390
+ if (!revisionRunner) {
6391
+ revisionRunner = createRevisionRunner(sseClients, cwd, (nodeId) => {
6392
+ setReviewState(cwd, nodeId, "in_review");
6393
+ broadcastChange();
6394
+ });
6395
+ }
6396
+ return revisionRunner;
6397
+ }
6398
+ function broadcastChange() {
6399
+ for (const client of sseClients) {
6400
+ try {
6401
+ client.write("event: change\ndata: {}\n\n");
6402
+ } catch (_) {
6403
+ sseClients.delete(client);
6404
+ }
6405
+ }
6406
+ }
6407
+ function watchPlanning(cwd) {
6408
+ const planningDir = path.join(cwd, ".planning");
6409
+ if (!fs.existsSync(planningDir)) return;
6410
+ let graphTimer = null;
6411
+ let activityTimer = null;
6412
+ const activityFile = path.join(planningDir, "activity.jsonl");
6413
+ try {
6414
+ fs.watch(planningDir, { recursive: true }, (_evt, filename) => {
6415
+ if (filename && filename.endsWith("activity.jsonl")) {
6416
+ if (activityTimer) clearTimeout(activityTimer);
6417
+ activityTimer = setTimeout(() => {
6418
+ for (const client of sseClients) {
6419
+ try {
6420
+ client.write("event: activity\ndata: {}\n\n");
6421
+ } catch {
6422
+ sseClients.delete(client);
6423
+ }
6424
+ }
6425
+ activityTimer = null;
6426
+ }, 50);
6427
+ } else {
6428
+ if (graphTimer) clearTimeout(graphTimer);
6429
+ graphTimer = setTimeout(() => {
6430
+ broadcastChange();
6431
+ graphTimer = null;
6432
+ }, 200);
6433
+ }
6434
+ });
6435
+ } catch (_) {
6436
+ }
6437
+ }
6438
+ function handleGetRevisions(res, cwd, nodeId) {
6439
+ try {
6440
+ const id = nodeId.toUpperCase();
6441
+ const prefix = id.split("-")[0];
6442
+ const planningDir = path.join(cwd, ".planning");
6443
+ let artifactPath = null;
6444
+ if (prefix === "D") {
6445
+ artifactPath = path.join(planningDir, "FUTURE.md");
6446
+ } else if (prefix === "M") {
6447
+ const folder = findMilestoneFolder(planningDir, id);
6448
+ if (folder) artifactPath = path.join(folder, "PLAN.md");
6449
+ } else if (prefix === "A") {
6450
+ const graph = runLoadGraph2(cwd);
6451
+ if (!("error" in graph)) {
6452
+ const action = graph.actions.find((a) => a.id.toUpperCase() === id);
6453
+ if (action) {
6454
+ const milestoneId = (action.causes || [])[0];
6455
+ if (milestoneId) {
6456
+ const folder = findMilestoneFolder(planningDir, milestoneId);
6457
+ if (folder) {
6458
+ const aNum = id.replace(/^A-/, "");
6459
+ artifactPath = path.join(folder, `A-${aNum}-EXEC-PLAN.md`);
6460
+ if (!fs.existsSync(artifactPath)) {
6461
+ artifactPath = path.join(folder, "PLAN.md");
6462
+ }
6463
+ }
6464
+ }
6465
+ }
6466
+ }
6467
+ }
6468
+ if (!artifactPath || !fs.existsSync(artifactPath)) {
6469
+ sendJson(res, 404, { error: "Artifact not found for node " + id });
6470
+ return;
6471
+ }
6472
+ const current = fs.readFileSync(artifactPath, "utf-8");
6473
+ const annData = readAnnotations(cwd, id);
6474
+ const revisionRound = annData.revisionRound || 0;
6475
+ let previous = null;
6476
+ if (revisionRound >= 1) {
6477
+ const prevRound = revisionRound - 1;
6478
+ const prevPath = artifactPath.replace(".md", "") + ".v" + prevRound + ".md";
6479
+ if (fs.existsSync(prevPath)) {
6480
+ previous = fs.readFileSync(prevPath, "utf-8");
6481
+ }
6482
+ }
6483
+ sendJson(res, 200, { current, previous, revisionRound });
6484
+ } catch (err) {
6485
+ sendJson(res, 500, { error: String(err) });
6486
+ }
6487
+ }
6488
+ async function handleRevise(req, res, cwd, nodeId) {
6489
+ try {
6490
+ const id = nodeId.toUpperCase();
6491
+ const prefix = id.split("-")[0];
6492
+ const planningDir = path.join(cwd, ".planning");
6493
+ let artifactPath = null;
6494
+ if (prefix === "D") {
6495
+ artifactPath = path.join(planningDir, "FUTURE.md");
6496
+ } else if (prefix === "M") {
6497
+ const folder = findMilestoneFolder(planningDir, id);
6498
+ if (folder) artifactPath = path.join(folder, "PLAN.md");
6499
+ } else if (prefix === "A") {
6500
+ const graph = runLoadGraph2(cwd);
6501
+ if (!("error" in graph)) {
6502
+ const action = graph.actions.find((a) => a.id.toUpperCase() === id);
6503
+ if (action) {
6504
+ const milestoneId = (action.causes || [])[0];
6505
+ if (milestoneId) {
6506
+ const folder = findMilestoneFolder(planningDir, milestoneId);
6507
+ if (folder) {
6508
+ const aNum = id.replace(/^A-/, "");
6509
+ artifactPath = path.join(folder, `A-${aNum}-EXEC-PLAN.md`);
6510
+ if (!fs.existsSync(artifactPath)) {
6511
+ artifactPath = path.join(folder, "PLAN.md");
6512
+ }
6513
+ }
6514
+ }
6515
+ }
6516
+ }
6517
+ }
6518
+ if (!artifactPath || !fs.existsSync(artifactPath)) {
6519
+ sendJson(res, 404, { error: "Artifact not found for node " + id });
6520
+ return;
6521
+ }
6522
+ const artifactContent = fs.readFileSync(artifactPath, "utf-8");
6523
+ const annData = readAnnotations(cwd, id);
6524
+ const annotations = annData.annotations || [];
6525
+ if (annotations.length === 0) {
6526
+ sendJson(res, 400, { error: "no_annotations" });
6527
+ return;
6528
+ }
6529
+ const rr = getRevisionRunner(cwd);
6530
+ const result = rr.revise(id, artifactPath, artifactContent, annotations);
6531
+ if (result.error) {
6532
+ sendJson(res, result.status || 500, { error: result.error });
6533
+ return;
6534
+ }
6535
+ sendJson(res, 202, { ok: true, sessionId: result.sessionId });
6536
+ } catch (err) {
6537
+ sendJson(res, 500, { error: String(err) });
6538
+ }
6539
+ }
6540
+ function handleReviseStop(res, cwd) {
6541
+ const rr = getRevisionRunner(cwd);
6542
+ const result = rr.stop();
6543
+ if (result.error) {
6544
+ sendJson(res, result.status || 500, { error: result.error });
6545
+ } else {
6546
+ sendJson(res, 200, { ok: true });
6547
+ }
6548
+ }
6549
+ var refineSession = null;
6550
+ function buildRefinePrompt(nodeId, graph, mode, userMessage) {
6551
+ const id = nodeId.toUpperCase();
6552
+ const prefix = id.split("-")[0];
6553
+ let nodeContent = "";
6554
+ let parentContent = "";
6555
+ let siblingsContent = "";
6556
+ let nodeType = "";
6557
+ if (prefix === "D") {
6558
+ nodeType = "declaration";
6559
+ const decl = graph.declarations.find((d) => d.id.toUpperCase() === id);
6560
+ if (!decl) return null;
6561
+ nodeContent = `${decl.id}: ${decl.title}
6562
+ Statement: ${decl.statement || decl.title}`;
6563
+ siblingsContent = graph.declarations.filter((d) => d.id !== decl.id).map((d) => `${d.id}: ${d.title}`).join("\n");
6564
+ parentContent = "This is a top-level declaration (no parent).";
6565
+ } else if (prefix === "M") {
6566
+ nodeType = "milestone";
6567
+ const mile = graph.milestones.find((m) => m.id.toUpperCase() === id);
6568
+ if (!mile) return null;
6569
+ nodeContent = `${mile.id}: ${mile.title}
6570
+ Produces: ${mile.produces || "(none)"}`;
6571
+ const parentDecls = graph.declarations.filter((d) => (mile.realizes || []).some((r) => r === d.id));
6572
+ parentContent = parentDecls.map((d) => `${d.id}: ${d.title}
6573
+ Statement: ${d.statement || d.title}`).join("\n\n");
6574
+ const siblingMiles = graph.milestones.filter((m) => m.id !== mile.id && (mile.realizes || []).some((r) => (m.realizes || []).includes(r)));
6575
+ siblingsContent = siblingMiles.map((m) => `${m.id}: ${m.title} [${m.status}]`).join("\n");
6576
+ } else if (prefix === "A") {
6577
+ nodeType = "action";
6578
+ const action = graph.actions.find((a) => a.id.toUpperCase() === id);
6579
+ if (!action) return null;
6580
+ nodeContent = `${action.id}: ${action.title}
6581
+ Produces: ${action.produces || "(none)"}`;
6582
+ const parentMiles = graph.milestones.filter((m) => (action.causes || []).includes(m.id));
6583
+ parentContent = parentMiles.map((m) => `${m.id}: ${m.title}
6584
+ Produces: ${m.produces || "(none)"}`).join("\n\n");
6585
+ const milestoneIds = action.causes || [];
6586
+ const siblingActions = graph.actions.filter((a) => a.id !== action.id && (a.causes || []).some((c) => milestoneIds.includes(c)));
6587
+ siblingsContent = siblingActions.map((a) => `${a.id}: ${a.title} [${a.status}]`).join("\n");
6588
+ }
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"}
6632
+
6633
+ ${siblingsContent || "(none)"}
6634
+
6635
+ ## Task
6636
+
6637
+ ${taskBlock}
6638
+
6639
+ If improvements are possible, suggest a revised version. Output ONLY the improved text in this format:
6640
+
6641
+ **Title:** <improved title>
6642
+ **Statement:** <improved statement or produces>
6643
+ **Reason:** <one sentence explaining why this is better>
6644
+
6645
+ If the current version is already good, output exactly: LGTM \u2014 no changes needed.`;
6646
+ }
6647
+ async function handleRefine(req, res, cwd, nodeId) {
6648
+ try {
6649
+ if (refineSession) {
6650
+ sendJson(res, 409, { error: "A refine session is already running" });
6651
+ return;
6652
+ }
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;
6660
+ }
6661
+ const prompt = buildRefinePrompt(nodeId, graph, mode, userMessage);
6662
+ if (!prompt) {
6663
+ sendJson(res, 404, { error: "Node not found: " + nodeId });
6664
+ return;
6665
+ }
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"], {
6671
+ 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 })}
6682
+
6683
+ `;
6684
+ for (const client of sseClients) {
6685
+ try {
6686
+ client.write(payload);
6687
+ } catch (_) {
6688
+ sseClients.delete(client);
6689
+ }
6690
+ }
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 })}
6706
+
6707
+ `;
6708
+ for (const client of sseClients) {
6709
+ try {
6710
+ client.write(payload);
6711
+ } catch (_) {
6712
+ sseClients.delete(client);
6713
+ }
6714
+ }
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 };
6718
+ try {
6719
+ fs.appendFileSync(activityFile, JSON.stringify(doneEntry) + "\n");
6720
+ } catch (_) {
3924
6721
  }
3925
- return false;
3926
- });
3927
- sendJson(res, 200, {
3928
- milestone,
3929
- actions: milestoneActions
3930
6722
  });
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 });
3931
6726
  } catch (err) {
3932
6727
  sendJson(res, 500, { error: String(err) });
3933
6728
  }
3934
6729
  }
3935
- function handleActivity(res, cwd) {
3936
- const activityFile = path.join(cwd, ".planning", "activity.jsonl");
3937
- if (!fs.existsSync(activityFile)) {
3938
- sendJson(res, 200, { events: [] });
6730
+ function handleRefineStop(res) {
6731
+ if (!refineSession) {
6732
+ sendJson(res, 404, { error: "No refine session running" });
3939
6733
  return;
3940
6734
  }
3941
6735
  try {
3942
- const lines = fs.readFileSync(activityFile, "utf-8").split("\n").filter(Boolean).slice(-100);
3943
- const events = lines.map((l) => {
3944
- try {
3945
- return JSON.parse(l);
3946
- } catch {
3947
- return null;
6736
+ refineSession.proc.kill();
6737
+ } catch (_) {
6738
+ }
6739
+ refineSession = null;
6740
+ sendJson(res, 200, { ok: true });
6741
+ }
6742
+ async function handleRefineAccept(req, res, cwd) {
6743
+ try {
6744
+ const body = await readJsonBody(req);
6745
+ const { nodeId, title, statement } = body;
6746
+ if (!nodeId) {
6747
+ sendJson(res, 400, { error: "Missing nodeId" });
6748
+ return;
6749
+ }
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");
3948
6764
  }
3949
- }).filter(Boolean).reverse();
3950
- sendJson(res, 200, { events });
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");
6775
+ }
6776
+ }
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 });
3951
6781
  } catch (err) {
3952
6782
  sendJson(res, 500, { error: String(err) });
3953
6783
  }
3954
6784
  }
3955
- var sseClients = /* @__PURE__ */ new Set();
3956
- function broadcastChange() {
3957
- for (const client of sseClients) {
3958
- try {
3959
- client.write("event: change\ndata: {}\n\n");
3960
- } catch (_) {
3961
- sseClients.delete(client);
6785
+ function handleArchiveNode(res, cwd, nodeId) {
6786
+ const id = nodeId.toUpperCase();
6787
+ const prefix = id.split("-")[0];
6788
+ const planningDir = path.join(cwd, ".planning");
6789
+ try {
6790
+ if (prefix === "D") {
6791
+ const futurePath = path.join(planningDir, "FUTURE.md");
6792
+ const content = fs.readFileSync(futurePath, "utf-8");
6793
+ const declarations = parseFutureFile(content);
6794
+ const decl = declarations.find((d) => d.id === id);
6795
+ if (decl) {
6796
+ decl.status = "DONE";
6797
+ decl.reviewState = "approved";
6798
+ const projectNameMatch = content.match(/^# Future:\s*(.+)/m);
6799
+ const projectName = projectNameMatch ? projectNameMatch[1].trim() : "Project";
6800
+ fs.writeFileSync(futurePath, writeFutureFile(declarations, projectName), "utf-8");
6801
+ }
6802
+ } else if (prefix === "M") {
6803
+ const msPath = path.join(planningDir, "MILESTONES.md");
6804
+ const content = fs.readFileSync(msPath, "utf-8");
6805
+ const { milestones } = parseMilestonesFile(content);
6806
+ const mile = milestones.find((m) => m.id === id);
6807
+ if (mile) {
6808
+ mile.status = "DONE";
6809
+ mile.reviewState = "approved";
6810
+ const projectNameMatch = content.match(/^# Milestones:\s*(.+)/m);
6811
+ const projectName = projectNameMatch ? projectNameMatch[1].trim() : "Project";
6812
+ fs.writeFileSync(msPath, writeMilestonesFile(milestones, projectName), "utf-8");
6813
+ }
6814
+ } else if (prefix === "A") {
6815
+ const graph = runLoadGraph2(cwd);
6816
+ if (!("error" in graph)) {
6817
+ const action = graph.actions.find((a) => a.id.toUpperCase() === id);
6818
+ if (action) {
6819
+ for (const mId of action.causes || []) {
6820
+ const folder = findMilestoneFolder(planningDir, mId);
6821
+ if (folder) {
6822
+ const planPath = path.join(folder, "PLAN.md");
6823
+ if (fs.existsSync(planPath)) {
6824
+ const content = fs.readFileSync(planPath, "utf-8");
6825
+ const updated = updateActionStatus(content, id, "DONE");
6826
+ fs.writeFileSync(planPath, updated, "utf-8");
6827
+ }
6828
+ }
6829
+ }
6830
+ }
6831
+ }
3962
6832
  }
6833
+ const activityFile = path.join(planningDir, "activity.jsonl");
6834
+ const entry = { ts: (/* @__PURE__ */ new Date()).toISOString(), tool: "Review", phase: "end", desc: `Archived ${id}`, nodeId: id };
6835
+ fs.appendFileSync(activityFile, JSON.stringify(entry) + "\n");
6836
+ const payload = `event: change
6837
+ data: ${JSON.stringify({ reason: "archive", nodeId: id })}
6838
+
6839
+ `;
6840
+ for (const client of sseClients) {
6841
+ try {
6842
+ client.write(payload);
6843
+ } catch (_) {
6844
+ sseClients.delete(client);
6845
+ }
6846
+ }
6847
+ sendJson(res, 200, { ok: true });
6848
+ } catch (err) {
6849
+ sendJson(res, 500, { error: String(err) });
3963
6850
  }
3964
6851
  }
3965
- function watchPlanning(cwd) {
6852
+ function handleDeleteNode(res, cwd, nodeId) {
6853
+ const id = nodeId.toUpperCase();
6854
+ const prefix = id.split("-")[0];
3966
6855
  const planningDir = path.join(cwd, ".planning");
3967
- if (!fs.existsSync(planningDir)) return;
3968
- let graphTimer = null;
3969
- let activityTimer = null;
3970
- const activityFile = path.join(planningDir, "activity.jsonl");
3971
6856
  try {
3972
- fs.watch(planningDir, { recursive: true }, (_evt, filename) => {
3973
- if (filename && filename.endsWith("activity.jsonl")) {
3974
- if (activityTimer) clearTimeout(activityTimer);
3975
- activityTimer = setTimeout(() => {
3976
- for (const client of sseClients) {
3977
- try {
3978
- client.write("event: activity\ndata: {}\n\n");
3979
- } catch {
3980
- sseClients.delete(client);
6857
+ if (prefix === "D") {
6858
+ const futurePath = path.join(planningDir, "FUTURE.md");
6859
+ const content = fs.readFileSync(futurePath, "utf-8");
6860
+ const declarations = parseFutureFile(content);
6861
+ const filtered = declarations.filter((d) => d.id !== id);
6862
+ if (filtered.length === declarations.length) {
6863
+ sendJson(res, 404, { error: "Declaration not found: " + id });
6864
+ return;
6865
+ }
6866
+ const projectNameMatch = content.match(/^# Future:\s*(.+)/m);
6867
+ const projectName = projectNameMatch ? projectNameMatch[1].trim() : "Project";
6868
+ fs.writeFileSync(futurePath, writeFutureFile(filtered, projectName), "utf-8");
6869
+ } else if (prefix === "M") {
6870
+ const msPath = path.join(planningDir, "MILESTONES.md");
6871
+ const content = fs.readFileSync(msPath, "utf-8");
6872
+ const { milestones } = parseMilestonesFile(content);
6873
+ const filtered = milestones.filter((m) => m.id !== id);
6874
+ if (filtered.length === milestones.length) {
6875
+ sendJson(res, 404, { error: "Milestone not found: " + id });
6876
+ return;
6877
+ }
6878
+ const projectNameMatch = content.match(/^# Milestones:\s*(.+)/m);
6879
+ const projectName = projectNameMatch ? projectNameMatch[1].trim() : "Project";
6880
+ fs.writeFileSync(msPath, writeMilestonesFile(filtered, projectName), "utf-8");
6881
+ } else if (prefix === "A") {
6882
+ const graph = runLoadGraph2(cwd);
6883
+ if (!("error" in graph)) {
6884
+ const action = graph.actions.find((a) => a.id.toUpperCase() === id);
6885
+ if (action) {
6886
+ for (const mId of action.causes || []) {
6887
+ const folder = findMilestoneFolder(planningDir, mId);
6888
+ if (folder) {
6889
+ const planPath = path.join(folder, "PLAN.md");
6890
+ if (fs.existsSync(planPath)) {
6891
+ let content = fs.readFileSync(planPath, "utf-8");
6892
+ const regex = new RegExp(`### ${id}:[\\s\\S]*?(?=### |$)`, "i");
6893
+ content = content.replace(regex, "");
6894
+ fs.writeFileSync(planPath, content, "utf-8");
6895
+ }
3981
6896
  }
3982
6897
  }
3983
- activityTimer = null;
3984
- }, 50);
3985
- } else {
3986
- if (graphTimer) clearTimeout(graphTimer);
3987
- graphTimer = setTimeout(() => {
3988
- broadcastChange();
3989
- graphTimer = null;
3990
- }, 200);
6898
+ }
3991
6899
  }
3992
- });
3993
- } catch (_) {
6900
+ }
6901
+ const activityFile = path.join(planningDir, "activity.jsonl");
6902
+ const entry = { ts: (/* @__PURE__ */ new Date()).toISOString(), tool: "Review", phase: "end", desc: `Deleted ${id}`, nodeId: id };
6903
+ fs.appendFileSync(activityFile, JSON.stringify(entry) + "\n");
6904
+ const payload2 = `event: change
6905
+ data: ${JSON.stringify({ reason: "delete", nodeId: id })}
6906
+
6907
+ `;
6908
+ for (const client of sseClients) {
6909
+ try {
6910
+ client.write(payload2);
6911
+ } catch (_) {
6912
+ sseClients.delete(client);
6913
+ }
6914
+ }
6915
+ sendJson(res, 200, { ok: true });
6916
+ } catch (err) {
6917
+ sendJson(res, 500, { error: String(err) });
3994
6918
  }
3995
6919
  }
3996
6920
  function route(req, res, cwd) {
@@ -4000,16 +6924,352 @@ var require_server = __commonJS({
4000
6924
  if (method === "OPTIONS") {
4001
6925
  res.writeHead(204, {
4002
6926
  "Access-Control-Allow-Origin": "*",
4003
- "Access-Control-Allow-Methods": "GET, OPTIONS",
6927
+ "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
4004
6928
  "Access-Control-Allow-Headers": "Content-Type"
4005
6929
  });
4006
6930
  res.end();
4007
6931
  return;
4008
6932
  }
4009
- if (method !== "GET") {
6933
+ if (method !== "GET" && method !== "POST" && method !== "PUT" && method !== "DELETE") {
4010
6934
  sendJson(res, 405, { error: "Method Not Allowed" });
4011
6935
  return;
4012
6936
  }
6937
+ if (method === "POST" && urlPath === "/api/declarations") {
6938
+ readJsonBody(req).then((body) => {
6939
+ if (!body.title || !body.statement) {
6940
+ sendJson(res, 400, { error: "Missing required fields: title and statement" });
6941
+ return;
6942
+ }
6943
+ const result = runAddDeclaration2(cwd, ["--title", body.title, "--statement", body.statement]);
6944
+ if ("error" in result) {
6945
+ sendJson(res, 400, result);
6946
+ return;
6947
+ }
6948
+ sendJson(res, 201, result);
6949
+ broadcastChange();
6950
+ }).catch((err) => sendJson(res, 400, { error: String(err) }));
6951
+ return;
6952
+ }
6953
+ const declRefPutMatch = method === "PUT" && urlPath.match(/^\/api\/declarations\/([^/]+)\/ref$/);
6954
+ if (declRefPutMatch) {
6955
+ readJsonBody(req).then((body) => {
6956
+ const declId = declRefPutMatch[1].toUpperCase();
6957
+ const planningDir = path.join(cwd, ".planning");
6958
+ const futurePath = path.join(planningDir, "FUTURE.md");
6959
+ if (!fs.existsSync(futurePath)) {
6960
+ sendJson(res, 404, { error: "FUTURE.md not found" });
6961
+ return;
6962
+ }
6963
+ const futureContent = fs.readFileSync(futurePath, "utf-8");
6964
+ const declarations = parseFutureFile(futureContent);
6965
+ const decl = declarations.find((d) => d.id === declId);
6966
+ if (!decl) {
6967
+ sendJson(res, 404, { error: `Declaration not found: ${declId}` });
6968
+ return;
6969
+ }
6970
+ const ref = {};
6971
+ if (body.url != null) ref.url = body.url || void 0;
6972
+ if (body.path != null) ref.path = body.path || void 0;
6973
+ decl.ref = ref.url || ref.path ? ref : void 0;
6974
+ const headerMatch = futureContent.match(/^# Future: (.+)/m);
6975
+ const projectName = headerMatch ? headerMatch[1].trim() : "Project";
6976
+ const content = writeFutureFile(declarations, projectName);
6977
+ fs.writeFileSync(futurePath, content, "utf-8");
6978
+ sendJson(res, 200, { id: declId, ref: decl.ref || null });
6979
+ broadcastChange();
6980
+ }).catch((err) => sendJson(res, 400, { error: String(err) }));
6981
+ return;
6982
+ }
6983
+ const getAnnotationsMatch = method === "GET" && urlPath.match(/^\/api\/node\/([^/]+)\/annotations$/);
6984
+ if (getAnnotationsMatch) {
6985
+ handleGetAnnotations(res, cwd, getAnnotationsMatch[1]);
6986
+ return;
6987
+ }
6988
+ const getRevisionsMatch = method === "GET" && urlPath.match(/^\/api\/node\/([^/]+)\/revisions$/);
6989
+ if (getRevisionsMatch) {
6990
+ handleGetRevisions(res, cwd, getRevisionsMatch[1]);
6991
+ return;
6992
+ }
6993
+ const incrementRoundMatch = method === "POST" && urlPath.match(/^\/api\/node\/([^/]+)\/annotations\/increment-round$/);
6994
+ if (incrementRoundMatch) {
6995
+ handleIncrementRevisionRound(res, cwd, incrementRoundMatch[1]);
6996
+ return;
6997
+ }
6998
+ const postAnnotationsMatch = method === "POST" && urlPath.match(/^\/api\/node\/([^/]+)\/annotations$/);
6999
+ if (postAnnotationsMatch) {
7000
+ handleAddAnnotation(req, res, cwd, postAnnotationsMatch[1]);
7001
+ return;
7002
+ }
7003
+ const deleteAnnotationMatch = method === "DELETE" && urlPath.match(/^\/api\/node\/([^/]+)\/annotations\/([^/]+)$/);
7004
+ if (deleteAnnotationMatch) {
7005
+ handleDeleteAnnotation(res, cwd, deleteAnnotationMatch[1], deleteAnnotationMatch[2]);
7006
+ return;
7007
+ }
7008
+ const reviewStateMatch = method === "PUT" && urlPath.match(/^\/api\/node\/([^/]+)\/review-state$/);
7009
+ if (reviewStateMatch) {
7010
+ handleUpdateReviewState(req, res, cwd, reviewStateMatch[1]);
7011
+ return;
7012
+ }
7013
+ const declPutMatch = method === "PUT" && urlPath.match(/^\/api\/declarations\/([^/]+)$/);
7014
+ if (declPutMatch) {
7015
+ readJsonBody(req).then((body) => {
7016
+ const args = ["--id", declPutMatch[1]];
7017
+ if (body.title) {
7018
+ args.push("--title", body.title);
7019
+ }
7020
+ if (body.statement) {
7021
+ args.push("--statement", body.statement);
7022
+ }
7023
+ if (body.status) {
7024
+ args.push("--status", body.status);
7025
+ }
7026
+ if (!body.title && !body.statement && !body.status) {
7027
+ sendJson(res, 400, { error: "At least one of title, statement, or status must be provided" });
7028
+ return;
7029
+ }
7030
+ const result = runUpdateDeclaration2(cwd, args);
7031
+ if ("error" in result) {
7032
+ const status = result.error.includes("not found") ? 404 : 400;
7033
+ sendJson(res, status, result);
7034
+ return;
7035
+ }
7036
+ sendJson(res, 200, result);
7037
+ broadcastChange();
7038
+ }).catch((err) => sendJson(res, 400, { error: String(err) }));
7039
+ return;
7040
+ }
7041
+ const classifyMatch = method === "PUT" && urlPath.match(/^\/api\/milestones\/([^/]+)\/classify$/);
7042
+ if (classifyMatch) {
7043
+ readJsonBody(req).then((body) => {
7044
+ const milestoneId = classifyMatch[1].toUpperCase();
7045
+ const newClassification = body.classification === "human" ? "human" : "agent";
7046
+ try {
7047
+ const milestonesPath = path.join(cwd, ".planning", "MILESTONES.md");
7048
+ if (!fs.existsSync(milestonesPath)) {
7049
+ sendJson(res, 404, { error: "MILESTONES.md not found" });
7050
+ return;
7051
+ }
7052
+ const { parseMilestonesFile: parseMF, writeMilestonesFile: writeMF } = require_milestones();
7053
+ const content = fs.readFileSync(milestonesPath, "utf-8");
7054
+ const { milestones: allM } = parseMF(content);
7055
+ const target = allM.find((m) => m.id.toUpperCase() === milestoneId);
7056
+ if (!target) {
7057
+ sendJson(res, 404, { error: `Milestone '${milestoneId}' not found` });
7058
+ return;
7059
+ }
7060
+ target.classification = newClassification;
7061
+ const nameMatch = content.match(/^# Milestones:\s*(.+)/m);
7062
+ const pName = nameMatch ? nameMatch[1].trim() : "Project";
7063
+ fs.writeFileSync(milestonesPath, writeMF(allM, pName));
7064
+ sendJson(res, 200, { ok: true, id: target.id, classification: newClassification });
7065
+ broadcastChange();
7066
+ } catch (err) {
7067
+ sendJson(res, 500, { error: String(err) });
7068
+ }
7069
+ }).catch((err) => sendJson(res, 400, { error: String(err) }));
7070
+ return;
7071
+ }
7072
+ const depsMatch = method === "PUT" && urlPath.match(/^\/api\/milestones\/([^/]+)\/depends-on$/);
7073
+ if (depsMatch) {
7074
+ readJsonBody(req).then((body) => {
7075
+ const milestoneId = depsMatch[1].toUpperCase();
7076
+ const deps = Array.isArray(body.dependsOn) ? body.dependsOn.map((d) => d.toUpperCase()) : [];
7077
+ try {
7078
+ const milestonesPath = path.join(cwd, ".planning", "MILESTONES.md");
7079
+ if (!fs.existsSync(milestonesPath)) {
7080
+ sendJson(res, 404, { error: "MILESTONES.md not found" });
7081
+ return;
7082
+ }
7083
+ const { parseMilestonesFile: parseMF, writeMilestonesFile: writeMF } = require_milestones();
7084
+ const content = fs.readFileSync(milestonesPath, "utf-8");
7085
+ const { milestones: allM } = parseMF(content);
7086
+ const target = allM.find((m) => m.id.toUpperCase() === milestoneId);
7087
+ if (!target) {
7088
+ sendJson(res, 404, { error: `Milestone '${milestoneId}' not found` });
7089
+ return;
7090
+ }
7091
+ for (const depId of deps) {
7092
+ if (!allM.find((m) => m.id.toUpperCase() === depId)) {
7093
+ sendJson(res, 400, { error: `Dependency '${depId}' not found` });
7094
+ return;
7095
+ }
7096
+ }
7097
+ if (deps.includes(milestoneId)) {
7098
+ sendJson(res, 400, { error: "Cannot depend on self" });
7099
+ return;
7100
+ }
7101
+ target.dependsOn = deps;
7102
+ const nameMatch = content.match(/^# Milestones:\s*(.+)/m);
7103
+ const pName = nameMatch ? nameMatch[1].trim() : "Project";
7104
+ fs.writeFileSync(milestonesPath, writeMF(allM, pName));
7105
+ sendJson(res, 200, { ok: true, id: target.id, dependsOn: deps });
7106
+ broadcastChange();
7107
+ } catch (err) {
7108
+ sendJson(res, 500, { error: String(err) });
7109
+ }
7110
+ }).catch((err) => sendJson(res, 400, { error: String(err) }));
7111
+ return;
7112
+ }
7113
+ const declDeleteMatch = method === "DELETE" && urlPath.match(/^\/api\/declarations\/([^/]+)$/);
7114
+ if (declDeleteMatch) {
7115
+ const result = runDeleteDeclaration2(cwd, ["--id", declDeleteMatch[1]]);
7116
+ if ("error" in result) {
7117
+ const status = result.error.includes("not found") ? 404 : 400;
7118
+ sendJson(res, status, result);
7119
+ return;
7120
+ }
7121
+ sendJson(res, 200, result);
7122
+ broadcastChange();
7123
+ return;
7124
+ }
7125
+ const nodeDeleteMatch = method === "DELETE" && urlPath.match(/^\/api\/node\/([^/]+)$/);
7126
+ if (nodeDeleteMatch) {
7127
+ handleDeleteNode(res, cwd, nodeDeleteMatch[1]);
7128
+ return;
7129
+ }
7130
+ if (method === "POST") {
7131
+ const executeMatch = urlPath.match(/^\/api\/action\/([^/]+)\/execute$/);
7132
+ if (executeMatch) {
7133
+ handleExecuteAction(res, cwd, executeMatch[1]);
7134
+ return;
7135
+ }
7136
+ const stopMatch = urlPath.match(/^\/api\/action\/([^/]+)\/stop$/);
7137
+ if (stopMatch) {
7138
+ const pm = getProcessManager(cwd);
7139
+ const result = pm.stop(stopMatch[1]);
7140
+ if (result.error) {
7141
+ sendJson(res, result.status || 500, { error: result.error });
7142
+ } else {
7143
+ sendJson(res, 200, { ok: true });
7144
+ }
7145
+ return;
7146
+ }
7147
+ if (urlPath === "/api/milestones/derive") {
7148
+ handleDerive(req, res, cwd);
7149
+ return;
7150
+ }
7151
+ if (urlPath === "/api/milestones/derive/stop") {
7152
+ handleDeriveStop(res, cwd);
7153
+ return;
7154
+ }
7155
+ if (urlPath === "/api/milestones/derive/accept") {
7156
+ handleDeriveAccept(req, res, cwd);
7157
+ return;
7158
+ }
7159
+ const actionDeriveMatch = urlPath.match(/^\/api\/milestones\/([^/]+)\/actions\/derive$/);
7160
+ if (actionDeriveMatch) {
7161
+ handleActionDerive(res, cwd, actionDeriveMatch[1]);
7162
+ return;
7163
+ }
7164
+ const actionDeriveStopMatch = urlPath.match(/^\/api\/milestones\/([^/]+)\/actions\/derive\/stop$/);
7165
+ if (actionDeriveStopMatch) {
7166
+ handleActionDeriveStop(res, cwd);
7167
+ return;
7168
+ }
7169
+ const actionDeriveAcceptMatch = urlPath.match(/^\/api\/milestones\/([^/]+)\/actions\/derive\/accept$/);
7170
+ if (actionDeriveAcceptMatch) {
7171
+ handleActionDeriveAccept(req, res, cwd, actionDeriveAcceptMatch[1]);
7172
+ return;
7173
+ }
7174
+ if (urlPath === "/api/play") {
7175
+ const manifestPath = path.join(cwd, ".planning", "execution-manifest.json");
7176
+ const hasManifest = fs.existsSync(manifestPath);
7177
+ if (hasManifest) {
7178
+ const graph = runLoadGraph2(cwd);
7179
+ if ("error" in graph) {
7180
+ sendJson(res, 500, { error: graph.error });
7181
+ return;
7182
+ }
7183
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
7184
+ const actionIds = /* @__PURE__ */ new Set();
7185
+ for (const wave of manifest.waves || []) {
7186
+ for (const m of wave.milestones || []) {
7187
+ for (const aid of m.actions || []) actionIds.add(aid.toUpperCase());
7188
+ }
7189
+ }
7190
+ const unapproved = (graph.actions || []).filter((a) => actionIds.has(a.id.toUpperCase()) && a.reviewState !== "approved");
7191
+ if (unapproved.length > 0) {
7192
+ sendJson(res, 403, {
7193
+ error: "Cannot play: unapproved actions exist",
7194
+ unapproved: unapproved.map((a) => ({ id: a.id, title: a.title, reviewState: a.reviewState || "draft" }))
7195
+ });
7196
+ return;
7197
+ }
7198
+ const plr = getPipelineRunner(cwd);
7199
+ const result = plr.start();
7200
+ if (result.error) {
7201
+ sendJson(res, 409, { error: result.error });
7202
+ } else {
7203
+ sendJson(res, 202, { ok: true, waves: result.waves });
7204
+ }
7205
+ } else {
7206
+ const pr = getPlayRunner(cwd);
7207
+ const result = pr.start();
7208
+ if (result.error) {
7209
+ const status = result.unapproved ? 403 : 409;
7210
+ sendJson(res, status, { error: result.error, ...result.unapproved && { unapproved: result.unapproved } });
7211
+ } else {
7212
+ sendJson(res, 202, { ok: true, waves: result.waves });
7213
+ }
7214
+ }
7215
+ return;
7216
+ }
7217
+ if (urlPath === "/api/play/stop") {
7218
+ const pr = getPlayRunner(cwd);
7219
+ const result = pr.stop();
7220
+ const plr = getPipelineRunner(cwd);
7221
+ if (plr.running()) plr.stop();
7222
+ if (result.error && !plr.running()) {
7223
+ sendJson(res, 400, { error: result.error });
7224
+ } else {
7225
+ sendJson(res, 200, { ok: true });
7226
+ }
7227
+ return;
7228
+ }
7229
+ if (urlPath === "/api/pipeline/skip-action") {
7230
+ const plr = getPipelineRunner(cwd);
7231
+ const result = plr.skip();
7232
+ if (result.error) {
7233
+ sendJson(res, 400, { error: result.error });
7234
+ } else {
7235
+ sendJson(res, 200, { ok: true });
7236
+ }
7237
+ return;
7238
+ }
7239
+ const reviseMatch = urlPath.match(/^\/api\/node\/([^/]+)\/revise$/);
7240
+ if (reviseMatch) {
7241
+ handleRevise(req, res, cwd, reviseMatch[1]);
7242
+ return;
7243
+ }
7244
+ if (urlPath === "/api/revise/stop") {
7245
+ handleReviseStop(res, cwd);
7246
+ return;
7247
+ }
7248
+ const archiveMatch = urlPath.match(/^\/api\/node\/([^/]+)\/archive$/);
7249
+ if (archiveMatch) {
7250
+ handleArchiveNode(res, cwd, archiveMatch[1]);
7251
+ return;
7252
+ }
7253
+ const refineMatch = urlPath.match(/^\/api\/node\/([^/]+)\/refine$/);
7254
+ if (refineMatch) {
7255
+ handleRefine(req, res, cwd, refineMatch[1]);
7256
+ return;
7257
+ }
7258
+ if (urlPath === "/api/refine/stop") {
7259
+ handleRefineStop(res);
7260
+ return;
7261
+ }
7262
+ if (urlPath === "/api/refine/accept") {
7263
+ handleRefineAccept(req, res, cwd);
7264
+ return;
7265
+ }
7266
+ if (urlPath === "/api/execution-manifest") {
7267
+ handleSaveManifest(req, res, cwd);
7268
+ return;
7269
+ }
7270
+ sendJson(res, 404, { error: `Route not found: ${urlPath}` });
7271
+ return;
7272
+ }
4013
7273
  if (urlPath === "/events") {
4014
7274
  res.writeHead(200, {
4015
7275
  "Content-Type": "text/event-stream",
@@ -4030,15 +7290,95 @@ var require_server = __commonJS({
4030
7290
  handleStatus(res, cwd);
4031
7291
  return;
4032
7292
  }
7293
+ if (urlPath === "/api/project") {
7294
+ try {
7295
+ const projectPath = path.join(cwd, ".planning", "PROJECT.md");
7296
+ const content = fs.existsSync(projectPath) ? fs.readFileSync(projectPath, "utf-8") : "";
7297
+ const titleMatch = content.match(/^#\s+(.+)/m);
7298
+ const title = titleMatch ? titleMatch[1].trim() : path.basename(cwd);
7299
+ const whatMatch = content.match(/## What This Is\s*\n([\s\S]*?)(?=\n##|\n$|$)/);
7300
+ const description = whatMatch ? whatMatch[1].trim() : "";
7301
+ const coreMatch = content.match(/## Core Value\s*\n([\s\S]*?)(?=\n##|\n$|$)/);
7302
+ const coreValue = coreMatch ? coreMatch[1].trim() : "";
7303
+ const stateMatch = content.match(/## Current State\s*\n([\s\S]*?)(?=\n##|\n---|$)/);
7304
+ let currentState = "";
7305
+ if (stateMatch) {
7306
+ const lines = stateMatch[1].trim().split("\n").filter((l) => l.trim());
7307
+ currentState = lines.slice(0, 3).map((l) => l.replace(/^\*\*/, "").replace(/\*\*/, "")).join(" | ");
7308
+ }
7309
+ sendJson(res, 200, { title, description, coreValue, currentState, folder: path.basename(cwd) });
7310
+ } catch (err) {
7311
+ sendJson(res, 500, { error: String(err) });
7312
+ }
7313
+ return;
7314
+ }
7315
+ const workabilityMatch = urlPath.match(/^\/api\/workability\/([^/]+)$/);
7316
+ if (workabilityMatch) {
7317
+ handleWorkability(res, cwd, workabilityMatch[1]);
7318
+ return;
7319
+ }
7320
+ const milestoneLogMatch = urlPath.match(/^\/api\/milestone\/([^/]+)\/log$/);
7321
+ if (milestoneLogMatch) {
7322
+ handleMilestoneLog(res, cwd, milestoneLogMatch[1]);
7323
+ return;
7324
+ }
4033
7325
  const milestoneMatch = urlPath.match(/^\/api\/milestone\/([^/]+)$/);
4034
7326
  if (milestoneMatch) {
4035
7327
  handleMilestone(res, cwd, milestoneMatch[1]);
4036
7328
  return;
4037
7329
  }
7330
+ if (urlPath === "/api/running") {
7331
+ const pm = getProcessManager(cwd);
7332
+ sendJson(res, 200, { running: pm.running() });
7333
+ return;
7334
+ }
7335
+ if (urlPath === "/api/play/status") {
7336
+ const pr = getPlayRunner(cwd);
7337
+ const plr = getPipelineRunner(cwd);
7338
+ sendJson(res, 200, { running: pr.running() || plr.running(), paused: plr.paused(), status: pr.status() || plr.status() });
7339
+ return;
7340
+ }
7341
+ if (urlPath === "/api/pipeline/state") {
7342
+ const plr = getPipelineRunner(cwd);
7343
+ const state = plr.getFullState();
7344
+ if (state) {
7345
+ sendJson(res, 200, { active: true, ...state });
7346
+ } else {
7347
+ sendJson(res, 200, { active: false });
7348
+ }
7349
+ return;
7350
+ }
7351
+ if (urlPath === "/api/derivation/running") {
7352
+ const dr = getDerivationRunner(cwd);
7353
+ sendJson(res, 200, { running: dr.running() });
7354
+ return;
7355
+ }
7356
+ const actionDeriveRunningMatch = urlPath.match(/^\/api\/milestones\/([^/]+)\/actions\/derive\/running$/);
7357
+ if (actionDeriveRunningMatch) {
7358
+ const adr = getActionDerivationRunner(cwd);
7359
+ sendJson(res, 200, { running: adr.running() });
7360
+ return;
7361
+ }
7362
+ if (urlPath === "/api/workflow/state") {
7363
+ handleWorkflowState(res, cwd);
7364
+ return;
7365
+ }
4038
7366
  if (urlPath === "/api/activity") {
4039
7367
  handleActivity(res, cwd);
4040
7368
  return;
4041
7369
  }
7370
+ if (urlPath === "/api/readiness") {
7371
+ handleReadiness(res, cwd);
7372
+ return;
7373
+ }
7374
+ if (urlPath === "/api/execution-manifest") {
7375
+ handleGetManifest(res, cwd);
7376
+ return;
7377
+ }
7378
+ if (urlPath === "/api/files") {
7379
+ handleFileContent(req, res, cwd);
7380
+ return;
7381
+ }
4042
7382
  const actionMatch = urlPath.match(/^\/api\/action\/([^/]+)$/);
4043
7383
  if (actionMatch) {
4044
7384
  const result = runGetExecPlan2(cwd, ["--action", actionMatch[1]]);
@@ -4125,6 +7465,73 @@ var require_serve = __commonJS({
4125
7465
  }
4126
7466
  });
4127
7467
 
7468
+ // src/commands/open.js
7469
+ var require_open = __commonJS({
7470
+ "src/commands/open.js"(exports2, module2) {
7471
+ "use strict";
7472
+ var fs = require("fs");
7473
+ var path = require("path");
7474
+ var http = require("http");
7475
+ var { spawn } = require("child_process");
7476
+ function checkServer(port) {
7477
+ return new Promise((resolve) => {
7478
+ const req = http.get(`http://localhost:${port}/api/graph`, (res) => {
7479
+ resolve(res.statusCode >= 200 && res.statusCode < 300);
7480
+ res.resume();
7481
+ });
7482
+ req.on("error", () => resolve(false));
7483
+ req.setTimeout(2e3, () => {
7484
+ req.destroy();
7485
+ resolve(false);
7486
+ });
7487
+ });
7488
+ }
7489
+ async function waitForServer(port, maxAttempts = 10, intervalMs = 100) {
7490
+ for (let i = 0; i < maxAttempts; i++) {
7491
+ if (await checkServer(port)) return true;
7492
+ await new Promise((r) => setTimeout(r, intervalMs));
7493
+ }
7494
+ return false;
7495
+ }
7496
+ async function runOpen2(cwd, args) {
7497
+ const planningDir = path.join(cwd, ".planning");
7498
+ 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);
7509
+ }
7510
+ const portFile = path.join(cwd, ".planning", "server.port");
7511
+ const port = fs.existsSync(portFile) ? parseInt(fs.readFileSync(portFile, "utf8").trim(), 10) : 3847;
7512
+ const isRunning = await checkServer(port);
7513
+ if (!isRunning) {
7514
+ const bundlePath = path.resolve(__dirname, "declare-tools.cjs");
7515
+ const child = spawn(process.execPath, [bundlePath, "serve", "--port", String(port)], {
7516
+ cwd,
7517
+ detached: true,
7518
+ stdio: "ignore"
7519
+ });
7520
+ child.unref();
7521
+ const ready = await waitForServer(port);
7522
+ if (!ready) {
7523
+ console.error("[declare] Warning: server may not be ready yet");
7524
+ }
7525
+ }
7526
+ const url = `http://localhost:${port}`;
7527
+ const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
7528
+ spawn(opener, [url], { stdio: "ignore", detached: true }).unref();
7529
+ console.log(`Dashboard: ${url}`);
7530
+ }
7531
+ module2.exports = { runOpen: runOpen2 };
7532
+ }
7533
+ });
7534
+
4128
7535
  // src/declare-tools.js
4129
7536
  var { commitPlanningDocs } = require_commit();
4130
7537
  var { readState, recordSession } = require_state();
@@ -4132,6 +7539,8 @@ var { runInit } = require_init();
4132
7539
  var { runStatus } = require_status();
4133
7540
  var { runHelp } = require_help();
4134
7541
  var { runAddDeclaration } = require_add_declaration();
7542
+ var { runUpdateDeclaration } = require_update_declaration();
7543
+ var { runDeleteDeclaration } = require_delete_declaration();
4135
7544
  var { runAddMilestone } = require_add_milestone();
4136
7545
  var { runAddMilestonesBatch } = require_add_milestones_batch();
4137
7546
  var { runAddAction } = require_add_action();
@@ -4158,6 +7567,7 @@ var { runConfigGet } = require_config_get();
4158
7567
  var { runConfigSet } = require_config_set();
4159
7568
  var { runHealthCheck, runHealthCheckRepair } = require_health_check();
4160
7569
  var { runServe } = require_serve();
7570
+ var { runOpen } = require_open();
4161
7571
  function parseCwdFlag(argv) {
4162
7572
  const idx = argv.indexOf("--cwd");
4163
7573
  if (idx === -1 || idx + 1 >= argv.length) return null;
@@ -4199,9 +7609,18 @@ function parseNamedFlag(argv, flag) {
4199
7609
  function main() {
4200
7610
  const args = process.argv.slice(2);
4201
7611
  const command = args[0];
4202
- if (!command) {
4203
- console.log(JSON.stringify({ error: "No command specified. Use: commit, init, status, add-declaration, add-milestone, add-milestones, create-plan, load-graph, trace, prioritize, visualize, compute-waves, generate-exec-plan, verify-wave, verify-milestone, execute, check-drift, check-occurrence, compute-performance, renegotiate, complete-milestone, sync-status, serve, record-session, get-state, quick-task, add-todo, check-todos, complete-todo, config-get, config-set, health-check, help" }));
4204
- process.exit(1);
7612
+ const sub = args[0];
7613
+ const isDefaultOpen = !sub || sub === "." || (sub.startsWith("/") || sub.startsWith("~"));
7614
+ if (isDefaultOpen) {
7615
+ let projectRoot = process.cwd();
7616
+ if (sub && sub !== ".") {
7617
+ projectRoot = sub.startsWith("~") ? sub.replace("~", require("os").homedir()) : sub;
7618
+ }
7619
+ runOpen(projectRoot, args.slice(1)).catch((err) => {
7620
+ console.error("[declare] " + err.message);
7621
+ process.exit(1);
7622
+ });
7623
+ return;
4205
7624
  }
4206
7625
  try {
4207
7626
  switch (command) {
@@ -4244,6 +7663,20 @@ function main() {
4244
7663
  if (result.error) process.exit(1);
4245
7664
  break;
4246
7665
  }
7666
+ case "update-declaration": {
7667
+ const cwdUpdateDecl = parseCwdFlag(args) || process.cwd();
7668
+ const result = runUpdateDeclaration(cwdUpdateDecl, args.slice(1));
7669
+ console.log(JSON.stringify(result));
7670
+ if (result.error) process.exit(1);
7671
+ break;
7672
+ }
7673
+ case "delete-declaration": {
7674
+ const cwdDeleteDecl = parseCwdFlag(args) || process.cwd();
7675
+ const result = runDeleteDeclaration(cwdDeleteDecl, args.slice(1));
7676
+ console.log(JSON.stringify(result));
7677
+ if (result.error) process.exit(1);
7678
+ break;
7679
+ }
4247
7680
  case "add-milestone": {
4248
7681
  const cwdAddMs = parseCwdFlag(args) || process.cwd();
4249
7682
  const result = runAddMilestone(cwdAddMs, args.slice(1));
@@ -4469,7 +7902,7 @@ function main() {
4469
7902
  break;
4470
7903
  }
4471
7904
  default:
4472
- console.log(JSON.stringify({ error: `Unknown command: ${command}. Use: commit, init, status, add-declaration, add-milestone, add-milestones, create-plan, load-graph, trace, prioritize, visualize, compute-waves, generate-exec-plan, verify-wave, verify-milestone, execute, check-drift, check-occurrence, compute-performance, renegotiate, complete-milestone, sync-status, serve, record-session, get-state, quick-task, add-todo, check-todos, complete-todo, config-get, config-set, health-check, help` }));
7905
+ console.log(JSON.stringify({ error: `Unknown command: ${command}. Use: commit, init, status, add-declaration, update-declaration, delete-declaration, add-milestone, add-milestones, create-plan, load-graph, trace, prioritize, visualize, compute-waves, generate-exec-plan, verify-wave, verify-milestone, execute, check-drift, check-occurrence, compute-performance, renegotiate, complete-milestone, sync-status, serve, record-session, get-state, quick-task, add-todo, check-todos, complete-todo, config-get, config-set, health-check, help` }));
4473
7906
  process.exit(1);
4474
7907
  }
4475
7908
  } catch (err) {