@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.
- package/dist/display-status.d.ts +99 -0
- package/dist/display-status.d.ts.map +1 -0
- package/dist/display-status.js +176 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/naming.d.ts +14 -0
- package/dist/naming.d.ts.map +1 -1
- package/dist/naming.js +18 -0
- package/dist/plan-store.d.ts +59 -2
- package/dist/plan-store.d.ts.map +1 -1
- package/dist/plan-store.js +312 -37
- package/dist/recap.d.ts.map +1 -1
- package/dist/recap.js +16 -4
- package/dist/schema.d.ts +12 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +7 -2
- package/package.json +4 -1
|
@@ -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
|
|
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";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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;
|
|
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). */
|
package/dist/naming.d.ts.map
CHANGED
|
@@ -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). */
|
package/dist/plan-store.d.ts
CHANGED
|
@@ -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
|
-
|
|
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>;
|
package/dist/plan-store.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plan-store.d.ts","sourceRoot":"","sources":["../src/plan-store.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"plan-store.d.ts","sourceRoot":"","sources":["../src/plan-store.ts"],"names":[],"mappings":"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"}
|
package/dist/plan-store.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
40
|
-
|
|
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) ===
|
|
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
|
-
|
|
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) ===
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
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 (
|
|
1394
|
+
if (hasBlocked)
|
|
1195
1395
|
return "blocked";
|
|
1196
|
-
if (
|
|
1396
|
+
if (hasWaiting)
|
|
1197
1397
|
return "waiting";
|
|
1198
|
-
if (
|
|
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
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
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 (
|
|
1427
|
+
if (hasBlocked)
|
|
1220
1428
|
return "blocked";
|
|
1221
|
-
if (
|
|
1429
|
+
if (hasWaiting)
|
|
1222
1430
|
return "waiting";
|
|
1223
|
-
if (
|
|
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
|
|
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
|
-
|
|
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));
|
package/dist/recap.d.ts.map
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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 ? "
|
|
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>;
|
package/dist/schema.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,eAAO,MAAM,eAAe,aAAwB,CAAC;AACrD,eAAO,MAAM,UAAU,aAAiD,CAAC;AAEzE,eAAO,MAAM,kBAAkB;;;;;;;;;EAG7B,CAAC;AAEH,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;EAK7B,CAAC;AAEH,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAmBhC,CAAC;AAEH,eAAO,MAAM,iBAAiB;;;;;IAK5B;;mFAE+E;;;;;;;;;;;;;;;;;;;;;;;;;;EAM/E,CAAC;AAEH,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;EAM9B,CAAC;AAEH,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAE5B,CAAC;AAEH,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;EAMzB,CAAC;AAEH,eAAO,MAAM,mBAAmB;;;;;;;;;;;;EAI9B,CAAC;AAEH,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;EAOjC,CAAC;AAEH,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAexB,gGAAgG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAIhG,CAAC;AAEH,eAAO,MAAM,mBAAmB,yGAAuG,CAAC;AACxI,eAAO,MAAM,gBAAgB,yGAAuG,CAAC;AACrI,eAAO,MAAM,iBAAiB,+HAA6H,CAAC;AAC5J,eAAO,MAAM,uBAAuB,yGAAuG,CAAC;AAC5I,eAAO,MAAM,mBAAmB,yGAAuG,CAAC;AAExI,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;EAOxB,CAAC;AAEH,eAAO,MAAM,mBAAmB;;IAE9B;;yDAEqD;;;;;;;;;;;;;;EAIrD,CAAC;AAIH,wEAAwE;AACxE,eAAO,MAAM,8BAA8B,aAEzC,CAAC;AAEH;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAK7E;AAED,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;EAO/B,CAAC;AAEH;2EAC2E;AAC3E,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;;;;;;;EAOpC,CAAC;AAEH,eAAO,MAAM,UAAU
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|