@agent-plan/core 0.2.19-next.9 → 0.2.19

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,16 @@
1
+ import type { ChecklistItem } from "./schema.js";
2
+ /**
3
+ * Granular per-task checklist helpers. Each item has a stable unique `id`
4
+ * (the robust handle for add/remove/toggle) plus a progressive `number`
5
+ * (C1..Cn display label, renumbered on remove for readability). Selectors
6
+ * accept C{n} (e.g. C2), the item id, or a title (case-insensitive, first
7
+ * exact then partial match).
8
+ */
9
+ export declare function findChecklistItem(items: ChecklistItem[], selector: string): ChecklistItem | undefined;
10
+ /** Append a new item. number = max(existing)+1 (stable, never reused). Mutates nothing; returns the new item. */
11
+ export declare function addChecklistItem(items: ChecklistItem[], taskId: string, title: string): ChecklistItem;
12
+ /** Remove the matched item in place (splice) and renumber the rest 1..n. Returns the removed item, or undefined. */
13
+ export declare function removeChecklistItem(items: ChecklistItem[], selector: string): ChecklistItem | undefined;
14
+ /** Tick/untick the matched item in place. checked omitted → toggle. Returns the item, or undefined. */
15
+ export declare function toggleChecklistItem(items: ChecklistItem[], selector: string, checked?: boolean): ChecklistItem | undefined;
16
+ //# sourceMappingURL=checklist.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checklist.d.ts","sourceRoot":"","sources":["../src/checklist.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD;;;;;;GAMG;AAEH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,aAAa,EAAE,EAAE,QAAQ,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS,CAerG;AAED,iHAAiH;AACjH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,aAAa,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,aAAa,CAKrG;AAED,oHAAoH;AACpH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,aAAa,EAAE,EAAE,QAAQ,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS,CASvG;AAED,uGAAuG;AACvG,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,aAAa,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,GAAG,aAAa,GAAG,SAAS,CAK1H"}
@@ -0,0 +1,52 @@
1
+ import { createChecklistItemId } from "./naming.js";
2
+ /**
3
+ * Granular per-task checklist helpers. Each item has a stable unique `id`
4
+ * (the robust handle for add/remove/toggle) plus a progressive `number`
5
+ * (C1..Cn display label, renumbered on remove for readability). Selectors
6
+ * accept C{n} (e.g. C2), the item id, or a title (case-insensitive, first
7
+ * exact then partial match).
8
+ */
9
+ export function findChecklistItem(items, selector) {
10
+ const s = selector.trim();
11
+ if (!s)
12
+ return undefined;
13
+ const cMatch = /^C(\d+)$/i.exec(s);
14
+ if (cMatch) {
15
+ const n = parseInt(cMatch[1], 10);
16
+ return items.find((i) => i.number === n);
17
+ }
18
+ const byId = items.find((i) => i.id === s);
19
+ if (byId)
20
+ return byId;
21
+ const needle = s.toLowerCase();
22
+ return (items.find((i) => i.title.trim().toLowerCase() === needle) ??
23
+ items.find((i) => i.title.trim().toLowerCase().includes(needle)));
24
+ }
25
+ /** Append a new item. number = max(existing)+1 (stable, never reused). Mutates nothing; returns the new item. */
26
+ export function addChecklistItem(items, taskId, title) {
27
+ const clean = title.trim();
28
+ const number = items.length === 0 ? 1 : Math.max(...items.map((i) => i.number)) + 1;
29
+ const id = createChecklistItemId(taskId, number, clean);
30
+ return { id, number, title: clean, checked: false };
31
+ }
32
+ /** Remove the matched item in place (splice) and renumber the rest 1..n. Returns the removed item, or undefined. */
33
+ export function removeChecklistItem(items, selector) {
34
+ const found = findChecklistItem(items, selector);
35
+ if (!found)
36
+ return undefined;
37
+ const idx = items.findIndex((i) => i.id === found.id);
38
+ if (idx >= 0)
39
+ items.splice(idx, 1);
40
+ items.forEach((i, n) => {
41
+ i.number = n + 1;
42
+ });
43
+ return found;
44
+ }
45
+ /** Tick/untick the matched item in place. checked omitted → toggle. Returns the item, or undefined. */
46
+ export function toggleChecklistItem(items, selector, checked) {
47
+ const found = findChecklistItem(items, selector);
48
+ if (!found)
49
+ return undefined;
50
+ found.checked = checked ?? !found.checked;
51
+ return found;
52
+ }
@@ -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
@@ -1,8 +1,12 @@
1
1
  export * from "./naming.js";
2
2
  export * from "./refs.js";
3
3
  export * from "./schema.js";
4
+ export * from "./checklist.js";
4
5
  export * from "./recap.js";
5
- export { PlanStore, PlanStoreError, setWriteBusyHook, setWriteNotifyHook, migrateToUuids, migrateToGlobalSequence, withFeatureLock, type PhaseHandoffSummary } from "./plan-store.js";
6
+ export * from "./display-status.js";
7
+ export * from "./task-context.js";
8
+ export * from "./task-selection.js";
9
+ export { PlanStore, PlanStoreError, setWriteBusyHook, setWriteNotifyHook, migrateToUuids, migrateToGlobalSequence, withFeatureLock, type PhaseHandoffSummary, type OrphanPhaseSummary } from "./plan-store.js";
6
10
  export { PlanRenderer } from "./renderer.js";
7
11
  export { ExportService } from "./export-service.js";
8
12
  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,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,cAAc,mBAAmB,CAAC;AAClC,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
@@ -1,7 +1,11 @@
1
1
  export * from "./naming.js";
2
2
  export * from "./refs.js";
3
3
  export * from "./schema.js";
4
+ export * from "./checklist.js";
4
5
  export * from "./recap.js";
6
+ export * from "./display-status.js";
7
+ export * from "./task-context.js";
8
+ export * from "./task-selection.js";
5
9
  export { PlanStore, PlanStoreError, setWriteBusyHook, setWriteNotifyHook, migrateToUuids, migrateToGlobalSequence, withFeatureLock } from "./plan-store.js";
6
10
  export { PlanRenderer } from "./renderer.js";
7
11
  export { ExportService } from "./export-service.js";
package/dist/naming.d.ts CHANGED
@@ -3,10 +3,27 @@
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
- * but without 0/1/I/O). Retries until the id is not in `existing` (project-scoped
8
- * collision guard). Throws only in the impossible saturation case (~50 retries). */
9
- export declare function createShortId(existing?: Set<string>): string;
21
+ * but without 0/1/I/O). If `seed` is provided, the id is derived deterministically
22
+ * from a stable hash of the seed so two worktrees starting from the same commit
23
+ * produce identical shortIds for the same entity. Retries until the id is not in
24
+ * `existing` (project-scoped collision guard). Throws only in the impossible
25
+ * saturation case (~50 retries). */
26
+ export declare function createShortId(existing?: Set<string>, seed?: string): string;
10
27
  export declare function normalizeSlug(input: string): string;
11
28
  /** Normalize, truncate to maxLen, and strip dangling dashes so the result
12
29
  * always satisfies SlugSchema (/^[a-z0-9]+(?:-[a-z0-9]+)*$/). Returns fallback
@@ -39,4 +56,5 @@ export declare function createRequirementId(): string;
39
56
  export declare function createMacroTaskId(): string;
40
57
  export declare function createFeatureId(): string;
41
58
  export declare function createChecklistItemId(taskId: string, number: number, title: string): string;
59
+ export declare function createStatusLogEntryId(): string;
42
60
  //# sourceMappingURL=naming.d.ts.map
@@ -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"}
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;;;;;qCAKqC;AACrC,wBAAgB,aAAa,CAAC,QAAQ,GAAE,GAAG,CAAC,MAAM,CAAa,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CA2BtF;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,10 +6,49 @@ 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
- * but without 0/1/I/O). Retries until the id is not in `existing` (project-scoped
11
- * collision guard). Throws only in the impossible saturation case (~50 retries). */
12
- export function createShortId(existing = new Set()) {
28
+ * but without 0/1/I/O). If `seed` is provided, the id is derived deterministically
29
+ * from a stable hash of the seed so two worktrees starting from the same commit
30
+ * produce identical shortIds for the same entity. Retries until the id is not in
31
+ * `existing` (project-scoped collision guard). Throws only in the impossible
32
+ * saturation case (~50 retries). */
33
+ export function createShortId(existing = new Set(), seed) {
34
+ if (seed) {
35
+ // Stable string hash -> Crockford encoding.
36
+ let hash = 0;
37
+ for (const c of seed) {
38
+ hash = ((hash << 5) - hash + c.charCodeAt(0)) | 0;
39
+ }
40
+ const base = Math.abs(hash);
41
+ for (let offset = 0; offset < 64; offset += 1) {
42
+ let id = "";
43
+ let value = base + offset;
44
+ for (let i = 0; i < SHORT_ID_LENGTH; i += 1) {
45
+ id = CROCKFORD_ALPHABET[value % CROCKFORD_ALPHABET.length] + id;
46
+ value = Math.floor(value / CROCKFORD_ALPHABET.length);
47
+ }
48
+ if (!existing.has(id))
49
+ return id;
50
+ }
51
+ }
13
52
  const max = CROCKFORD_ALPHABET.length;
14
53
  for (let attempt = 0; attempt < 64; attempt += 1) {
15
54
  let id = "";
@@ -86,3 +125,6 @@ export function createFeatureId() {
86
125
  export function createChecklistItemId(taskId, number, title) {
87
126
  return `${taskId}-check-${formatThreeDigitNumber(number)}-${normalizeSlug(title)}`;
88
127
  }
128
+ export function createStatusLogEntryId() {
129
+ return randomUUID();
130
+ }