@agent-plan/server 0.2.29 → 0.3.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/dist/serve.d.ts.map +1 -1
- package/dist/serve.js +59 -76
- package/package.json +2 -2
- package/web-ui-dist/assets/index-CmsfDpZ0.js +334 -0
- package/web-ui-dist/assets/index-DKmMhG5Y.css +1 -0
- package/web-ui-dist/index.html +2 -2
- package/web-ui-dist/assets/index-C5sLUPQ_.js +0 -329
- package/web-ui-dist/assets/index-DHgouqOR.css +0 -1
package/dist/serve.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../src/serve.ts"],"names":[],"mappings":"AAYA,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../src/serve.ts"],"names":[],"mappings":"AAYA,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAyJpC,MAAM,WAAW,kBAAkB;IACjC,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,GAAG,CAAC,EAAE,OAAO,CAAC;CACf;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,OAAO,GAAG,KAAK,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC7B;AAED,MAAM,WAAW,QAAQ;IACvB,SAAS,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,QAAQ,GAAG,MAAM,GAAG,QAAQ,GAAG,QAAQ,EAAE,kBAAkB,CAAC,CAAC,CAAC;IACzF,MAAM,CAAC,EAAE,cAAc,GAAG,SAAS,CAAC;CACrC;AA+6CD,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,QAAQ,CAAC,EAAE,QAAQ,GAAG,SAAS,CAAC;IAChC,MAAM,CAAC,EAAE,CAAC,MAAM,OAAO,CAAC,GAAG,SAAS,CAAC;CACtC;AAED,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,IAAI,EAAE,OAAO,GAAG,KAAK,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,KAAK,CAAC;IACX,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5B;AAED,wBAAsB,KAAK,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,CA6EvE"}
|
package/dist/serve.js
CHANGED
|
@@ -7,15 +7,11 @@ import { fileURLToPath } from "node:url";
|
|
|
7
7
|
import { createAdaptorServer } from "@hono/node-server";
|
|
8
8
|
import { Hono } from "hono";
|
|
9
9
|
import { cors } from "hono/cors";
|
|
10
|
-
import { ExportService, PlanStore, PlanStoreError, PlanStaleWriteError, PlanWriterBusyError, createFeatureId, createPhaseId, createChecklistItemId, createRequirementId, createTaskId, findPhaseByRef, normalizeSlug, withFeatureLock, needsMotivation, checkExplicitTaskStart, recommendNextTask, recommendNextWork, reconcileRequirementMacroTasks, RequirementMacroTaskError, packageVersionFromModule } from "@agent-plan/core";
|
|
10
|
+
import { ExportService, PlanStore, PlanStoreError, PlanStaleWriteError, PlanWriterBusyError, createFeatureId, createPhaseId, createChecklistItemId, createRequirementId, createTaskId, findPhaseByRef, normalizeSlug, withFeatureLock, needsMotivation, checkExplicitTaskStart, recommendNextTask, recommendNextWork, reconcileRequirementMacroTasks, RequirementMacroTaskError, packageVersionFromModule, ACCEPTED_DECISION_RAW_REPLACEMENT_DISABLED_MESSAGE, ACCEPTED_DECISION_SEMANTIC_MUTATION_REQUIRED_ERROR_CODE, deleteFeatureCascade, nowISO, applyTaskLifecycleDates, resolveAcceptedDecisionTarget } from "@agent-plan/core";
|
|
11
11
|
import { WsHub } from "./ws-hub.js";
|
|
12
12
|
// ─── Watcher ────────────────────────────────────────────────────────────
|
|
13
13
|
let watcherAbort = null;
|
|
14
|
-
let watcherHubRef = { current: null };
|
|
15
14
|
const SERVER_PACKAGE = packageVersionFromModule(import.meta.url, "@agent-plan/server");
|
|
16
|
-
function nowISO() {
|
|
17
|
-
return new Date().toISOString();
|
|
18
|
-
}
|
|
19
15
|
/** Resolve requirement phase refs before persistence so requirements never dangle. */
|
|
20
16
|
async function resolveRequirementPhaseIds(store, linkedPhaseIds) {
|
|
21
17
|
if (!Array.isArray(linkedPhaseIds) || linkedPhaseIds.length === 0) {
|
|
@@ -46,40 +42,27 @@ function noMutableFieldsResponse(fields) {
|
|
|
46
42
|
mutableFields: [...fields],
|
|
47
43
|
};
|
|
48
44
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
?? (() => {
|
|
71
|
-
const taskNumberMatch = targetRef.match(/^T(\d+)$/i);
|
|
72
|
-
return taskNumberMatch
|
|
73
|
-
? phases.flatMap((phase) => phase.tasks.map((task) => ({ phase, task }))).find(({ task }) => task.number === Number(taskNumberMatch[1]))
|
|
74
|
-
: undefined;
|
|
75
|
-
})();
|
|
76
|
-
if (!found)
|
|
77
|
-
return { ok: false, error: `task not found: ${targetRef}` };
|
|
78
|
-
return { ok: true, owner: { kind: "task", phaseId: found.phase.id, taskId: found.task.id }, targetRef: `P${String(found.phase.number).padStart(3, "0")}/T${String(found.task.number).padStart(3, "0")}`, event: { type: "task", phaseId: found.phase.id, taskId: found.task.id, featureId: found.phase.featureId ?? "" } };
|
|
79
|
-
}
|
|
80
|
-
function nextTaskNumber(phase) {
|
|
81
|
-
const numbers = phase.tasks.map((task) => task.number || 0).filter((n) => Number.isFinite(n));
|
|
82
|
-
return (numbers.length > 0 ? Math.max(...numbers) : 0) + 1;
|
|
45
|
+
/**
|
|
46
|
+
* Resolve an accepted-decision target with the same ref grammar the adapters
|
|
47
|
+
* use (plan-core's resolveAcceptedDecisionTarget: composite/shortId/title for
|
|
48
|
+
* tasks, exact/partial name with ambiguity detection for features), then add
|
|
49
|
+
* the WebSocket broadcast envelope this HTTP layer alone needs. Resolution
|
|
50
|
+
* itself is never re-derived here — only the event shape is server-local.
|
|
51
|
+
*/
|
|
52
|
+
async function resolveAcceptedDecisionEventTarget(store, targetType, rawTargetRef) {
|
|
53
|
+
const target = await resolveAcceptedDecisionTarget(store, targetType, rawTargetRef);
|
|
54
|
+
if (!target.ok)
|
|
55
|
+
return target;
|
|
56
|
+
const { owner, ref: targetRef } = target;
|
|
57
|
+
if (owner.kind === "project")
|
|
58
|
+
return { ok: true, owner, targetRef, event: { type: "project" } };
|
|
59
|
+
if (owner.kind === "feature")
|
|
60
|
+
return { ok: true, owner, targetRef, event: { type: "feature", featureId: owner.featureId } };
|
|
61
|
+
const phase = await store.loadPhase(owner.phaseId).catch(() => null);
|
|
62
|
+
const featureId = phase?.featureId ?? "";
|
|
63
|
+
if (owner.kind === "phase")
|
|
64
|
+
return { ok: true, owner, targetRef, event: { type: "phase", phaseId: owner.phaseId, featureId } };
|
|
65
|
+
return { ok: true, owner, targetRef, event: { type: "task", phaseId: owner.phaseId, taskId: owner.taskId, featureId } };
|
|
83
66
|
}
|
|
84
67
|
function isPrivateIpv4(address) {
|
|
85
68
|
return /^10\./.test(address) || /^192\.168\./.test(address) || /^172\.(1[6-9]|2\d|3[01])\./.test(address);
|
|
@@ -129,29 +112,12 @@ function phaseGovernanceReady(phase) {
|
|
|
129
112
|
function fromWebUi(c) {
|
|
130
113
|
return c.req.header("X-Planner-Source") === "web-ui";
|
|
131
114
|
}
|
|
132
|
-
function applyTaskLifecycleDates(task, nextStatus, now) {
|
|
133
|
-
const previousStatus = task.status;
|
|
134
|
-
if (nextStatus === "in-progress" && !task.startedAt) {
|
|
135
|
-
task.startedAt = now;
|
|
136
|
-
}
|
|
137
|
-
if (nextStatus === "done") {
|
|
138
|
-
if (!task.startedAt)
|
|
139
|
-
task.startedAt = now;
|
|
140
|
-
task.completedAt = now;
|
|
141
|
-
}
|
|
142
|
-
else if (previousStatus === "done") {
|
|
143
|
-
task.completedAt = "";
|
|
144
|
-
}
|
|
145
|
-
task.status = nextStatus;
|
|
146
|
-
return task;
|
|
147
|
-
}
|
|
148
115
|
function startWatcher(planRoot, hubRef) {
|
|
149
116
|
stopWatcher();
|
|
150
117
|
if (!existsSync(planRoot))
|
|
151
118
|
return;
|
|
152
119
|
const ac = new AbortController();
|
|
153
120
|
watcherAbort = ac;
|
|
154
|
-
watcherHubRef = hubRef;
|
|
155
121
|
try {
|
|
156
122
|
const watcher = watch(planRoot, { recursive: true, signal: ac.signal }, (_event, filename) => {
|
|
157
123
|
if (filename && !filename.includes(".tmp.")) {
|
|
@@ -171,13 +137,6 @@ function stopWatcher() {
|
|
|
171
137
|
watcherAbort?.abort();
|
|
172
138
|
watcherAbort = null;
|
|
173
139
|
}
|
|
174
|
-
// ── Helpers ─────────────────────────────────────────────────────────
|
|
175
|
-
async function propagateTaskStatus(phaseId) {
|
|
176
|
-
// This is now handled by PlanStore.syncStatuses()
|
|
177
|
-
}
|
|
178
|
-
async function propagatePhaseStatus(featureId) {
|
|
179
|
-
// This is now handled by PlanStore.syncStatuses()
|
|
180
|
-
}
|
|
181
140
|
function createApiApp(store, hubRef, apiPrefix = "", uiConfig, isBusy) {
|
|
182
141
|
const hub = () => hubRef.current;
|
|
183
142
|
const route = (path) => `${apiPrefix}${path}`;
|
|
@@ -248,7 +207,12 @@ function createApiApp(store, hubRef, apiPrefix = "", uiConfig, isBusy) {
|
|
|
248
207
|
const body = await c.req.json();
|
|
249
208
|
const existingProject = await store.loadProject();
|
|
250
209
|
if (body.acceptedDecisions !== undefined && JSON.stringify(body.acceptedDecisions) !== JSON.stringify(existingProject.acceptedDecisions))
|
|
251
|
-
return c.json({ updated: false, errorCode:
|
|
210
|
+
return c.json({ updated: false, errorCode: ACCEPTED_DECISION_SEMANTIC_MUTATION_REQUIRED_ERROR_CODE, message: ACCEPTED_DECISION_RAW_REPLACEMENT_DISABLED_MESSAGE }, 400);
|
|
211
|
+
// The legacy free-form `decisions` array stays writable over HTTP. It is an
|
|
212
|
+
// agent-governance rule, and .planner/SKILL.md makes the Web UI a human-
|
|
213
|
+
// supervisor surface that may bypass agent-only gates. Blocking it here also
|
|
214
|
+
// blocked the only way to seed the legacy state the planner-load migration
|
|
215
|
+
// exists to consume. The Pi and MCP tools still refuse it.
|
|
252
216
|
const mutableFields = ["name", "goal", "description", "descriptionRef", "webPort", "scope", "outOfScope", "decisions", "globalRules", "technologies", "tools", "contentLanguage", "chatLanguage", "workflowRules", "projectGuidelines"];
|
|
253
217
|
if (!hasDefinedField(body, mutableFields))
|
|
254
218
|
return c.json(noMutableFieldsResponse(mutableFields), 400);
|
|
@@ -311,7 +275,7 @@ function createApiApp(store, hubRef, apiPrefix = "", uiConfig, isBusy) {
|
|
|
311
275
|
const body = await c.req.json();
|
|
312
276
|
if (!body.targetType)
|
|
313
277
|
return c.json({ error: "targetType required" }, 400);
|
|
314
|
-
const target = await
|
|
278
|
+
const target = await resolveAcceptedDecisionEventTarget(store, body.targetType, body.targetRef);
|
|
315
279
|
if (!target.ok)
|
|
316
280
|
return c.json({ error: target.error, created: false }, 404);
|
|
317
281
|
const createInput = {
|
|
@@ -333,7 +297,7 @@ function createApiApp(store, hubRef, apiPrefix = "", uiConfig, isBusy) {
|
|
|
333
297
|
const mutableFields = ["title", "decision", "rationale", "implementationNotes"];
|
|
334
298
|
if (!hasDefinedField(body, mutableFields))
|
|
335
299
|
return c.json(noMutableFieldsResponse(mutableFields), 400);
|
|
336
|
-
const target = await
|
|
300
|
+
const target = await resolveAcceptedDecisionEventTarget(store, body.targetType, body.targetRef);
|
|
337
301
|
if (!target.ok)
|
|
338
302
|
return c.json({ error: target.error, updated: false }, 404);
|
|
339
303
|
const updateInput = {
|
|
@@ -352,7 +316,7 @@ function createApiApp(store, hubRef, apiPrefix = "", uiConfig, isBusy) {
|
|
|
352
316
|
const body = await c.req.json().catch(() => ({}));
|
|
353
317
|
if (!body.targetType)
|
|
354
318
|
return c.json({ error: "targetType required", deleted: false }, 400);
|
|
355
|
-
const target = await
|
|
319
|
+
const target = await resolveAcceptedDecisionEventTarget(store, body.targetType, body.targetRef);
|
|
356
320
|
if (!target.ok)
|
|
357
321
|
return c.json({ error: target.error, deleted: false }, 404);
|
|
358
322
|
if (body.confirmed !== true)
|
|
@@ -639,7 +603,7 @@ function createApiApp(store, hubRef, apiPrefix = "", uiConfig, isBusy) {
|
|
|
639
603
|
if (body.status !== undefined && body.status !== existing.status)
|
|
640
604
|
return c.json({ updated: false, errorCode: "DERIVED_STATUS_READ_ONLY", message: "Feature status is derived from child phases and tasks and cannot be updated directly." }, 400);
|
|
641
605
|
if (body.acceptedDecisions !== undefined && JSON.stringify(body.acceptedDecisions) !== JSON.stringify(existing.acceptedDecisions))
|
|
642
|
-
return c.json({ updated: false, errorCode:
|
|
606
|
+
return c.json({ updated: false, errorCode: ACCEPTED_DECISION_SEMANTIC_MUTATION_REQUIRED_ERROR_CODE, message: ACCEPTED_DECISION_RAW_REPLACEMENT_DISABLED_MESSAGE }, 400);
|
|
643
607
|
const mutableFields = ["name", "description", "descriptionRef", "discussedAt", "contextReady", "contextReadyReason", "startDate", "endDate", "workDone", "workRemaining", "priority", "dependsOn"];
|
|
644
608
|
if (!hasDefinedField(body, mutableFields))
|
|
645
609
|
return c.json(noMutableFieldsResponse(mutableFields), 400);
|
|
@@ -668,11 +632,21 @@ function createApiApp(store, hubRef, apiPrefix = "", uiConfig, isBusy) {
|
|
|
668
632
|
}));
|
|
669
633
|
app.delete(route("/features/:id"), async (c) => {
|
|
670
634
|
const id = c.req.param("id");
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
});
|
|
675
|
-
|
|
635
|
+
if (!id)
|
|
636
|
+
return c.json({ error: "id required" }, 400);
|
|
637
|
+
const cascade = c.req.query("cascade") === "true";
|
|
638
|
+
const outcome = await deleteFeatureCascade(store, id, { cascade });
|
|
639
|
+
if (!outcome.ok) {
|
|
640
|
+
// FEATURE_NOT_FOUND: no such feature, or it vanished between resolve
|
|
641
|
+
// and write (race). Either way, this is a 404 — never a reported
|
|
642
|
+
// success for a delete that did not happen (same defect class as
|
|
643
|
+
// T413).
|
|
644
|
+
return c.json({ error: outcome.error }, 404);
|
|
645
|
+
}
|
|
646
|
+
const { result } = outcome;
|
|
647
|
+
for (const phase of result.phases) {
|
|
648
|
+
hub()?.broadcast({ type: "phases-updated", data: { action: phase.action === "deleted" ? "deleted" : "updated", id: phase.id, phaseId: phase.id, featureId: phase.action === "deleted" ? id : "" } });
|
|
649
|
+
}
|
|
676
650
|
hub()?.broadcast({ type: "features-updated", data: { action: "deleted", id, featureId: id } });
|
|
677
651
|
hub()?.broadcast({ type: "plan-rendered", data: {} });
|
|
678
652
|
await store.syncStatuses();
|
|
@@ -725,7 +699,6 @@ function createApiApp(store, hubRef, apiPrefix = "", uiConfig, isBusy) {
|
|
|
725
699
|
return c.json({ error: "featureId required: a phase must belong to a feature" }, 400);
|
|
726
700
|
let phase;
|
|
727
701
|
await store.runBatch(() => withFeatureLock(featureId, async () => {
|
|
728
|
-
const allPhases = await store.loadAllPhases();
|
|
729
702
|
const slug = normalizeSlug(title);
|
|
730
703
|
const id = createPhaseId();
|
|
731
704
|
const identity = await store.allocateEntityIdentity("phase", id);
|
|
@@ -816,7 +789,12 @@ function createApiApp(store, hubRef, apiPrefix = "", uiConfig, isBusy) {
|
|
|
816
789
|
if (body.status !== undefined && body.status !== existingPhase.status)
|
|
817
790
|
return c.json({ updated: false, errorCode: "DERIVED_STATUS_READ_ONLY", message: "Phase status is derived from child tasks and cannot be updated directly." }, 400);
|
|
818
791
|
if (body.acceptedDecisions !== undefined && JSON.stringify(body.acceptedDecisions) !== JSON.stringify(existingPhase.acceptedDecisions))
|
|
819
|
-
return c.json({ updated: false, errorCode:
|
|
792
|
+
return c.json({ updated: false, errorCode: ACCEPTED_DECISION_SEMANTIC_MUTATION_REQUIRED_ERROR_CODE, message: ACCEPTED_DECISION_RAW_REPLACEMENT_DISABLED_MESSAGE }, 400);
|
|
793
|
+
// The legacy free-form `decisions` array stays writable over HTTP. It is an
|
|
794
|
+
// agent-governance rule, and .planner/SKILL.md makes the Web UI a human-
|
|
795
|
+
// supervisor surface that may bypass agent-only gates. Blocking it here also
|
|
796
|
+
// blocked the only way to seed the legacy state the planner-load migration
|
|
797
|
+
// exists to consume. The Pi and MCP tools still refuse it.
|
|
820
798
|
let nextFeatureId = existingPhase.featureId;
|
|
821
799
|
if (body.featureId !== undefined) {
|
|
822
800
|
nextFeatureId = body.featureId.trim() || undefined;
|
|
@@ -1280,7 +1258,12 @@ function createApiApp(store, hubRef, apiPrefix = "", uiConfig, isBusy) {
|
|
|
1280
1258
|
if (!existing)
|
|
1281
1259
|
return c.json({ error: "task not found" }, 404);
|
|
1282
1260
|
if (body.acceptedDecisions !== undefined && JSON.stringify(body.acceptedDecisions) !== JSON.stringify(existing.acceptedDecisions))
|
|
1283
|
-
return c.json({ updated: false, errorCode:
|
|
1261
|
+
return c.json({ updated: false, errorCode: ACCEPTED_DECISION_SEMANTIC_MUTATION_REQUIRED_ERROR_CODE, message: ACCEPTED_DECISION_RAW_REPLACEMENT_DISABLED_MESSAGE }, 400);
|
|
1262
|
+
// The legacy free-form `decisions` array stays writable over HTTP. It is an
|
|
1263
|
+
// agent-governance rule, and .planner/SKILL.md makes the Web UI a human-
|
|
1264
|
+
// supervisor surface that may bypass agent-only gates. Blocking it here also
|
|
1265
|
+
// blocked the only way to seed the legacy state the planner-load migration
|
|
1266
|
+
// exists to consume. The Pi and MCP tools still refuse it.
|
|
1284
1267
|
const mutableFields = ["title", "description", "descriptionRef", "status", "notes", "decisions", "priority", "checklist", "subtasks"];
|
|
1285
1268
|
if (!hasDefinedField(body, mutableFields))
|
|
1286
1269
|
return c.json(noMutableFieldsResponse(mutableFields), 400);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-plan/server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Local HTTP/WebSocket server for Agent Plan dashboards and planner APIs.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"@hono/node-server": "^1.14.0",
|
|
42
42
|
"hono": "^4.7.0",
|
|
43
43
|
"ws": "^8.18.0",
|
|
44
|
-
"@agent-plan/core": "0.
|
|
44
|
+
"@agent-plan/core": "0.3.0"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@types/ws": "^8.18.0"
|