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.
- package/bin/declare.js +16 -0
- package/commands/declare/complete-milestone.md +3 -3
- package/commands/declare/execute.md +1 -1
- package/commands/declare/{new-milestone.md → new-cycle.md} +1 -1
- package/commands/declare/plan.md +5 -3
- package/commands/declare/progress.md +2 -2
- package/commands/declare/research.md +6 -6
- package/commands/declare/resume.md +1 -1
- package/commands/declare/verify.md +1 -0
- package/dist/declare-tools.cjs +3601 -168
- package/dist/public/app.js +5801 -467
- package/dist/public/index.html +2747 -156
- package/package.json +5 -3
package/dist/declare-tools.cjs
CHANGED
|
@@ -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
|
-
|
|
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
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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/
|
|
3829
|
-
var
|
|
3830
|
-
"src/server/
|
|
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
|
|
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 {
|
|
3837
|
-
|
|
3838
|
-
|
|
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
|
-
|
|
3885
|
-
|
|
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
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
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
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
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
|
-
|
|
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
|
|
3922
|
-
|
|
3923
|
-
|
|
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
|
|
3936
|
-
|
|
3937
|
-
|
|
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
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
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
|
-
}
|
|
3950
|
-
|
|
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
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
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
|
|
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
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
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
|
-
|
|
3984
|
-
}, 50);
|
|
3985
|
-
} else {
|
|
3986
|
-
if (graphTimer) clearTimeout(graphTimer);
|
|
3987
|
-
graphTimer = setTimeout(() => {
|
|
3988
|
-
broadcastChange();
|
|
3989
|
-
graphTimer = null;
|
|
3990
|
-
}, 200);
|
|
6898
|
+
}
|
|
3991
6899
|
}
|
|
3992
|
-
}
|
|
3993
|
-
|
|
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
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
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) {
|