@agent-plan/core 0.2.19-next.16 → 0.2.19-next.18

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.
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Derived parent display-status layer.
3
+ *
4
+ * Parent entities (feature, phase) need a presentation-oriented summary of
5
+ * their children's workflow states that reads clearly in the Web UI, without
6
+ * overloading the canonical workflow statuses persisted in planner data.
7
+ *
8
+ * This module introduces two PARENT-ONLY derived presentation states:
9
+ * - `started`: the entity has clearly begun (historical progress exists) but
10
+ * no child is active now, and the unfinished remainder is mixed or cannot
11
+ * honestly collapse to one specific workflow label.
12
+ * - `closed`: every child is terminal, but outcomes are mixed (e.g.
13
+ * `done + canceled`, `canceled + rejected`).
14
+ *
15
+ * These are NEVER persisted: they are computed on demand from children's
16
+ * canonical workflow statuses. The canonical workflow model
17
+ * (`planned | in-progress | waiting | blocked | deferred | done | canceled |
18
+ * rejected`) is unchanged.
19
+ */
20
+ import type { TaskStatus, PhaseStatus } from "./schema.js";
21
+ /**
22
+ * Canonical workflow status values used by tasks, phases, and features.
23
+ * Mirrors the union of TaskStatus and PhaseStatus used in the planner data.
24
+ */
25
+ export type WorkflowStatus = "planned" | "in-progress" | "waiting" | "blocked" | "deferred" | "done" | "canceled" | "rejected";
26
+ /**
27
+ * Display status adds parent-only presentation states on top of workflow
28
+ * statuses. Only parent entities (feature, phase) use the full union; leaf
29
+ * tasks always use plain WorkflowStatus.
30
+ */
31
+ export type DisplayStatus = WorkflowStatus | "started" | "closed";
32
+ /** Per-status counts for a set of children. */
33
+ export interface StatusBreakdown {
34
+ planned: number;
35
+ inProgress: number;
36
+ waiting: number;
37
+ blocked: number;
38
+ deferred: number;
39
+ done: number;
40
+ canceled: number;
41
+ rejected: number;
42
+ }
43
+ /** Derived presentation snapshot for a parent entity. */
44
+ export interface ParentDisplay {
45
+ /** Single presentation status for badges/summaries. */
46
+ displayStatus: DisplayStatus;
47
+ /** Counts of children per canonical workflow status. */
48
+ breakdown: StatusBreakdown;
49
+ /** True when at least one meaningful child is not `planned`
50
+ * (i.e. the entity has clearly begun). */
51
+ hasStarted: boolean;
52
+ /** Total number of children (including canceled/rejected). */
53
+ totalChildren: number;
54
+ /** Meaningful children count (canceled/rejected excluded). */
55
+ meaningfulChildren: number;
56
+ }
57
+ /** Count children per canonical workflow status. Pure; does not mutate input. */
58
+ export declare function countBreakdown(statuses: readonly WorkflowStatus[]): StatusBreakdown;
59
+ /**
60
+ * Derive the parent display snapshot from its children's canonical statuses.
61
+ *
62
+ * Algorithm (locked by P039 accepted decisions):
63
+ * 1. If any meaningful child is active now → `in-progress`.
64
+ * 2. If every child is terminal:
65
+ * - all `done` → `done`
66
+ * - all `canceled` → `canceled`
67
+ * - all `rejected` → `rejected`
68
+ * - otherwise → `closed` (mixed terminal outcomes)
69
+ * 3. Compute `unfinished = meaningful ∩ OPEN`.
70
+ * - homogeneous `waiting` → `waiting`
71
+ * - homogeneous `blocked` → `blocked`
72
+ * - homogeneous `deferred` → `deferred`
73
+ * 4. If all unfinished are `planned` and the entity has not started → `planned`.
74
+ * 5. Fallback → `started` (mixed non-active remainder, or planned with
75
+ * historical progress).
76
+ *
77
+ * Edge cases:
78
+ * - empty input → `planned` (a parent with no meaningful children yet reads
79
+ * as not started; this is the least surprising default for empty phases).
80
+ * - `meaningful` excludes `canceled` and `rejected` (terminal non-positive).
81
+ * - `hasStarted = meaningful.some(s => s !== "planned")`.
82
+ *
83
+ * Pure and non-persisting. Never mutates the input array.
84
+ */
85
+ export declare function deriveParentDisplay(childStatuses: readonly WorkflowStatus[]): ParentDisplay;
86
+ /**
87
+ * Narrow an arbitrary canonical status string to a WorkflowStatus.
88
+ * Useful for adapters that hold the union of task/phase statuses.
89
+ * Returns `null` when the value is not a recognized workflow status.
90
+ */
91
+ export declare function toWorkflowStatus(value: string): WorkflowStatus | null;
92
+ /**
93
+ * Convert a canonical task/phase status into the workflow status union used
94
+ * by the display layer. Accepts the extra phase statuses and maps them:
95
+ * - `discovery` → `in-progress` (a phase in discovery is active work)
96
+ * - `draft` → `planned` (a draft phase has no tasks yet → not started)
97
+ */
98
+ export declare function fromCanonicalStatus(status: TaskStatus | PhaseStatus): WorkflowStatus;
99
+ //# sourceMappingURL=display-status.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"display-status.d.ts","sourceRoot":"","sources":["../src/display-status.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE3D;;;GAGG;AACH,MAAM,MAAM,cAAc,GACtB,SAAS,GACT,aAAa,GACb,SAAS,GACT,SAAS,GACT,UAAU,GACV,MAAM,GACN,UAAU,GACV,UAAU,CAAC;AAEf;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG,cAAc,GAAG,SAAS,GAAG,QAAQ,CAAC;AAElE,+CAA+C;AAC/C,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,yDAAyD;AACzD,MAAM,WAAW,aAAa;IAC5B,uDAAuD;IACvD,aAAa,EAAE,aAAa,CAAC;IAC7B,wDAAwD;IACxD,SAAS,EAAE,eAAe,CAAC;IAC3B;+CAC2C;IAC3C,UAAU,EAAE,OAAO,CAAC;IACpB,8DAA8D;IAC9D,aAAa,EAAE,MAAM,CAAC;IACtB,8DAA8D;IAC9D,kBAAkB,EAAE,MAAM,CAAC;CAC5B;AAMD,iFAAiF;AACjF,wBAAgB,cAAc,CAAC,QAAQ,EAAE,SAAS,cAAc,EAAE,GAAG,eAAe,CAkBnF;AAMD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,mBAAmB,CAAC,aAAa,EAAE,SAAS,cAAc,EAAE,GAAG,aAAa,CAiE3F;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CAcrE;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,UAAU,GAAG,WAAW,GAAG,cAAc,CAIpF"}
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Derived parent display-status layer.
3
+ *
4
+ * Parent entities (feature, phase) need a presentation-oriented summary of
5
+ * their children's workflow states that reads clearly in the Web UI, without
6
+ * overloading the canonical workflow statuses persisted in planner data.
7
+ *
8
+ * This module introduces two PARENT-ONLY derived presentation states:
9
+ * - `started`: the entity has clearly begun (historical progress exists) but
10
+ * no child is active now, and the unfinished remainder is mixed or cannot
11
+ * honestly collapse to one specific workflow label.
12
+ * - `closed`: every child is terminal, but outcomes are mixed (e.g.
13
+ * `done + canceled`, `canceled + rejected`).
14
+ *
15
+ * These are NEVER persisted: they are computed on demand from children's
16
+ * canonical workflow statuses. The canonical workflow model
17
+ * (`planned | in-progress | waiting | blocked | deferred | done | canceled |
18
+ * rejected`) is unchanged.
19
+ */
20
+ const ACTIVE = new Set(["in-progress"]);
21
+ const OPEN = new Set(["planned", "waiting", "blocked", "deferred"]);
22
+ const TERMINAL = new Set(["done", "canceled", "rejected"]);
23
+ /** Count children per canonical workflow status. Pure; does not mutate input. */
24
+ export function countBreakdown(statuses) {
25
+ const breakdown = {
26
+ planned: 0, inProgress: 0, waiting: 0, blocked: 0,
27
+ deferred: 0, done: 0, canceled: 0, rejected: 0,
28
+ };
29
+ for (const s of statuses) {
30
+ switch (s) {
31
+ case "planned":
32
+ breakdown.planned++;
33
+ break;
34
+ case "in-progress":
35
+ breakdown.inProgress++;
36
+ break;
37
+ case "waiting":
38
+ breakdown.waiting++;
39
+ break;
40
+ case "blocked":
41
+ breakdown.blocked++;
42
+ break;
43
+ case "deferred":
44
+ breakdown.deferred++;
45
+ break;
46
+ case "done":
47
+ breakdown.done++;
48
+ break;
49
+ case "canceled":
50
+ breakdown.canceled++;
51
+ break;
52
+ case "rejected":
53
+ breakdown.rejected++;
54
+ break;
55
+ }
56
+ }
57
+ return breakdown;
58
+ }
59
+ function emptyBreakdown() {
60
+ return { planned: 0, inProgress: 0, waiting: 0, blocked: 0, deferred: 0, done: 0, canceled: 0, rejected: 0 };
61
+ }
62
+ /**
63
+ * Derive the parent display snapshot from its children's canonical statuses.
64
+ *
65
+ * Algorithm (locked by P039 accepted decisions):
66
+ * 1. If any meaningful child is active now → `in-progress`.
67
+ * 2. If every child is terminal:
68
+ * - all `done` → `done`
69
+ * - all `canceled` → `canceled`
70
+ * - all `rejected` → `rejected`
71
+ * - otherwise → `closed` (mixed terminal outcomes)
72
+ * 3. Compute `unfinished = meaningful ∩ OPEN`.
73
+ * - homogeneous `waiting` → `waiting`
74
+ * - homogeneous `blocked` → `blocked`
75
+ * - homogeneous `deferred` → `deferred`
76
+ * 4. If all unfinished are `planned` and the entity has not started → `planned`.
77
+ * 5. Fallback → `started` (mixed non-active remainder, or planned with
78
+ * historical progress).
79
+ *
80
+ * Edge cases:
81
+ * - empty input → `planned` (a parent with no meaningful children yet reads
82
+ * as not started; this is the least surprising default for empty phases).
83
+ * - `meaningful` excludes `canceled` and `rejected` (terminal non-positive).
84
+ * - `hasStarted = meaningful.some(s => s !== "planned")`.
85
+ *
86
+ * Pure and non-persisting. Never mutates the input array.
87
+ */
88
+ export function deriveParentDisplay(childStatuses) {
89
+ const statuses = childStatuses;
90
+ const breakdown = countBreakdown(statuses);
91
+ const totalChildren = statuses.length;
92
+ if (totalChildren === 0) {
93
+ return {
94
+ displayStatus: "planned",
95
+ breakdown: emptyBreakdown(),
96
+ hasStarted: false,
97
+ totalChildren: 0,
98
+ meaningfulChildren: 0,
99
+ };
100
+ }
101
+ // Meaningful set excludes terminal non-positive outcomes.
102
+ const meaningful = statuses.filter((s) => s !== "canceled" && s !== "rejected");
103
+ const meaningfulChildren = meaningful.length;
104
+ const hasStarted = meaningful.some((s) => s !== "planned");
105
+ // 1. Active child work exists now.
106
+ if (meaningful.some((s) => ACTIVE.has(s))) {
107
+ return { displayStatus: "in-progress", breakdown, hasStarted, totalChildren, meaningfulChildren };
108
+ }
109
+ // 2. All children are terminal.
110
+ if (statuses.every((s) => TERMINAL.has(s))) {
111
+ if (statuses.every((s) => s === "done")) {
112
+ return { displayStatus: "done", breakdown, hasStarted, totalChildren, meaningfulChildren };
113
+ }
114
+ if (statuses.every((s) => s === "canceled")) {
115
+ return { displayStatus: "canceled", breakdown, hasStarted, totalChildren, meaningfulChildren };
116
+ }
117
+ if (statuses.every((s) => s === "rejected")) {
118
+ return { displayStatus: "rejected", breakdown, hasStarted, totalChildren, meaningfulChildren };
119
+ }
120
+ return { displayStatus: "closed", breakdown, hasStarted, totalChildren, meaningfulChildren };
121
+ }
122
+ // 3. Unfinished meaningful remainder.
123
+ const unfinished = meaningful.filter((s) => OPEN.has(s));
124
+ const allWaiting = unfinished.length > 0 && unfinished.every((s) => s === "waiting");
125
+ if (allWaiting) {
126
+ return { displayStatus: "waiting", breakdown, hasStarted, totalChildren, meaningfulChildren };
127
+ }
128
+ const allBlocked = unfinished.length > 0 && unfinished.every((s) => s === "blocked");
129
+ if (allBlocked) {
130
+ return { displayStatus: "blocked", breakdown, hasStarted, totalChildren, meaningfulChildren };
131
+ }
132
+ const allDeferred = unfinished.length > 0 && unfinished.every((s) => s === "deferred");
133
+ if (allDeferred) {
134
+ return { displayStatus: "deferred", breakdown, hasStarted, totalChildren, meaningfulChildren };
135
+ }
136
+ // 4. All unfinished are planned and the entity has never started.
137
+ const allPlanned = unfinished.length > 0 && unfinished.every((s) => s === "planned");
138
+ if (allPlanned && !hasStarted) {
139
+ return { displayStatus: "planned", breakdown, hasStarted, totalChildren, meaningfulChildren };
140
+ }
141
+ // 5. Fallback: started (mixed non-active remainder, or planned with history).
142
+ return { displayStatus: "started", breakdown, hasStarted, totalChildren, meaningfulChildren };
143
+ }
144
+ /**
145
+ * Narrow an arbitrary canonical status string to a WorkflowStatus.
146
+ * Useful for adapters that hold the union of task/phase statuses.
147
+ * Returns `null` when the value is not a recognized workflow status.
148
+ */
149
+ export function toWorkflowStatus(value) {
150
+ switch (value) {
151
+ case "planned":
152
+ case "in-progress":
153
+ case "waiting":
154
+ case "blocked":
155
+ case "deferred":
156
+ case "done":
157
+ case "canceled":
158
+ case "rejected":
159
+ return value;
160
+ default:
161
+ return null;
162
+ }
163
+ }
164
+ /**
165
+ * Convert a canonical task/phase status into the workflow status union used
166
+ * by the display layer. Accepts the extra phase statuses and maps them:
167
+ * - `discovery` → `in-progress` (a phase in discovery is active work)
168
+ * - `draft` → `planned` (a draft phase has no tasks yet → not started)
169
+ */
170
+ export function fromCanonicalStatus(status) {
171
+ if (status === "discovery")
172
+ return "in-progress";
173
+ if (status === "draft")
174
+ return "planned";
175
+ return status;
176
+ }
package/dist/index.d.ts CHANGED
@@ -3,7 +3,8 @@ export * from "./refs.js";
3
3
  export * from "./schema.js";
4
4
  export * from "./checklist.js";
5
5
  export * from "./recap.js";
6
- export { PlanStore, PlanStoreError, setWriteBusyHook, setWriteNotifyHook, migrateToUuids, migrateToGlobalSequence, withFeatureLock, type PhaseHandoffSummary } from "./plan-store.js";
6
+ export * from "./display-status.js";
7
+ export { PlanStore, PlanStoreError, setWriteBusyHook, setWriteNotifyHook, migrateToUuids, migrateToGlobalSequence, withFeatureLock, type PhaseHandoffSummary, type OrphanPhaseSummary } from "./plan-store.js";
7
8
  export { PlanRenderer } from "./renderer.js";
8
9
  export { ExportService } from "./export-service.js";
9
10
  export type { CodebaseProfile, ResumeFocus, ActivityEntry, ActivityLog, AmbientFacts } from "./schema.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,YAAY,CAAC;AAC3B,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,cAAc,EAAE,uBAAuB,EAAE,eAAe,EAAE,KAAK,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AACtL,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,YAAY,EAAE,eAAe,EAAE,WAAW,EAAE,aAAa,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,YAAY,CAAC;AAC3B,cAAc,qBAAqB,CAAC;AACpC,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,cAAc,EAAE,uBAAuB,EAAE,eAAe,EAAE,KAAK,mBAAmB,EAAE,KAAK,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAC/M,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,YAAY,EAAE,eAAe,EAAE,WAAW,EAAE,aAAa,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC"}
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@ export * from "./refs.js";
3
3
  export * from "./schema.js";
4
4
  export * from "./checklist.js";
5
5
  export * from "./recap.js";
6
+ export * from "./display-status.js";
6
7
  export { PlanStore, PlanStoreError, setWriteBusyHook, setWriteNotifyHook, migrateToUuids, migrateToGlobalSequence, withFeatureLock } from "./plan-store.js";
7
8
  export { PlanRenderer } from "./renderer.js";
8
9
  export { ExportService } from "./export-service.js";
package/dist/naming.d.ts CHANGED
@@ -3,6 +3,20 @@
3
3
  export declare const CROCKFORD_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
4
4
  export declare const SHORT_ID_LENGTH = 5;
5
5
  export declare const SHORT_ID_PATTERN: RegExp;
6
+ /** Loose UUID v4 regex (case-insensitive). Used for input sanity checks. */
7
+ export declare const UUID_PATTERN: RegExp;
8
+ export declare function isUuid(value: unknown): value is string;
9
+ /** Belt-and-suspenders validation: a resolved ref must be a real UUID and the
10
+ * target must still exist in the store before we allocate numbers or write.
11
+ * Used by adapter create tools (task_create, phase_create). */
12
+ export declare function validateResolvedTarget<T extends {
13
+ id: string;
14
+ }>(kind: "feature" | "phase", resolvedId: string, loader: () => Promise<T | undefined>): Promise<{
15
+ ok: true;
16
+ } | {
17
+ ok: false;
18
+ error: string;
19
+ }>;
6
20
  /** Generate a globally-unique short id (5 chars, Crockford Base32, e.g. `UUXD1`-style
7
21
  * but without 0/1/I/O). Retries until the id is not in `existing` (project-scoped
8
22
  * collision guard). Throws only in the impossible saturation case (~50 retries). */
@@ -1 +1 @@
1
- {"version":3,"file":"naming.d.ts","sourceRoot":"","sources":["../src/naming.ts"],"names":[],"mappings":"AAKA;iFACiF;AACjF,eAAO,MAAM,kBAAkB,qCAAqC,CAAC;AAErE,eAAO,MAAM,eAAe,IAAI,CAAC;AACjC,eAAO,MAAM,gBAAgB,QAAkB,CAAC;AAEhD;;qFAEqF;AACrF,wBAAgB,aAAa,CAAC,QAAQ,GAAE,GAAG,CAAC,MAAM,CAAa,GAAG,MAAM,CAUvE;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAOnD;AAED;;;;8BAI8B;AAC9B,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,SAAK,EAAE,QAAQ,SAAa,GAAG,MAAM,CAGnF;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE5D;AAED;;iBAEiB;AACjB,wBAAgB,cAAc,CAAC,WAAW,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,MAAM,GAAG,MAAM,CAGlF;AAED,4DAA4D;AAC5D,wBAAgB,gBAAgB,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,CAE9D;AAED,oFAAoF;AACpF,wBAAgB,oBAAoB,CAClC,KAAK,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAA;CAAE,EAChD,QAAQ,EAAE;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,EAAE,GACzC,MAAM,GAAG,SAAS,CAEpB;AAED,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAED,mFAAmF;AACnF,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAExD;AAED,0FAA0F;AAC1F,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAGtF;AAED,wBAAgB,YAAY,IAAI,MAAM,CAErC;AAED,wBAAgB,mBAAmB,IAAI,MAAM,CAE5C;AAED,wBAAgB,iBAAiB,IAAI,MAAM,CAE1C;AAED,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAE3F;AAED,wBAAgB,sBAAsB,IAAI,MAAM,CAE/C"}
1
+ {"version":3,"file":"naming.d.ts","sourceRoot":"","sources":["../src/naming.ts"],"names":[],"mappings":"AAKA;iFACiF;AACjF,eAAO,MAAM,kBAAkB,qCAAqC,CAAC;AAErE,eAAO,MAAM,eAAe,IAAI,CAAC;AACjC,eAAO,MAAM,gBAAgB,QAAkB,CAAC;AAEhD,4EAA4E;AAC5E,eAAO,MAAM,YAAY,QAAoE,CAAC;AAE9F,wBAAgB,MAAM,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAEtD;AAED;;gEAEgE;AAChE,wBAAsB,sBAAsB,CAAC,CAAC,SAAS;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,EACnE,IAAI,EAAE,SAAS,GAAG,OAAO,EACzB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC,GACnC,OAAO,CAAC;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAStD;AAGD;;qFAEqF;AACrF,wBAAgB,aAAa,CAAC,QAAQ,GAAE,GAAG,CAAC,MAAM,CAAa,GAAG,MAAM,CAUvE;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAOnD;AAED;;;;8BAI8B;AAC9B,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,SAAK,EAAE,QAAQ,SAAa,GAAG,MAAM,CAGnF;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE5D;AAED;;iBAEiB;AACjB,wBAAgB,cAAc,CAAC,WAAW,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,MAAM,GAAG,MAAM,CAGlF;AAED,4DAA4D;AAC5D,wBAAgB,gBAAgB,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,CAE9D;AAED,oFAAoF;AACpF,wBAAgB,oBAAoB,CAClC,KAAK,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAA;CAAE,EAChD,QAAQ,EAAE;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,EAAE,GACzC,MAAM,GAAG,SAAS,CAEpB;AAED,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAED,mFAAmF;AACnF,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAExD;AAED,0FAA0F;AAC1F,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAGtF;AAED,wBAAgB,YAAY,IAAI,MAAM,CAErC;AAED,wBAAgB,mBAAmB,IAAI,MAAM,CAE5C;AAED,wBAAgB,iBAAiB,IAAI,MAAM,CAE1C;AAED,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAE3F;AAED,wBAAgB,sBAAsB,IAAI,MAAM,CAE/C"}
package/dist/naming.js CHANGED
@@ -6,6 +6,24 @@ const MULTI_DASH_PATTERN = /-+/g;
6
6
  export const CROCKFORD_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
7
7
  export const SHORT_ID_LENGTH = 5;
8
8
  export const SHORT_ID_PATTERN = /^[A-Z2-9]{5}$/;
9
+ /** Loose UUID v4 regex (case-insensitive). Used for input sanity checks. */
10
+ export const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
11
+ export function isUuid(value) {
12
+ return typeof value === "string" && UUID_PATTERN.test(value);
13
+ }
14
+ /** Belt-and-suspenders validation: a resolved ref must be a real UUID and the
15
+ * target must still exist in the store before we allocate numbers or write.
16
+ * Used by adapter create tools (task_create, phase_create). */
17
+ export async function validateResolvedTarget(kind, resolvedId, loader) {
18
+ if (!isUuid(resolvedId)) {
19
+ return { ok: false, error: `Resolved ${kind} id is not a valid UUID: ${resolvedId}` };
20
+ }
21
+ const target = await loader();
22
+ if (!target) {
23
+ return { ok: false, error: `Resolved ${kind} ${resolvedId} no longer exists. Refusing to create child.` };
24
+ }
25
+ return { ok: true };
26
+ }
9
27
  /** Generate a globally-unique short id (5 chars, Crockford Base32, e.g. `UUXD1`-style
10
28
  * but without 0/1/I/O). Retries until the id is not in `existing` (project-scoped
11
29
  * collision guard). Throws only in the impossible saturation case (~50 retries). */
@@ -1,7 +1,26 @@
1
- import { type Feature, type FeaturesDocument, type Manifest, type Phase, type PlanWorkspace, type Project, type RequirementsDocument, type ActivityEntry, type ActivityLog, type CodebaseProfile, type ResumeFocus } from "./schema.js";
1
+ import { type Feature, type FeaturesDocument, type Manifest, type Phase, type PlanWorkspace, type Project, type RequirementsDocument, type Requirement, type ActivityEntry, type ActivityLog, type CodebaseProfile, type ResumeFocus } from "./schema.js";
2
+ import { type ParentDisplay } from "./display-status.js";
3
+ export interface PlanStoreErrorDetails {
4
+ path?: string;
5
+ operation?: string;
6
+ backupTried?: boolean;
7
+ backupFailed?: boolean;
8
+ jsonParseError?: {
9
+ message: string;
10
+ line?: number;
11
+ column?: number;
12
+ };
13
+ validationErrors?: {
14
+ path: string;
15
+ message: string;
16
+ }[];
17
+ rawPreview?: string;
18
+ [key: string]: unknown;
19
+ }
2
20
  export declare class PlanStoreError extends Error {
3
21
  readonly cause?: unknown | undefined;
4
- constructor(message: string, cause?: unknown | undefined);
22
+ readonly details?: PlanStoreErrorDetails | undefined;
23
+ constructor(message: string, cause?: unknown | undefined, details?: PlanStoreErrorDetails | undefined);
5
24
  }
6
25
  export declare function setWriteBusyHook(hook: ((busy: boolean) => void) | undefined): void;
7
26
  export declare function setWriteNotifyHook(hook: (() => void) | undefined): void;
@@ -21,6 +40,18 @@ export interface PhaseHandoffSummary {
21
40
  * inline without a per-phase fetch. */
22
41
  content: string;
23
42
  }
43
+ /** Summary of a phase file that exists on disk but no longer resolves to a
44
+ * valid owning feature. Missing back-links alone do NOT make a phase orphan:
45
+ * if `phase.featureId` still resolves to a known feature, repair can relink
46
+ * it. */
47
+ export interface OrphanPhaseSummary {
48
+ phaseId: string;
49
+ featureId?: string | undefined;
50
+ shortId?: string | undefined;
51
+ compositeRef: string;
52
+ title: string;
53
+ reason: string;
54
+ }
24
55
  export declare function migrateToUuids(store: PlanStore): Promise<void>;
25
56
  /**
26
57
  * One-time idempotent migration to GLOBAL F/P/T numbering.
@@ -116,6 +147,12 @@ export declare class PlanStore {
116
147
  allocFeatureNumber(): Promise<number>;
117
148
  allocPhaseNumber(): Promise<number>;
118
149
  allocTaskNumber(): Promise<number>;
150
+ /** Allocate a globally-unique sequence number. `atomicUpdateJson` already
151
+ * serializes concurrent calls via `withWriteLock` on the project file, so
152
+ * the read-modify-write is race-free in-process. The collision guard
153
+ * additionally skips any candidate that already exists in the data (safety
154
+ * net for cross-process races or manual edits) and persists the corrected
155
+ * counter. */
119
156
  private allocSeqNumber;
120
157
  loadPhase(phaseId: string): Promise<Phase>;
121
158
  /** Read raw feature files WITHOUT the derived `status` field. Used
@@ -143,7 +180,21 @@ export declare class PlanStore {
143
180
  /** Derive an up-to-date resume focus from the current workspace state. */
144
181
  refreshResume(notes?: string, lastSessionSummary?: string): Promise<ResumeFocus>;
145
182
  loadRequirements(): Promise<RequirementsDocument>;
183
+ linkedRequirementsForPhase(phaseId: string): Promise<Requirement[]>;
184
+ loadPhaseWithRequirements(phaseId: string): Promise<Phase & {
185
+ linkedRequirements: Requirement[];
186
+ }>;
187
+ loadAllPhasesWithRequirements(): Promise<Array<Phase & {
188
+ linkedRequirements: Requirement[];
189
+ }>>;
146
190
  loadAllPhases(): Promise<Phase[]>;
191
+ /** Derive the parent display snapshot for a phase from its tasks' canonical
192
+ * statuses. Pure, non-persisting. */
193
+ loadPhaseDisplay(phaseId: string): Promise<ParentDisplay>;
194
+ /** Derive the parent display snapshot for a feature from its phases' DERIVED
195
+ * canonical statuses (each phase status is derived from its tasks at read
196
+ * time, then mapped via fromCanonicalStatus). Pure, non-persisting. */
197
+ loadFeatureDisplay(featureId: string): Promise<ParentDisplay>;
147
198
  loadAll(): Promise<PlanWorkspace>;
148
199
  /** Migrate legacy non-feature-scoped phase ids to feature-scoped ids and repair
149
200
  * dangling feature.phaseIds references. Idempotent. */
@@ -200,6 +251,7 @@ export declare class PlanStore {
200
251
  duplicateShortIds: string[];
201
252
  };
202
253
  }>;
254
+ private repairPhaseFeatureRefs;
203
255
  /** Validate plan integrity: globally unique phase ids and resolvable feature.phaseIds. */
204
256
  validateIntegrity(): Promise<{
205
257
  duplicatePhaseIds: string[];
@@ -263,6 +315,11 @@ export declare class PlanStore {
263
315
  /** List all phases that have a non-empty handoff, newest first, with a
264
316
  * human-readable composite ref (P00x or P00x(F00x)) and a first-line excerpt. */
265
317
  listHandoffs(): Promise<PhaseHandoffSummary[]>;
318
+ listOrphanPhases(): Promise<OrphanPhaseSummary[]>;
319
+ cleanupOrphanPhases(): Promise<{
320
+ found: OrphanPhaseSummary[];
321
+ removed: OrphanPhaseSummary[];
322
+ }>;
266
323
  deletePhase(phaseId: string): Promise<void>;
267
324
  /** Load the full workspace (manifest + phases + project + requirements + features) */
268
325
  loadWorkspace(): Promise<PlanWorkspace>;
@@ -1 +1 @@
1
- {"version":3,"file":"plan-store.d.ts","sourceRoot":"","sources":["../src/plan-store.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"plan-store.d.ts","sourceRoot":"","sources":["../src/plan-store.ts"],"names":[],"mappings":"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"}
@@ -1,15 +1,40 @@
1
- import { access, copyFile, mkdir, readdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
1
+ import { access, copyFile, mkdir, readdir, readFile, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
+ import { ZodError } from "zod";
3
4
  import { CodebaseProfileSchema, FeatureSchema, FeaturesDocumentSchema, ManifestSchema, PhaseSchema, ProjectSchema, RequirementsDocumentSchema, ResumeFocusSchema, ActivityLogSchema, } from "./schema.js";
4
5
  import { createFeatureId, createPhaseId, createRequirementId, createShortId, createStatusLogEntryId, createTaskId, formatPhaseRef, isLegacyPhaseId } from "./naming.js";
6
+ import { deriveParentDisplay, fromCanonicalStatus } from "./display-status.js";
5
7
  function nowISO() {
6
8
  return new Date().toISOString();
7
9
  }
10
+ function resolveStoredFeatureId(features, ref) {
11
+ const raw = ref?.trim();
12
+ if (!raw)
13
+ return undefined;
14
+ const normalized = raw.toLowerCase();
15
+ const byId = features.find((feature) => feature.id.toLowerCase() === normalized);
16
+ if (byId)
17
+ return byId.id;
18
+ const byNumber = normalized.match(/^f(\d+)$/)
19
+ ? features.find((feature) => feature.number === parseInt(normalized.slice(1), 10))
20
+ : undefined;
21
+ if (byNumber)
22
+ return byNumber.id;
23
+ const byShortId = features.find((feature) => feature.shortId?.toLowerCase() === normalized);
24
+ if (byShortId)
25
+ return byShortId.id;
26
+ const byExactName = features.find((feature) => feature.name.toLowerCase() === normalized);
27
+ if (byExactName)
28
+ return byExactName.id;
29
+ return undefined;
30
+ }
8
31
  export class PlanStoreError extends Error {
9
32
  cause;
10
- constructor(message, cause) {
33
+ details;
34
+ constructor(message, cause, details) {
11
35
  super(message);
12
36
  this.cause = cause;
37
+ this.details = details;
13
38
  this.name = "PlanStoreError";
14
39
  }
15
40
  }
@@ -32,14 +57,52 @@ let writeNotifyHook;
32
57
  export function setWriteNotifyHook(hook) {
33
58
  writeNotifyHook = hook;
34
59
  }
60
+ const CROSS_PROCESS_LOCK_STALE_MS = 30_000;
61
+ const CROSS_PROCESS_LOCK_RETRY_MS = 10;
62
+ async function acquireCrossProcessLock(path) {
63
+ const lockPath = `${path}.lock`;
64
+ for (;;) {
65
+ try {
66
+ await mkdir(lockPath);
67
+ return async () => {
68
+ await rm(lockPath, { recursive: true, force: true }).catch(() => { });
69
+ };
70
+ }
71
+ catch (error) {
72
+ const err = error;
73
+ if (err?.code !== "EEXIST")
74
+ throw err;
75
+ try {
76
+ const info = await stat(lockPath);
77
+ if (Date.now() - info.mtimeMs > CROSS_PROCESS_LOCK_STALE_MS) {
78
+ await rm(lockPath, { recursive: true, force: true });
79
+ continue;
80
+ }
81
+ }
82
+ catch {
83
+ continue;
84
+ }
85
+ await new Promise((resolve) => setTimeout(resolve, CROSS_PROCESS_LOCK_RETRY_MS));
86
+ }
87
+ }
88
+ }
35
89
  function withWriteLock(path, fn) {
36
90
  const prev = writeLocks.get(path) ?? Promise.resolve();
37
91
  let release;
38
92
  const next = new Promise((resolve) => { release = resolve; });
39
- writeLocks.set(path, prev.then(() => next));
40
- return prev.then(fn).finally(() => {
93
+ const tail = prev.then(() => next);
94
+ writeLocks.set(path, tail);
95
+ return prev.then(async () => {
96
+ const releaseCrossProcess = await acquireCrossProcessLock(path);
97
+ try {
98
+ return await fn();
99
+ }
100
+ finally {
101
+ await releaseCrossProcess();
102
+ }
103
+ }).finally(() => {
41
104
  release();
42
- if (writeLocks.get(path) === prev.then(() => next))
105
+ if (writeLocks.get(path) === tail)
43
106
  writeLocks.delete(path);
44
107
  });
45
108
  }
@@ -47,10 +110,11 @@ export function withFeatureLock(featureId, fn) {
47
110
  const prev = featureLocks.get(featureId) ?? Promise.resolve();
48
111
  let release;
49
112
  const next = new Promise((resolve) => { release = resolve; });
50
- featureLocks.set(featureId, prev.then(() => next));
113
+ const tail = prev.then(() => next);
114
+ featureLocks.set(featureId, tail);
51
115
  return prev.then(fn).finally(() => {
52
116
  release();
53
- if (featureLocks.get(featureId) === prev.then(() => next))
117
+ if (featureLocks.get(featureId) === tail)
54
118
  featureLocks.delete(featureId);
55
119
  });
56
120
  }
@@ -271,20 +335,55 @@ export async function migrateToGlobalSequence(store) {
271
335
  });
272
336
  }
273
337
  async function readJson(path, schema) {
338
+ let backupTried = false;
339
+ let backupFailed = false;
340
+ let rawPreview;
274
341
  try {
275
342
  const raw = await readFile(path, "utf-8");
343
+ rawPreview = raw.slice(0, 240);
276
344
  return schema.parse(JSON.parse(raw));
277
345
  }
278
346
  catch (cause) {
279
347
  // Try the .bak backup before giving up (recover from external-write corruption).
348
+ backupTried = true;
280
349
  try {
281
350
  const bak = await readFile(`${path}.bak`, "utf-8");
351
+ rawPreview = bak.slice(0, 240);
282
352
  return schema.parse(JSON.parse(bak));
283
353
  }
284
354
  catch {
355
+ backupFailed = true;
285
356
  // fall through to original error
286
357
  }
287
- throw new PlanStoreError(`read failed: ${path}`, cause);
358
+ const details = {
359
+ path,
360
+ operation: "readJson",
361
+ backupTried,
362
+ backupFailed,
363
+ };
364
+ if (rawPreview != null)
365
+ details.rawPreview = rawPreview;
366
+ if (cause instanceof SyntaxError) {
367
+ const match = cause.message.match(/position\s+(\d+)/i);
368
+ const position = match && match[1] ? Number.parseInt(match[1], 10) : undefined;
369
+ if (rawPreview != null && position != null && position >= 0) {
370
+ const upTo = rawPreview.slice(0, position);
371
+ const line = upTo.split("\n").length;
372
+ const lastNL = upTo.lastIndexOf("\n");
373
+ const column = position - (lastNL >= 0 ? lastNL : 0);
374
+ details.jsonParseError = { message: cause.message, line, column };
375
+ }
376
+ else {
377
+ details.jsonParseError = { message: cause.message };
378
+ }
379
+ }
380
+ else if (cause instanceof ZodError) {
381
+ details.validationErrors = cause.issues.slice(0, 8).map((issue) => ({
382
+ path: issue.path.map((p) => (typeof p === "number" ? `[${p}]` : String(p))).join("."),
383
+ message: issue.message,
384
+ }));
385
+ }
386
+ throw new PlanStoreError(`read failed: ${path}`, cause, details);
288
387
  }
289
388
  }
290
389
  export class PlanStore {
@@ -361,7 +460,12 @@ export class PlanStore {
361
460
  const phasesByFeature = new Map();
362
461
  const orphanPhases = [];
363
462
  for (const phase of phases) {
364
- if (phase.featureId) {
463
+ const resolvedFeatureId = resolveStoredFeatureId(featuresDoc.features, phase.featureId);
464
+ if (resolvedFeatureId && resolvedFeatureId !== phase.featureId) {
465
+ phase.featureId = resolvedFeatureId;
466
+ changed = true;
467
+ }
468
+ if (phase.featureId && featuresDoc.features.some((feature) => feature.id === phase.featureId)) {
365
469
  const bucket = phasesByFeature.get(phase.featureId) ?? [];
366
470
  bucket.push(phase);
367
471
  phasesByFeature.set(phase.featureId, bucket);
@@ -639,15 +743,46 @@ export class PlanStore {
639
743
  * The counter never reuses a number — deletions leave gaps (by design:
640
744
  * stable references survive deletion).
641
745
  */
642
- async allocFeatureNumber() { return this.allocSeqNumber("nextFeatureNumber"); }
643
- async allocPhaseNumber() { return this.allocSeqNumber("nextPhaseNumber"); }
644
- async allocTaskNumber() { return this.allocSeqNumber("nextTaskNumber"); }
645
- async allocSeqNumber(key) {
646
- const project = await this.loadProject();
647
- const n = project[key];
648
- project[key] = n + 1;
649
- await this.saveProject(project);
650
- return n;
746
+ async allocFeatureNumber() { return this.allocSeqNumber("nextFeatureNumber", "feature"); }
747
+ async allocPhaseNumber() { return this.allocSeqNumber("nextPhaseNumber", "phase"); }
748
+ async allocTaskNumber() { return this.allocSeqNumber("nextTaskNumber", "task"); }
749
+ /** Allocate a globally-unique sequence number. `atomicUpdateJson` already
750
+ * serializes concurrent calls via `withWriteLock` on the project file, so
751
+ * the read-modify-write is race-free in-process. The collision guard
752
+ * additionally skips any candidate that already exists in the data (safety
753
+ * net for cross-process races or manual edits) and persists the corrected
754
+ * counter. */
755
+ async allocSeqNumber(key, kind) {
756
+ // Load already-used numbers for this kind (best-effort read; the
757
+ // atomicUpdateJson below is the authoritative write).
758
+ const used = new Set();
759
+ if (kind === "task") {
760
+ const phases = await this.loadAllPhases();
761
+ for (const p of phases)
762
+ for (const t of p.tasks)
763
+ used.add(t.number);
764
+ }
765
+ else if (kind === "phase") {
766
+ const phases = await this.loadAllPhases();
767
+ for (const p of phases)
768
+ used.add(p.number);
769
+ }
770
+ else {
771
+ const feats = await this.loadRawFeatures();
772
+ for (const f of feats)
773
+ used.add(f.number);
774
+ }
775
+ let allocated = 0;
776
+ await this.updateProject((project) => {
777
+ let candidate = project[key];
778
+ // Collision guard: skip any candidate that already exists.
779
+ while (used.has(candidate))
780
+ candidate++;
781
+ allocated = candidate;
782
+ project[key] = candidate + 1;
783
+ return project;
784
+ });
785
+ return allocated;
651
786
  }
652
787
  async loadPhase(phaseId) {
653
788
  const raw = await readJson(this.phasePath(phaseId), PhaseSchema);
@@ -700,7 +835,7 @@ export class PlanStore {
700
835
  const raws = await this.loadRawFeatures();
701
836
  const phases = await this.loadAllPhases();
702
837
  const features = raws.map((f) => ({ ...f, status: this.deriveFeatureStatus(f.id, phases) }));
703
- return this.normalizeFeaturesDocument({ features }).doc;
838
+ return this.normalizeStructureSnapshot({ features }, phases).features;
704
839
  }
705
840
  async loadCodebaseProfile() {
706
841
  try {
@@ -831,6 +966,27 @@ export class PlanStore {
831
966
  return { requirements: [] };
832
967
  }
833
968
  }
969
+ async linkedRequirementsForPhase(phaseId) {
970
+ const requirements = await this.loadRequirements();
971
+ return requirements.requirements.filter((requirement) => requirement.linkedPhaseIds.includes(phaseId));
972
+ }
973
+ async loadPhaseWithRequirements(phaseId) {
974
+ const [phase, linkedRequirements] = await Promise.all([
975
+ this.loadPhase(phaseId),
976
+ this.linkedRequirementsForPhase(phaseId),
977
+ ]);
978
+ return { ...phase, linkedRequirements };
979
+ }
980
+ async loadAllPhasesWithRequirements() {
981
+ const [phases, requirements] = await Promise.all([
982
+ this.loadAllPhases(),
983
+ this.loadRequirements(),
984
+ ]);
985
+ return phases.map((phase) => ({
986
+ ...phase,
987
+ linkedRequirements: requirements.requirements.filter((requirement) => requirement.linkedPhaseIds.includes(phase.id)),
988
+ }));
989
+ }
834
990
  async loadAllPhases() {
835
991
  const { readdir } = await import("node:fs/promises");
836
992
  let files;
@@ -861,6 +1017,22 @@ export class PlanStore {
861
1017
  return left.createdAt.localeCompare(right.createdAt);
862
1018
  });
863
1019
  }
1020
+ /** Derive the parent display snapshot for a phase from its tasks' canonical
1021
+ * statuses. Pure, non-persisting. */
1022
+ async loadPhaseDisplay(phaseId) {
1023
+ const phase = await this.loadPhase(phaseId);
1024
+ const childStatuses = phase.tasks.map((t) => fromCanonicalStatus(t.status));
1025
+ return deriveParentDisplay(childStatuses);
1026
+ }
1027
+ /** Derive the parent display snapshot for a feature from its phases' DERIVED
1028
+ * canonical statuses (each phase status is derived from its tasks at read
1029
+ * time, then mapped via fromCanonicalStatus). Pure, non-persisting. */
1030
+ async loadFeatureDisplay(featureId) {
1031
+ const phases = await this.loadAllPhases();
1032
+ const featurePhases = phases.filter((p) => p.featureId === featureId);
1033
+ const childStatuses = featurePhases.map((p) => fromCanonicalStatus(p.status));
1034
+ return deriveParentDisplay(childStatuses);
1035
+ }
864
1036
  async loadAll() {
865
1037
  const [manifest, project, requirements, phases] = await Promise.all([
866
1038
  this.loadManifest(),
@@ -870,7 +1042,8 @@ export class PlanStore {
870
1042
  ]);
871
1043
  const rawFeatures = await this.loadRawFeatures();
872
1044
  const features = rawFeatures.map((f) => ({ ...f, status: this.deriveFeatureStatus(f.id, phases) }));
873
- return { manifest, project, requirements, phases, features: { features } };
1045
+ const normalized = this.normalizeStructureSnapshot({ features }, phases);
1046
+ return { manifest, project, requirements, phases: normalized.phases, features: normalized.features };
874
1047
  }
875
1048
  /** Migrate legacy non-feature-scoped phase ids to feature-scoped ids and repair
876
1049
  * dangling feature.phaseIds references. Idempotent. */
@@ -1129,6 +1302,7 @@ export class PlanStore {
1129
1302
  async repair() {
1130
1303
  return this.runAsBatch(async () => {
1131
1304
  const migrated = await this.migratePhaseIds();
1305
+ await this.repairPhaseFeatureRefs();
1132
1306
  const backfill = await this.ensureShortIdsAndPriority();
1133
1307
  // Rebuild phase containment from each task's own phaseId. Heals plans
1134
1308
  // corrupted by the migrateToGlobalSequence index-mismatch bug (core
@@ -1139,6 +1313,22 @@ export class PlanStore {
1139
1313
  return { migrated, backfill, containment, integrity };
1140
1314
  });
1141
1315
  }
1316
+ async repairPhaseFeatureRefs() {
1317
+ const features = await this.loadRawFeatures();
1318
+ const phases = await this.loadAllPhases();
1319
+ let changed = 0;
1320
+ for (const phase of phases) {
1321
+ const resolvedFeatureId = resolveStoredFeatureId(features, phase.featureId);
1322
+ if (resolvedFeatureId && resolvedFeatureId !== phase.featureId) {
1323
+ await this.savePhase({ ...phase, featureId: resolvedFeatureId });
1324
+ changed += 1;
1325
+ }
1326
+ }
1327
+ if (changed > 0) {
1328
+ await this.updateFeatures((doc) => doc);
1329
+ }
1330
+ return changed;
1331
+ }
1142
1332
  /** Validate plan integrity: globally unique phase ids and resolvable feature.phaseIds. */
1143
1333
  async validateIntegrity() {
1144
1334
  const phases = await this.loadAllPhases();
@@ -1184,18 +1374,28 @@ export class PlanStore {
1184
1374
  return "rejected";
1185
1375
  if (meaningful.every((s) => s === "done"))
1186
1376
  return "done";
1187
- // Lifecycle truth: any progress (active work OR partial completion)
1188
- // in-progress, until fully done. This is what prevents a single
1189
- // blocked/waiting/deferred task from poisoning the parent when there is
1190
- // substantial done or in-progress work (the long-standing rollup bug).
1191
- if (meaningful.some((s) => s === "in-progress") || meaningful.some((s) => s === "done"))
1377
+ const hasDone = meaningful.some((s) => s === "done");
1378
+ const hasActive = meaningful.some((s) => s === "in-progress");
1379
+ const hasPlanned = meaningful.some((s) => s === "planned");
1380
+ const hasBlocked = meaningful.some((s) => s === "blocked");
1381
+ const hasWaiting = meaningful.some((s) => s === "waiting");
1382
+ const hasDeferred = meaningful.some((s) => s === "deferred");
1383
+ if (hasActive)
1384
+ return "in-progress";
1385
+ // If completed work exists and the ONLY remaining meaningful work is deferred,
1386
+ // surface deferred instead of implying active execution.
1387
+ if (hasDone && !hasPlanned && !hasBlocked && !hasWaiting && hasDeferred)
1388
+ return "deferred";
1389
+ // Partial completion with remaining planned/blocked/waiting work still means
1390
+ // the phase has genuinely started and is not terminal yet.
1391
+ if (hasDone)
1192
1392
  return "in-progress";
1193
1393
  // No progress at all ⇒ surface the stall / not-started state (blocked > waiting > deferred > planned).
1194
- if (meaningful.some((s) => s === "blocked"))
1394
+ if (hasBlocked)
1195
1395
  return "blocked";
1196
- if (meaningful.some((s) => s === "waiting"))
1396
+ if (hasWaiting)
1197
1397
  return "waiting";
1198
- if (meaningful.some((s) => s === "deferred"))
1398
+ if (hasDeferred)
1199
1399
  return "deferred";
1200
1400
  return "planned";
1201
1401
  }
@@ -1210,17 +1410,25 @@ export class PlanStore {
1210
1410
  return "rejected";
1211
1411
  if (meaningful.every((s) => s === "done"))
1212
1412
  return "done";
1213
- // Any progress (an active phase, or a partially-complete done phase)
1214
- // in-progress. Prevents a single stalled phase from poisoning the feature
1215
- // when other phases have done/in-progress work.
1216
- if (meaningful.some((s) => s === "discovery" || s === "in-progress") || meaningful.some((s) => s === "done"))
1413
+ const hasDone = meaningful.some((s) => s === "done");
1414
+ const hasActive = meaningful.some((s) => s === "discovery" || s === "in-progress");
1415
+ const hasPlanned = meaningful.some((s) => s === "planned");
1416
+ const hasBlocked = meaningful.some((s) => s === "blocked");
1417
+ const hasWaiting = meaningful.some((s) => s === "waiting");
1418
+ const hasDeferred = meaningful.some((s) => s === "deferred");
1419
+ if (hasActive)
1420
+ return "in-progress";
1421
+ // Same rule as phases: done + deferred-only remainder is deferred, not active.
1422
+ if (hasDone && !hasPlanned && !hasBlocked && !hasWaiting && hasDeferred)
1423
+ return "deferred";
1424
+ if (hasDone)
1217
1425
  return "in-progress";
1218
1426
  // No progress at all ⇒ surface the stall / not-started state.
1219
- if (meaningful.some((s) => s === "blocked"))
1427
+ if (hasBlocked)
1220
1428
  return "blocked";
1221
- if (meaningful.some((s) => s === "waiting"))
1429
+ if (hasWaiting)
1222
1430
  return "waiting";
1223
- if (meaningful.some((s) => s === "deferred"))
1431
+ if (hasDeferred)
1224
1432
  return "deferred";
1225
1433
  return "planned";
1226
1434
  }
@@ -1376,7 +1584,17 @@ export class PlanStore {
1376
1584
  await this.touchManifest();
1377
1585
  }
1378
1586
  async savePhase(phase) {
1379
- const parsed = PhaseSchema.parse(this.normalizePhaseDocument(phase).phase);
1587
+ const features = await this.loadRawFeatures();
1588
+ const resolvedFeatureId = resolveStoredFeatureId(features, phase.featureId);
1589
+ // Referential integrity: if a featureId is present but cannot be resolved
1590
+ // to a known feature, REJECT — never persist an orphan phase.
1591
+ if (phase.featureId && phase.featureId.trim() && !resolvedFeatureId) {
1592
+ throw new PlanStoreError(`Cannot save phase "${phase.title}": featureId "${phase.featureId}" does not match any existing feature. Use a valid feature UUID, F00x ref, or shortId.`);
1593
+ }
1594
+ const normalizedInput = resolvedFeatureId && resolvedFeatureId !== phase.featureId
1595
+ ? { ...phase, featureId: resolvedFeatureId }
1596
+ : phase;
1597
+ const parsed = PhaseSchema.parse(this.normalizePhaseDocument(normalizedInput).phase);
1380
1598
  await mkdir(this.phasesDir(), { recursive: true });
1381
1599
  await atomicWriteJson(this.phasePath(parsed.id), parsed);
1382
1600
  await this.touchManifest();
@@ -1386,6 +1604,7 @@ export class PlanStore {
1386
1604
  * task_create / phase_update calls on the SAME phaseId so batch operations
1387
1605
  * don't lose tasks (last-write-wins race condition). */
1388
1606
  async updatePhase(phaseId, updater) {
1607
+ const features = await this.loadRawFeatures();
1389
1608
  // Augment the raw (on-disk) phase with its DERIVED status before handing it
1390
1609
  // to the updater, so updaters that read 'phase.status' see the truth. The
1391
1610
  // returned object's 'status' is stripped by PhaseSchema.parse (status is
@@ -1393,7 +1612,15 @@ export class PlanStore {
1393
1612
  const raw = await atomicUpdateJson(this.phasePath(phaseId), PhaseSchema, (rawPhase) => {
1394
1613
  const current = { ...rawPhase, status: this.derivePhaseStatus(rawPhase.tasks) };
1395
1614
  const next = updater(current);
1396
- return this.normalizePhaseDocument(next).phase;
1615
+ const resolvedFeatureId = resolveStoredFeatureId(features, next.featureId);
1616
+ // Referential integrity: reject orphan featureId.
1617
+ if (next.featureId && next.featureId.trim() && !resolvedFeatureId) {
1618
+ throw new PlanStoreError(`Cannot update phase: featureId "${next.featureId}" does not match any existing feature.`);
1619
+ }
1620
+ const normalizedInput = resolvedFeatureId && resolvedFeatureId !== next.featureId
1621
+ ? { ...next, featureId: resolvedFeatureId }
1622
+ : next;
1623
+ return this.normalizePhaseDocument(normalizedInput).phase;
1397
1624
  });
1398
1625
  await this.maybeAutoSync();
1399
1626
  return { ...raw, status: this.derivePhaseStatus(raw.tasks) };
@@ -1505,6 +1732,54 @@ export class PlanStore {
1505
1732
  out.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
1506
1733
  return out;
1507
1734
  }
1735
+ async listOrphanPhases() {
1736
+ const phases = await this.loadAllPhases();
1737
+ const featuresDoc = await this.loadFeatures();
1738
+ const out = [];
1739
+ for (const phase of phases) {
1740
+ const resolvedFeatureId = resolveStoredFeatureId(featuresDoc.features, phase.featureId);
1741
+ if (resolvedFeatureId)
1742
+ continue;
1743
+ const reason = phase.featureId?.trim()
1744
+ ? `feature not found: ${phase.featureId}`
1745
+ : "missing featureId";
1746
+ out.push({
1747
+ phaseId: phase.id,
1748
+ featureId: phase.featureId,
1749
+ shortId: phase.shortId,
1750
+ compositeRef: formatPhaseRef(phase.number),
1751
+ title: phase.title,
1752
+ reason,
1753
+ });
1754
+ }
1755
+ out.sort((a, b) => a.compositeRef.localeCompare(b.compositeRef));
1756
+ return out;
1757
+ }
1758
+ async cleanupOrphanPhases() {
1759
+ return this.runAsBatch(async () => {
1760
+ const found = await this.listOrphanPhases();
1761
+ if (found.length === 0)
1762
+ return { found, removed: [] };
1763
+ const orphanIds = new Set(found.map((phase) => phase.phaseId));
1764
+ for (const orphan of found) {
1765
+ try {
1766
+ await unlink(this.phasePath(orphan.phaseId));
1767
+ }
1768
+ catch {
1769
+ // already gone
1770
+ }
1771
+ }
1772
+ await this.updateFeatures((doc) => {
1773
+ for (const feature of doc.features) {
1774
+ feature.phaseIds = feature.phaseIds.filter((id) => !orphanIds.has(id));
1775
+ }
1776
+ return doc;
1777
+ });
1778
+ await this.touchManifest();
1779
+ await this.writeGenerated();
1780
+ return { found, removed: found };
1781
+ });
1782
+ }
1508
1783
  async deletePhase(phaseId) {
1509
1784
  try {
1510
1785
  await unlink(this.phasePath(phaseId));
@@ -1 +1 @@
1
- {"version":3,"file":"recap.d.ts","sourceRoot":"","sources":["../src/recap.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAGjD;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC3B;AAED,yEAAyE;AACzE,MAAM,MAAM,YAAY,GAAG,IAAI,GAAG,KAAK,CAAC;AAExC,MAAM,WAAW,YAAY;IAC3B,OAAO,CAAC,EAAE,YAAY,CAAC;CACxB;AAKD;;;;;;;;;;;;;GAaG;AACH,wBAAsB,UAAU,CAAC,EAAE,EAAE,SAAS,EAAE,GAAG,GAAE,YAAiB,EAAE,IAAI,GAAE,YAAiB,GAAG,OAAO,CAAC,MAAM,CAAC,CA8HhH"}
1
+ {"version":3,"file":"recap.d.ts","sourceRoot":"","sources":["../src/recap.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAGjD;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC3B;AAED,yEAAyE;AACzE,MAAM,MAAM,YAAY,GAAG,IAAI,GAAG,KAAK,CAAC;AAExC,MAAM,WAAW,YAAY;IAC3B,OAAO,CAAC,EAAE,YAAY,CAAC;CACxB;AAKD;;;;;;;;;;;;;GAaG;AACH,wBAAsB,UAAU,CAAC,EAAE,EAAE,SAAS,EAAE,GAAG,GAAE,YAAiB,EAAE,IAAI,GAAE,YAAiB,GAAG,OAAO,CAAC,MAAM,CAAC,CAgJhH"}
package/dist/recap.js CHANGED
@@ -62,6 +62,10 @@ export async function buildRecap(st, web = {}, opts = {}) {
62
62
  const tr = tref(focusTask.task.number);
63
63
  lines.push(`${italian ? "Focus corrente" : "Current focus"}: ${fr} — ${focusFeature?.name ?? "?"} / ${pr} — ${focusPhase.title} / ${tr} — ${focusTask.task.title} (in-progress)`);
64
64
  }
65
+ else if (handoffs.length > 0) {
66
+ const top = handoffs[0];
67
+ lines.push(`${italian ? "Focus corrente" : "Current focus"}: ${italian ? "nessun task attivo" : "no active task"} · ${italian ? "handoff pendente più recente" : "most recent pending handoff"}: ${top.compositeRef} — "${top.firstLine}"`);
68
+ }
65
69
  else if (planComplete) {
66
70
  lines.push(italian
67
71
  ? "Focus corrente: piano completo — tutte le feature/fasi/task sono concluse."
@@ -92,11 +96,19 @@ export async function buildRecap(st, web = {}, opts = {}) {
92
96
  lines.push(staleNote);
93
97
  }
94
98
  if (handoffs.length > 0) {
99
+ const top = handoffs[0];
95
100
  lines.push("", italian ? `## Handoff di fase pendenti (${handoffs.length})` : `## Pending phase handoffs (${handoffs.length})`);
96
101
  handoffs.forEach((h, i) => lines.push(`[${i + 1}] ${h.compositeRef} — ${h.updatedAt} — "${h.firstLine}"`));
97
- lines.push("", italian
98
- ? `→ Leggi quello pertinente con ${handoffShowCmd} <ref> (valida contro lo stato attuale). L'handoff è MANTENUTO finché un task della fase non parte (auto-archiviato) o la fase non si conclude — non serve cancellarlo a mano.`
99
- : `→ Read the relevant one with ${handoffShowCmd} <ref> (validate against current state). It is KEPT until a task in that phase starts (then auto-archived) or the phase completesno need to clear it manually.`);
102
+ if (handoffs.length === 1) {
103
+ lines.push("", italian
104
+ ? `→ Vuoi riprendere da ${top.compositeRef}? Leggi l'handoff con ${handoffShowCmd} ${top.compositeRef} e avvia il primo task pertinente. L'handoff è MANTENUTO finché un task della fase non parte (auto-archiviato) o la fase non si conclude non serve cancellarlo a mano.`
105
+ : `→ Do you want to resume from ${top.compositeRef}? Read the handoff with ${handoffShowCmd} ${top.compositeRef} and start the first relevant task. It is KEPT until a task in that phase starts (then auto-archived) or the phase completes — no need to clear it manually.`);
106
+ }
107
+ else {
108
+ lines.push("", italian
109
+ ? `→ Scegli da quale fase ripartire. Il più recente è [1] ${top.compositeRef} — leggilo con ${handoffShowCmd} ${top.compositeRef} e avvia il primo task pertinente. Ogni handoff è MANTENUTO finché un task della fase non parte (auto-archiviato) o la fase non si conclude — non serve cancellarlo a mano.`
110
+ : `→ Choose which phase to resume from. The most recent is [1] ${top.compositeRef} — read it with ${handoffShowCmd} ${top.compositeRef} and start the first relevant task. Each handoff is KEPT until a task in that phase starts (then auto-archived) or the phase completes — no need to clear it manually.`);
111
+ }
100
112
  }
101
113
  else if (planComplete) {
102
114
  lines.push("", italian
@@ -114,6 +126,6 @@ export async function buildRecap(st, web = {}, opts = {}) {
114
126
  else {
115
127
  lines.push("", italian ? "## Web UI" : "## Web UI", `🌐 Web UI: ${italian ? "non attiva — avvia con /planner load" : "not running — start with /planner load"}`);
116
128
  }
117
- lines.push("", italian ? "Vuoi che riprendiamo da qui?" : "Do you want to resume from here?");
129
+ lines.push("", italian ? "Pronto a riprendere?" : "Ready to resume?");
118
130
  return lines.join("\n");
119
131
  }
package/dist/schema.d.ts CHANGED
@@ -486,6 +486,8 @@ export declare const PhaseStatusLogEntrySchema: z.ZodObject<{
486
486
  }>;
487
487
  export declare const TaskSchema: z.ZodEffects<z.ZodObject<{
488
488
  id: z.ZodString;
489
+ /** MUST be a phase UUID (not a ref like "P003"). Validated at the schema
490
+ * layer so no adapter can persist an unresolved ref string. */
489
491
  phaseId: z.ZodString;
490
492
  /** Global project-wide task sequence (assigned once at creation from project.nextTaskNumber; stable, gaps on delete). Bare T00x is unambiguous. */
491
493
  number: z.ZodDefault<z.ZodNumber>;
@@ -793,6 +795,9 @@ export declare const HandoffHistoryEntrySchema: z.ZodObject<{
793
795
  }>;
794
796
  export declare const PhaseSchema: z.ZodObject<{
795
797
  id: z.ZodString;
798
+ /** MUST be a feature UUID (not a ref like "F005"). Validated at the schema
799
+ * layer so no adapter can persist an unresolved ref string. Optional only
800
+ * for legacy pre-feature phases. */
796
801
  featureId: z.ZodOptional<z.ZodString>;
797
802
  /** Global project-wide phase sequence (assigned once at creation from project.nextPhaseNumber; stable, gaps on delete). Bare P00x is unambiguous. */
798
803
  number: z.ZodNumber;
@@ -839,6 +844,8 @@ export declare const PhaseSchema: z.ZodObject<{
839
844
  taskIds: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
840
845
  tasks: z.ZodDefault<z.ZodArray<z.ZodEffects<z.ZodObject<{
841
846
  id: z.ZodString;
847
+ /** MUST be a phase UUID (not a ref like "P003"). Validated at the schema
848
+ * layer so no adapter can persist an unresolved ref string. */
842
849
  phaseId: z.ZodString;
843
850
  /** Global project-wide task sequence (assigned once at creation from project.nextTaskNumber; stable, gaps on delete). Bare T00x is unambiguous. */
844
851
  number: z.ZodDefault<z.ZodNumber>;
@@ -2342,6 +2349,9 @@ export declare const PlanWorkspaceSchema: z.ZodObject<{
2342
2349
  }>;
2343
2350
  phases: z.ZodArray<z.ZodObject<{
2344
2351
  id: z.ZodString;
2352
+ /** MUST be a feature UUID (not a ref like "F005"). Validated at the schema
2353
+ * layer so no adapter can persist an unresolved ref string. Optional only
2354
+ * for legacy pre-feature phases. */
2345
2355
  featureId: z.ZodOptional<z.ZodString>;
2346
2356
  /** Global project-wide phase sequence (assigned once at creation from project.nextPhaseNumber; stable, gaps on delete). Bare P00x is unambiguous. */
2347
2357
  number: z.ZodNumber;
@@ -2388,6 +2398,8 @@ export declare const PlanWorkspaceSchema: z.ZodObject<{
2388
2398
  taskIds: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
2389
2399
  tasks: z.ZodDefault<z.ZodArray<z.ZodEffects<z.ZodObject<{
2390
2400
  id: z.ZodString;
2401
+ /** MUST be a phase UUID (not a ref like "P003"). Validated at the schema
2402
+ * layer so no adapter can persist an unresolved ref string. */
2391
2403
  phaseId: z.ZodString;
2392
2404
  /** Global project-wide task sequence (assigned once at creation from project.nextTaskNumber; stable, gaps on delete). Bare T00x is unambiguous. */
2393
2405
  number: z.ZodDefault<z.ZodNumber>;
@@ -1 +1 @@
1
- {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"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"}
package/dist/schema.js CHANGED
@@ -158,7 +158,9 @@ export const PhaseStatusLogEntrySchema = z.object({
158
158
  });
159
159
  export const TaskSchema = z.object({
160
160
  id: z.string(),
161
- phaseId: z.string(),
161
+ /** MUST be a phase UUID (not a ref like "P003"). Validated at the schema
162
+ * layer so no adapter can persist an unresolved ref string. */
163
+ phaseId: z.string().regex(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, "phaseId must be a UUID, not a ref string like P003"),
162
164
  /** Global project-wide task sequence (assigned once at creation from project.nextTaskNumber; stable, gaps on delete). Bare T00x is unambiguous. */
163
165
  number: z.number().int().nonnegative().default(0),
164
166
  shortId: z.string().regex(/^(|[A-Z2-9]{5})$/).default(""),
@@ -195,7 +197,10 @@ export const HandoffHistoryEntrySchema = z.object({
195
197
  });
196
198
  export const PhaseSchema = z.object({
197
199
  id: z.string(),
198
- featureId: z.string().optional(),
200
+ /** MUST be a feature UUID (not a ref like "F005"). Validated at the schema
201
+ * layer so no adapter can persist an unresolved ref string. Optional only
202
+ * for legacy pre-feature phases. */
203
+ featureId: z.string().regex(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, "featureId must be a UUID, not a ref string like F005").optional(),
199
204
  /** Global project-wide phase sequence (assigned once at creation from project.nextPhaseNumber; stable, gaps on delete). Bare P00x is unambiguous. */
200
205
  number: z.number().int().positive(),
201
206
  shortId: z.string().regex(/^(|[A-Z2-9]{5})$/).default(""),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-plan/core",
3
- "version": "0.2.19-next.16",
3
+ "version": "0.2.19-next.18",
4
4
  "private": false,
5
5
  "description": "Harness-agnostic core for Agent Plan: schemas, persistence, ordering, status rollups, and markdown rendering.",
6
6
  "license": "MIT",
@@ -40,6 +40,9 @@
40
40
  "dependencies": {
41
41
  "zod": "^3.24.0"
42
42
  },
43
+ "devDependencies": {
44
+ "zod-to-json-schema": "^3.25.2"
45
+ },
43
46
  "scripts": {
44
47
  "build": "tsc -p tsconfig.json",
45
48
  "check": "tsc -p tsconfig.json --noEmit",