@agent-plan/server 0.2.19-next.2 → 0.2.19-next.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../src/serve.ts"],"names":[],"mappings":"AAWA,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAiHpC,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;AAsuBD,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,CAkFvE"}
1
+ {"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../src/serve.ts"],"names":[],"mappings":"AAWA,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAuIpC,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;AAo0BD,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,CAkFvE"}
package/dist/serve.js CHANGED
@@ -6,7 +6,7 @@ import { fileURLToPath } from "node:url";
6
6
  import { createAdaptorServer } from "@hono/node-server";
7
7
  import { Hono } from "hono";
8
8
  import { cors } from "hono/cors";
9
- import { ExportService, PlanStore, PlanStoreError, createFeatureId, createPhaseId, createShortId, createTaskId, normalizeSlug, withFeatureLock, needsMotivation } from "@agent-plan/core";
9
+ import { ExportService, PlanStore, PlanStoreError, createFeatureId, createPhaseId, createChecklistItemId, createShortId, createTaskId, findPhaseByRef, normalizeSlug, withFeatureLock, needsMotivation } from "@agent-plan/core";
10
10
  import { WsHub } from "./ws-hub.js";
11
11
  // ─── Watcher ────────────────────────────────────────────────────────────
12
12
  let watcherAbort = null;
@@ -14,6 +14,22 @@ let watcherHubRef = { current: null };
14
14
  function nowISO() {
15
15
  return new Date().toISOString();
16
16
  }
17
+ /** Resolve requirement phase refs before persistence so requirements never dangle. */
18
+ async function resolveRequirementPhaseIds(store, linkedPhaseIds) {
19
+ if (!Array.isArray(linkedPhaseIds) || linkedPhaseIds.length === 0) {
20
+ return { error: "linkedPhaseIds must contain at least one phase" };
21
+ }
22
+ const refs = linkedPhaseIds.map((entry) => typeof entry === "string" ? entry.trim() : "");
23
+ if (refs.some((ref) => !ref)) {
24
+ return { error: "linkedPhaseIds must contain non-empty phase references" };
25
+ }
26
+ const [phases, featuresDocument] = await Promise.all([store.loadAllPhases(), store.loadFeatures()]);
27
+ const resolved = refs.map((ref) => findPhaseByRef(phases, featuresDocument.features, ref));
28
+ const missing = resolved.findIndex((phase) => !phase);
29
+ if (missing !== -1)
30
+ return { error: `linked phase not found: ${refs[missing]}` };
31
+ return { linkedPhaseIds: [...new Set(resolved.map((phase) => phase.id))] };
32
+ }
17
33
  function nextTaskNumber(phase) {
18
34
  const numbers = phase.tasks.map((task) => task.number || 0).filter((n) => Number.isFinite(n));
19
35
  return (numbers.length > 0 ? Math.max(...numbers) : 0) + 1;
@@ -168,21 +184,29 @@ function createApiApp(store, hubRef, apiPrefix = "", uiConfig, isBusy) {
168
184
  app.get(route("/requirements"), async (c) => c.json(await store.loadRequirements()));
169
185
  app.post(route("/requirements"), async (c) => {
170
186
  const body = await c.req.json();
171
- const reqs = await store.updateRequirements((doc) => { doc.requirements.push(body); return doc; });
187
+ const links = await resolveRequirementPhaseIds(store, body.linkedPhaseIds);
188
+ if ("error" in links)
189
+ return c.json({ error: links.error }, 400);
190
+ const requirement = { ...body, linkedPhaseIds: links.linkedPhaseIds };
191
+ const reqs = await store.updateRequirements((doc) => { doc.requirements.push(requirement); return doc; });
172
192
  await store.writeGenerated();
173
193
  hub()?.broadcast({ type: "requirements-updated", data: reqs });
174
194
  hub()?.broadcast({ type: "plan-rendered", data: {} });
175
- return c.json(body, 201);
195
+ return c.json(requirement, 201);
176
196
  });
177
197
  app.put(route("/requirements/:id"), async (c) => {
178
198
  const id = c.req.param("id");
179
199
  const body = await c.req.json();
200
+ const links = await resolveRequirementPhaseIds(store, body.linkedPhaseIds);
201
+ if ("error" in links)
202
+ return c.json({ error: links.error }, 400);
203
+ const requirement = { ...body, linkedPhaseIds: links.linkedPhaseIds };
180
204
  let found = false;
181
205
  const reqs = await store.updateRequirements((doc) => {
182
206
  const idx = doc.requirements.findIndex((r) => r.id === id);
183
207
  if (idx === -1)
184
208
  return doc;
185
- doc.requirements[idx] = body;
209
+ doc.requirements[idx] = requirement;
186
210
  found = true;
187
211
  return doc;
188
212
  });
@@ -191,7 +215,22 @@ function createApiApp(store, hubRef, apiPrefix = "", uiConfig, isBusy) {
191
215
  await store.writeGenerated();
192
216
  hub()?.broadcast({ type: "requirements-updated", data: reqs });
193
217
  hub()?.broadcast({ type: "plan-rendered", data: {} });
194
- return c.json(body);
218
+ return c.json(requirement);
219
+ });
220
+ app.delete(route("/requirements/:id"), async (c) => {
221
+ const id = c.req.param("id");
222
+ let found = false;
223
+ const reqs = await store.updateRequirements((doc) => {
224
+ const next = doc.requirements.filter((requirement) => requirement.id !== id);
225
+ found = next.length !== doc.requirements.length;
226
+ return found ? { requirements: next } : doc;
227
+ });
228
+ if (!found)
229
+ return c.json({ error: "not found" }, 404);
230
+ await store.writeGenerated();
231
+ hub()?.broadcast({ type: "requirements-updated", data: reqs });
232
+ hub()?.broadcast({ type: "plan-rendered", data: {} });
233
+ return c.json({ deleted: id });
195
234
  });
196
235
  // ── Features ─────────────────────────────────────────────────────
197
236
  app.get(route("/features"), async (c) => c.json((await store.loadFeatures()).features));
@@ -222,6 +261,7 @@ function createApiApp(store, hubRef, apiPrefix = "", uiConfig, isBusy) {
222
261
  workRemaining: "",
223
262
  acceptedDecisions: [],
224
263
  phaseIds: [], dependsOn: [],
264
+ statusLog: [],
225
265
  createdAt: now,
226
266
  updatedAt: now,
227
267
  };
@@ -280,7 +320,7 @@ function createApiApp(store, hubRef, apiPrefix = "", uiConfig, isBusy) {
280
320
  });
281
321
  // ── Phases ───────────────────────────────────────────────────────
282
322
  app.get(route("/phases"), async (c) => {
283
- const allPhases = await store.loadAllPhases();
323
+ const allPhases = await store.loadAllPhasesWithRequirements();
284
324
  const featureId = c.req.query("featureId");
285
325
  if (featureId)
286
326
  return c.json(allPhases.filter((p) => p.featureId === featureId));
@@ -344,6 +384,9 @@ function createApiApp(store, hubRef, apiPrefix = "", uiConfig, isBusy) {
344
384
  updatedAt: now,
345
385
  handoff: "",
346
386
  handoffUpdatedAt: "",
387
+ handoffReadAt: "",
388
+ handoffHistory: [],
389
+ statusLog: [],
347
390
  };
348
391
  await store.savePhase(phase);
349
392
  // Link to feature if featureId provided
@@ -371,7 +414,7 @@ function createApiApp(store, hubRef, apiPrefix = "", uiConfig, isBusy) {
371
414
  if (!id)
372
415
  return c.json({ error: "id required" }, 400);
373
416
  try {
374
- return c.json(await store.loadPhase(id));
417
+ return c.json(await store.loadPhaseWithRequirements(id));
375
418
  }
376
419
  catch {
377
420
  return c.json({ error: "phase not found" }, 404);
@@ -444,8 +487,11 @@ function createApiApp(store, hubRef, apiPrefix = "", uiConfig, isBusy) {
444
487
  const initialStatus = body.status ?? "planned";
445
488
  const shortId = createShortId(await store.assignedShortIds());
446
489
  const priority = await store.nextPriority("task", phase.id);
490
+ const taskId = createTaskId();
491
+ const checklistItems = (body.checklist ?? []).map((s) => s.trim()).filter((s) => s.length > 0)
492
+ .map((item, index) => ({ id: createChecklistItemId(taskId, index + 1, item), number: index + 1, title: item, checked: false }));
447
493
  const task = {
448
- id: createTaskId(),
494
+ id: taskId,
449
495
  phaseId: phase.id,
450
496
  number: taskNumber,
451
497
  shortId,
@@ -458,7 +504,7 @@ function createApiApp(store, hubRef, apiPrefix = "", uiConfig, isBusy) {
458
504
  statusLog: [],
459
505
  decisions: [],
460
506
  acceptedDecisions: [],
461
- checklist: [],
507
+ checklist: checklistItems,
462
508
  subtasks: [],
463
509
  dependsOn: [],
464
510
  startedAt: initialStatus === "in-progress" || initialStatus === "done" ? now : "",
@@ -642,20 +688,52 @@ function createApiApp(store, hubRef, apiPrefix = "", uiConfig, isBusy) {
642
688
  });
643
689
  // ── Reorder (priority) ───────────────────────────────────────────
644
690
  app.post(route("/reorder"), async (c) => {
691
+ // Midpoint-insert reorder: only the moved item's priority changes (when
692
+ // the gap between its new neighbours allows it). When the gap is exhausted
693
+ // (≤1, e.g. all-zero defaults or tight spacing), the segment is reindexed
694
+ // with GAP. Keeps priority as int (no schema migration) and minimises the
695
+ // set of changed items per drag.
645
696
  const body = await c.req.json();
646
- const { kind, ids } = body;
647
- if (!Array.isArray(ids) || ids.length === 0)
648
- return c.json({ error: "ids required" }, 400);
649
- const gap = 10;
697
+ const { kind, movedId, beforeId, afterId } = body;
698
+ if (!movedId)
699
+ return c.json({ error: "movedId required" }, 400);
700
+ const GAP = 10;
701
+ // Reindex a list in-place: reconstruct the desired order (moved removed +
702
+ // reinserted between beforeId and afterId) then assign (i+1)*GAP.
703
+ const reindexList = (all) => {
704
+ const sorted = [...all].sort((a, b) => a.priority - b.priority || a.number - b.number);
705
+ const moved = sorted.find((x) => x.id === movedId);
706
+ const withoutMoved = sorted.filter((x) => x.id !== movedId);
707
+ let insertIdx;
708
+ if (beforeId) {
709
+ const bi = withoutMoved.findIndex((x) => x.id === beforeId);
710
+ insertIdx = bi === -1 ? withoutMoved.length : bi + 1;
711
+ }
712
+ else {
713
+ insertIdx = 0;
714
+ }
715
+ if (moved)
716
+ withoutMoved.splice(insertIdx, 0, moved);
717
+ withoutMoved.forEach((item, i) => { item.priority = (i + 1) * GAP; });
718
+ };
650
719
  if (kind === "feature") {
651
720
  // Priority-only: suspend status rollup so reordering never flips a
652
721
  // partially-done feature to in-progress (priority ≠ status).
653
722
  await store.runBatch(async () => {
654
723
  await store.updateFeatures((doc) => {
655
- for (const [i, id] of ids.entries()) {
656
- const f = doc.features.find((x) => x.id === id);
657
- if (f)
658
- f.priority = (i + 1) * gap;
724
+ const moved = doc.features.find((x) => x.id === movedId);
725
+ if (!moved)
726
+ return doc;
727
+ const before = beforeId ? doc.features.find((x) => x.id === beforeId) : null;
728
+ const after = afterId ? doc.features.find((x) => x.id === afterId) : null;
729
+ const beforeP = before ? before.priority : 0;
730
+ const maxP = doc.features.reduce((m, f) => Math.max(m, f.priority), 0);
731
+ const afterP = after ? after.priority : (maxP + GAP);
732
+ if (afterP - beforeP > 1) {
733
+ moved.priority = Math.floor((beforeP + afterP) / 2);
734
+ }
735
+ else {
736
+ reindexList(doc.features);
659
737
  }
660
738
  doc.features.sort((a, b) => a.priority - b.priority || a.number - b.number);
661
739
  return doc;
@@ -665,10 +743,36 @@ function createApiApp(store, hubRef, apiPrefix = "", uiConfig, isBusy) {
665
743
  }
666
744
  else if (kind === "phase") {
667
745
  const phases = await store.loadAllPhases();
746
+ const moved = phases.find((p) => p.id === movedId);
747
+ if (!moved)
748
+ return c.json({ error: "moved phase not found" }, 404);
749
+ const siblings = phases.filter((p) => p.featureId === moved.featureId);
750
+ const before = beforeId ? siblings.find((p) => p.id === beforeId) : null;
751
+ const after = afterId ? siblings.find((p) => p.id === afterId) : null;
752
+ const beforeP = before ? before.priority : 0;
753
+ const maxP = siblings.reduce((m, p) => Math.max(m, p.priority), 0);
754
+ const afterP = after ? after.priority : (maxP + GAP);
668
755
  await store.runBatch(async () => {
669
- for (const [i, id] of ids.entries()) {
670
- if (phases.some((x) => x.id === id)) {
671
- await store.updatePhase(id, (entry) => { entry.priority = (i + 1) * gap; return entry; });
756
+ if (afterP - beforeP > 1) {
757
+ await store.updatePhase(movedId, (entry) => { entry.priority = Math.floor((beforeP + afterP) / 2); return entry; });
758
+ }
759
+ else {
760
+ // Reindex all siblings (each phase is a separate file)
761
+ const sorted = [...siblings].sort((a, b) => a.priority - b.priority || a.number - b.number);
762
+ const withoutMoved = sorted.filter((x) => x.id !== movedId);
763
+ let insertIdx;
764
+ if (beforeId) {
765
+ const bi = withoutMoved.findIndex((x) => x.id === beforeId);
766
+ insertIdx = bi === -1 ? withoutMoved.length : bi + 1;
767
+ }
768
+ else {
769
+ insertIdx = 0;
770
+ }
771
+ const movedEntry = sorted.find((x) => x.id === movedId);
772
+ if (movedEntry)
773
+ withoutMoved.splice(insertIdx, 0, movedEntry);
774
+ for (const [i, p] of withoutMoved.entries()) {
775
+ await store.updatePhase(p.id, (entry) => { entry.priority = (i + 1) * GAP; return entry; });
672
776
  }
673
777
  }
674
778
  });
@@ -676,14 +780,23 @@ function createApiApp(store, hubRef, apiPrefix = "", uiConfig, isBusy) {
676
780
  }
677
781
  else if (kind === "task") {
678
782
  const allPhases = await store.loadAllPhases();
679
- const host = allPhases.find((p) => p.tasks.some((t) => ids.includes(t.id)));
783
+ const host = allPhases.find((p) => p.tasks.some((t) => t.id === movedId));
680
784
  if (host) {
681
785
  await store.runBatch(async () => {
682
786
  await store.updatePhase(host.id, (phase) => {
683
- for (const [i, id] of ids.entries()) {
684
- const t = phase.tasks.find((x) => x.id === id);
685
- if (t)
686
- t.priority = (i + 1) * gap;
787
+ const moved = phase.tasks.find((x) => x.id === movedId);
788
+ if (!moved)
789
+ return phase;
790
+ const before = beforeId ? phase.tasks.find((x) => x.id === beforeId) : null;
791
+ const after = afterId ? phase.tasks.find((x) => x.id === afterId) : null;
792
+ const beforeP = before ? before.priority : 0;
793
+ const maxP = phase.tasks.reduce((m, t) => Math.max(m, t.priority), 0);
794
+ const afterP = after ? after.priority : (maxP + GAP);
795
+ if (afterP - beforeP > 1) {
796
+ moved.priority = Math.floor((beforeP + afterP) / 2);
797
+ }
798
+ else {
799
+ reindexList(phase.tasks);
687
800
  }
688
801
  phase.tasks.sort((a, b) => a.priority - b.priority || a.number - b.number);
689
802
  phase.taskIds = phase.tasks.map((t) => t.id);
@@ -698,7 +811,7 @@ function createApiApp(store, hubRef, apiPrefix = "", uiConfig, isBusy) {
698
811
  }
699
812
  await store.writeGenerated();
700
813
  hub()?.broadcast({ type: "plan-rendered", data: {} });
701
- return c.json({ ok: true, kind, count: ids.length });
814
+ return c.json({ ok: true, kind, movedId });
702
815
  });
703
816
  // ── Render ───────────────────────────────────────────────────────
704
817
  app.post(route("/render"), async (c) => {
@@ -711,6 +824,10 @@ function createApiApp(store, hubRef, apiPrefix = "", uiConfig, isBusy) {
711
824
  const list = await store.listHandoffs();
712
825
  return c.json({ handoffs: list });
713
826
  });
827
+ app.get(route("/handoffs/archive"), async (c) => {
828
+ const archived = await store.listArchivedHandoffs();
829
+ return c.json({ archived });
830
+ });
714
831
  app.get(route("/phases/:id/handoff"), async (c) => {
715
832
  const id = c.req.param("id");
716
833
  if (!id)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-plan/server",
3
- "version": "0.2.19-next.2",
3
+ "version": "0.2.19-next.21",
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.2.19-next.2"
44
+ "@agent-plan/core": "0.2.19-next.21"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@types/ws": "^8.18.0"