@anthropologies/claudestory 0.1.8 → 0.1.9

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/index.js CHANGED
@@ -9,6 +9,9 @@ var TICKET_STATUSES = ["open", "inprogress", "complete"];
9
9
  var TICKET_TYPES = ["task", "feature", "chore"];
10
10
  var ISSUE_STATUSES = ["open", "inprogress", "resolved"];
11
11
  var ISSUE_SEVERITIES = ["critical", "high", "medium", "low"];
12
+ var NOTE_STATUSES = ["active", "archived"];
13
+ var NOTE_ID_REGEX = /^N-\d+$/;
14
+ var NoteIdSchema = z.string().regex(NOTE_ID_REGEX, "Note ID must match N-NNN");
12
15
  var OUTPUT_FORMATS = ["json", "md"];
13
16
  var ERROR_CODES = [
14
17
  "not_found",
@@ -72,47 +75,65 @@ var IssueSchema = z3.object({
72
75
  lastModifiedBy: z3.string().nullable().optional()
73
76
  }).passthrough();
74
77
 
75
- // src/models/roadmap.ts
78
+ // src/models/note.ts
76
79
  import { z as z4 } from "zod";
77
- var BlockerSchema = z4.object({
78
- name: z4.string().min(1),
80
+ var NoteSchema = z4.object({
81
+ id: NoteIdSchema,
82
+ title: z4.preprocess((v) => v ?? null, z4.string().nullable()),
83
+ content: z4.string().refine((v) => v.trim().length > 0, "Content cannot be empty"),
84
+ tags: z4.preprocess(
85
+ (v) => {
86
+ const raw = Array.isArray(v) ? v : [];
87
+ return raw.filter((t) => typeof t === "string").map((t) => t.trim().toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "")).filter((t) => t.length > 0).filter((t, i, a) => a.indexOf(t) === i);
88
+ },
89
+ z4.array(z4.string())
90
+ ),
91
+ status: z4.enum(NOTE_STATUSES),
92
+ createdDate: DateSchema,
93
+ updatedDate: DateSchema
94
+ }).passthrough();
95
+
96
+ // src/models/roadmap.ts
97
+ import { z as z5 } from "zod";
98
+ var BlockerSchema = z5.object({
99
+ name: z5.string().min(1),
79
100
  // Legacy format (pre-T-082)
80
- cleared: z4.boolean().optional(),
101
+ cleared: z5.boolean().optional(),
81
102
  // New date-based format (T-082 migration)
82
103
  createdDate: DateSchema.optional(),
83
104
  clearedDate: DateSchema.nullable().optional(),
84
105
  // Present in all current data but optional for future minimal blockers
85
- note: z4.string().nullable().optional()
106
+ note: z5.string().nullable().optional()
86
107
  }).passthrough();
87
- var PhaseSchema = z4.object({
88
- id: z4.string().min(1),
89
- label: z4.string(),
90
- name: z4.string(),
91
- description: z4.string(),
92
- summary: z4.string().optional()
108
+ var PhaseSchema = z5.object({
109
+ id: z5.string().min(1),
110
+ label: z5.string(),
111
+ name: z5.string(),
112
+ description: z5.string(),
113
+ summary: z5.string().optional()
93
114
  }).passthrough();
94
- var RoadmapSchema = z4.object({
95
- title: z4.string(),
115
+ var RoadmapSchema = z5.object({
116
+ title: z5.string(),
96
117
  date: DateSchema,
97
- phases: z4.array(PhaseSchema),
98
- blockers: z4.array(BlockerSchema)
118
+ phases: z5.array(PhaseSchema),
119
+ blockers: z5.array(BlockerSchema)
99
120
  }).passthrough();
100
121
 
101
122
  // src/models/config.ts
102
- import { z as z5 } from "zod";
103
- var FeaturesSchema = z5.object({
104
- tickets: z5.boolean(),
105
- issues: z5.boolean(),
106
- handovers: z5.boolean(),
107
- roadmap: z5.boolean(),
108
- reviews: z5.boolean()
123
+ import { z as z6 } from "zod";
124
+ var FeaturesSchema = z6.object({
125
+ tickets: z6.boolean(),
126
+ issues: z6.boolean(),
127
+ handovers: z6.boolean(),
128
+ roadmap: z6.boolean(),
129
+ reviews: z6.boolean()
109
130
  }).passthrough();
110
- var ConfigSchema = z5.object({
111
- version: z5.number().int().min(1),
112
- schemaVersion: z5.number().int().optional(),
113
- project: z5.string().min(1),
114
- type: z5.string(),
115
- language: z5.string(),
131
+ var ConfigSchema = z6.object({
132
+ version: z6.number().int().min(1),
133
+ schemaVersion: z6.number().int().optional(),
134
+ project: z6.string().min(1),
135
+ type: z6.string(),
136
+ language: z6.string(),
116
137
  features: FeaturesSchema
117
138
  }).passthrough();
118
139
 
@@ -121,6 +142,7 @@ var ProjectState = class _ProjectState {
121
142
  // --- Public raw inputs (readonly) ---
122
143
  tickets;
123
144
  issues;
145
+ notes;
124
146
  roadmap;
125
147
  config;
126
148
  handoverFilenames;
@@ -135,15 +157,19 @@ var ProjectState = class _ProjectState {
135
157
  reverseBlocksMap;
136
158
  ticketsByID;
137
159
  issuesByID;
160
+ notesByID;
138
161
  // --- Counts ---
139
162
  totalTicketCount;
140
163
  openTicketCount;
141
164
  completeTicketCount;
142
165
  openIssueCount;
143
166
  issuesBySeverity;
167
+ activeNoteCount;
168
+ archivedNoteCount;
144
169
  constructor(input) {
145
170
  this.tickets = input.tickets;
146
171
  this.issues = input.issues;
172
+ this.notes = input.notes;
147
173
  this.roadmap = input.roadmap;
148
174
  this.config = input.config;
149
175
  this.handoverFilenames = input.handoverFilenames;
@@ -209,6 +235,11 @@ var ProjectState = class _ProjectState {
209
235
  iByID.set(i.id, i);
210
236
  }
211
237
  this.issuesByID = iByID;
238
+ const nByID = /* @__PURE__ */ new Map();
239
+ for (const n of input.notes) {
240
+ nByID.set(n.id, n);
241
+ }
242
+ this.notesByID = nByID;
212
243
  this.totalTicketCount = input.tickets.length;
213
244
  this.openTicketCount = input.tickets.filter(
214
245
  (t) => t.status !== "complete"
@@ -226,6 +257,12 @@ var ProjectState = class _ProjectState {
226
257
  }
227
258
  }
228
259
  this.issuesBySeverity = bySev;
260
+ this.activeNoteCount = input.notes.filter(
261
+ (n) => n.status === "active"
262
+ ).length;
263
+ this.archivedNoteCount = input.notes.filter(
264
+ (n) => n.status === "archived"
265
+ ).length;
229
266
  }
230
267
  // --- Query Methods ---
231
268
  isUmbrella(ticket) {
@@ -272,6 +309,9 @@ var ProjectState = class _ProjectState {
272
309
  issueByID(id) {
273
310
  return this.issuesByID.get(id);
274
311
  }
312
+ noteByID(id) {
313
+ return this.notesByID.get(id);
314
+ }
275
315
  // --- Deletion Safety ---
276
316
  /** IDs of tickets that list `ticketId` in their blockedBy. */
277
317
  ticketsBlocking(ticketId) {
@@ -331,7 +371,8 @@ import {
331
371
  stat,
332
372
  realpath,
333
373
  lstat,
334
- open
374
+ open,
375
+ mkdir
335
376
  } from "fs/promises";
336
377
  import { existsSync as existsSync2 } from "fs";
337
378
  import { join as join2, resolve, relative as relative2, extname as extname2, dirname, basename } from "path";
@@ -462,6 +503,12 @@ async function loadProject(root, options) {
462
503
  IssueSchema,
463
504
  warnings
464
505
  );
506
+ const notes = await loadDirectory(
507
+ join2(wrapDir, "notes"),
508
+ absRoot,
509
+ NoteSchema,
510
+ warnings
511
+ );
465
512
  const handoversDir = join2(wrapDir, "handovers");
466
513
  const handoverFilenames = await listHandovers(
467
514
  handoversDir,
@@ -482,6 +529,7 @@ async function loadProject(root, options) {
482
529
  const state = new ProjectState({
483
530
  tickets,
484
531
  issues,
532
+ notes,
485
533
  roadmap,
486
534
  config,
487
535
  handoverFilenames
@@ -620,6 +668,49 @@ async function deleteIssue(id, root) {
620
668
  await unlink(targetPath);
621
669
  });
622
670
  }
671
+ async function writeNoteUnlocked(note, root) {
672
+ const parsed = NoteSchema.parse(note);
673
+ if (!NOTE_ID_REGEX.test(parsed.id)) {
674
+ throw new ProjectLoaderError(
675
+ "invalid_input",
676
+ `Invalid note ID: ${parsed.id}`
677
+ );
678
+ }
679
+ const wrapDir = resolve(root, ".story");
680
+ const targetPath = join2(wrapDir, "notes", `${parsed.id}.json`);
681
+ await mkdir(dirname(targetPath), { recursive: true });
682
+ await guardPath(targetPath, wrapDir);
683
+ const json = serializeJSON(parsed);
684
+ await atomicWrite(targetPath, json);
685
+ }
686
+ async function writeNote(note, root) {
687
+ const wrapDir = resolve(root, ".story");
688
+ await withLock(wrapDir, async () => {
689
+ await writeNoteUnlocked(note, root);
690
+ });
691
+ }
692
+ async function deleteNote(id, root) {
693
+ if (!NOTE_ID_REGEX.test(id)) {
694
+ throw new ProjectLoaderError(
695
+ "invalid_input",
696
+ `Invalid note ID: ${id}`
697
+ );
698
+ }
699
+ const wrapDir = resolve(root, ".story");
700
+ const targetPath = join2(wrapDir, "notes", `${id}.json`);
701
+ await guardPath(targetPath, wrapDir);
702
+ await withLock(wrapDir, async () => {
703
+ try {
704
+ await stat(targetPath);
705
+ } catch {
706
+ throw new ProjectLoaderError(
707
+ "not_found",
708
+ `Note file not found: notes/${id}.json`
709
+ );
710
+ }
711
+ await unlink(targetPath);
712
+ });
713
+ }
623
714
  async function withProjectLock(root, options, handler) {
624
715
  const absRoot = resolve(root);
625
716
  const wrapDir = join2(absRoot, ".story");
@@ -783,8 +874,9 @@ async function loadProjectUnlocked(absRoot) {
783
874
  const warnings = [];
784
875
  const tickets = await loadDirectory(join2(wrapDir, "tickets"), absRoot, TicketSchema, warnings);
785
876
  const issues = await loadDirectory(join2(wrapDir, "issues"), absRoot, IssueSchema, warnings);
877
+ const notes = await loadDirectory(join2(wrapDir, "notes"), absRoot, NoteSchema, warnings);
786
878
  const handoverFilenames = await listHandovers(join2(wrapDir, "handovers"), absRoot, warnings);
787
- const state = new ProjectState({ tickets, issues, roadmap, config, handoverFilenames });
879
+ const state = new ProjectState({ tickets, issues, notes, roadmap, config, handoverFilenames });
788
880
  return { state, warnings };
789
881
  }
790
882
  async function loadSingletonFile(filename, wrapDir, root, schema) {
@@ -1045,6 +1137,53 @@ function nextTicket(state) {
1045
1137
  }
1046
1138
  return { kind: "empty_project" };
1047
1139
  }
1140
+ function nextTickets(state, count) {
1141
+ const effectiveCount = Math.max(1, count);
1142
+ const phases = state.roadmap.phases;
1143
+ if (phases.length === 0 || state.leafTickets.length === 0) {
1144
+ return { kind: "empty_project" };
1145
+ }
1146
+ const candidates = [];
1147
+ const skippedBlockedPhases = [];
1148
+ let allPhasesComplete = true;
1149
+ for (const phase of phases) {
1150
+ if (candidates.length >= effectiveCount) break;
1151
+ const leaves = state.phaseTickets(phase.id);
1152
+ if (leaves.length === 0) continue;
1153
+ const status = state.phaseStatus(phase.id);
1154
+ if (status === "complete") continue;
1155
+ allPhasesComplete = false;
1156
+ const incompleteLeaves = leaves.filter((t) => t.status !== "complete");
1157
+ const unblocked = incompleteLeaves.filter((t) => !state.isBlocked(t));
1158
+ if (unblocked.length === 0) {
1159
+ skippedBlockedPhases.push({
1160
+ phaseId: phase.id,
1161
+ blockedCount: incompleteLeaves.length
1162
+ });
1163
+ continue;
1164
+ }
1165
+ const remaining = effectiveCount - candidates.length;
1166
+ for (const ticket of unblocked.slice(0, remaining)) {
1167
+ const impact = ticketsUnblockedBy(ticket.id, state);
1168
+ const progress = ticket.parentTicket ? umbrellaProgress(ticket.parentTicket, state) : null;
1169
+ candidates.push({
1170
+ ticket,
1171
+ unblockImpact: { ticketId: ticket.id, wouldUnblock: impact },
1172
+ umbrellaProgress: progress
1173
+ });
1174
+ }
1175
+ }
1176
+ if (candidates.length > 0) {
1177
+ return { kind: "found", candidates, skippedBlockedPhases };
1178
+ }
1179
+ if (skippedBlockedPhases.length > 0) {
1180
+ return { kind: "all_blocked", phases: skippedBlockedPhases };
1181
+ }
1182
+ if (allPhasesComplete) {
1183
+ return { kind: "all_complete" };
1184
+ }
1185
+ return { kind: "empty_project" };
1186
+ }
1048
1187
  function blockedTickets(state) {
1049
1188
  return state.leafTickets.filter(
1050
1189
  (t) => t.status !== "complete" && state.isBlocked(t)
@@ -1092,6 +1231,9 @@ function isBlockerCleared(blocker) {
1092
1231
  if (blocker.clearedDate != null) return true;
1093
1232
  return false;
1094
1233
  }
1234
+ function descendantLeaves(ticketId, state) {
1235
+ return collectDescendantLeaves(ticketId, state, /* @__PURE__ */ new Set());
1236
+ }
1095
1237
  function collectDescendantLeaves(ticketId, state, visited) {
1096
1238
  if (visited.has(ticketId)) return [];
1097
1239
  visited.add(ticketId);
@@ -1107,39 +1249,6 @@ function collectDescendantLeaves(ticketId, state, visited) {
1107
1249
  return leaves;
1108
1250
  }
1109
1251
 
1110
- // src/core/id-allocation.ts
1111
- var TICKET_NUMERIC_REGEX = /^T-(\d+)[a-z]?$/;
1112
- var ISSUE_NUMERIC_REGEX = /^ISS-(\d+)$/;
1113
- function nextTicketID(tickets) {
1114
- let max = 0;
1115
- for (const t of tickets) {
1116
- if (!TICKET_ID_REGEX.test(t.id)) continue;
1117
- const match = t.id.match(TICKET_NUMERIC_REGEX);
1118
- if (match?.[1]) {
1119
- const num = parseInt(match[1], 10);
1120
- if (num > max) max = num;
1121
- }
1122
- }
1123
- return `T-${String(max + 1).padStart(3, "0")}`;
1124
- }
1125
- function nextIssueID(issues) {
1126
- let max = 0;
1127
- for (const i of issues) {
1128
- if (!ISSUE_ID_REGEX.test(i.id)) continue;
1129
- const match = i.id.match(ISSUE_NUMERIC_REGEX);
1130
- if (match?.[1]) {
1131
- const num = parseInt(match[1], 10);
1132
- if (num > max) max = num;
1133
- }
1134
- }
1135
- return `ISS-${String(max + 1).padStart(3, "0")}`;
1136
- }
1137
- function nextOrder(phaseId, state) {
1138
- const tickets = state.phaseTickets(phaseId);
1139
- if (tickets.length === 0) return 10;
1140
- return tickets[tickets.length - 1].order + 10;
1141
- }
1142
-
1143
1252
  // src/core/validation.ts
1144
1253
  function validateProject(state) {
1145
1254
  const findings = [];
@@ -1176,6 +1285,20 @@ function validateProject(state) {
1176
1285
  });
1177
1286
  }
1178
1287
  }
1288
+ const noteIDCounts = /* @__PURE__ */ new Map();
1289
+ for (const n of state.notes) {
1290
+ noteIDCounts.set(n.id, (noteIDCounts.get(n.id) ?? 0) + 1);
1291
+ }
1292
+ for (const [id, count] of noteIDCounts) {
1293
+ if (count > 1) {
1294
+ findings.push({
1295
+ level: "error",
1296
+ code: "duplicate_note_id",
1297
+ message: `Duplicate note ID: ${id} appears ${count} times.`,
1298
+ entity: id
1299
+ });
1300
+ }
1301
+ }
1179
1302
  const phaseIDCounts = /* @__PURE__ */ new Map();
1180
1303
  for (const p of state.roadmap.phases) {
1181
1304
  phaseIDCounts.set(p.id, (phaseIDCounts.get(p.id) ?? 0) + 1);
@@ -1381,9 +1504,285 @@ function dfsBlocked(id, state, visited, inStack, findings) {
1381
1504
  visited.add(id);
1382
1505
  }
1383
1506
 
1507
+ // src/core/recommend.ts
1508
+ var SEVERITY_RANK = {
1509
+ critical: 4,
1510
+ high: 3,
1511
+ medium: 2,
1512
+ low: 1
1513
+ };
1514
+ var PHASE_DISTANCE_PENALTY = 100;
1515
+ var MAX_PHASE_PENALTY = 400;
1516
+ var CATEGORY_PRIORITY = {
1517
+ validation_errors: 1,
1518
+ critical_issue: 2,
1519
+ inprogress_ticket: 3,
1520
+ high_impact_unblock: 4,
1521
+ near_complete_umbrella: 5,
1522
+ phase_momentum: 6,
1523
+ quick_win: 7,
1524
+ open_issue: 8
1525
+ };
1526
+ function recommend(state, count) {
1527
+ const effectiveCount = Math.max(1, Math.min(10, count));
1528
+ const dedup = /* @__PURE__ */ new Map();
1529
+ const phaseIndex = buildPhaseIndex(state);
1530
+ const generators = [
1531
+ () => generateValidationSuggestions(state),
1532
+ () => generateCriticalIssues(state),
1533
+ () => generateInProgressTickets(state, phaseIndex),
1534
+ () => generateHighImpactUnblocks(state),
1535
+ () => generateNearCompleteUmbrellas(state, phaseIndex),
1536
+ () => generatePhaseMomentum(state),
1537
+ () => generateQuickWins(state, phaseIndex),
1538
+ () => generateOpenIssues(state)
1539
+ ];
1540
+ for (const gen of generators) {
1541
+ for (const rec of gen()) {
1542
+ const existing = dedup.get(rec.id);
1543
+ if (!existing || rec.score > existing.score) {
1544
+ dedup.set(rec.id, rec);
1545
+ }
1546
+ }
1547
+ }
1548
+ const curPhase = currentPhase(state);
1549
+ const curPhaseIdx = curPhase ? phaseIndex.get(curPhase.id) ?? 0 : 0;
1550
+ for (const [id, rec] of dedup) {
1551
+ if (rec.kind !== "ticket") continue;
1552
+ const ticket = state.ticketByID(id);
1553
+ if (!ticket || ticket.phase == null) continue;
1554
+ const ticketPhaseIdx = phaseIndex.get(ticket.phase);
1555
+ if (ticketPhaseIdx === void 0) continue;
1556
+ const phasesAhead = ticketPhaseIdx - curPhaseIdx;
1557
+ if (phasesAhead > 0) {
1558
+ const penalty = Math.min(phasesAhead * PHASE_DISTANCE_PENALTY, MAX_PHASE_PENALTY);
1559
+ dedup.set(id, {
1560
+ ...rec,
1561
+ score: rec.score - penalty,
1562
+ reason: rec.reason + " (future phase)"
1563
+ });
1564
+ }
1565
+ }
1566
+ const all = [...dedup.values()].sort((a, b) => {
1567
+ if (b.score !== a.score) return b.score - a.score;
1568
+ const catDiff = CATEGORY_PRIORITY[a.category] - CATEGORY_PRIORITY[b.category];
1569
+ if (catDiff !== 0) return catDiff;
1570
+ return a.id.localeCompare(b.id);
1571
+ });
1572
+ return {
1573
+ recommendations: all.slice(0, effectiveCount),
1574
+ totalCandidates: all.length
1575
+ };
1576
+ }
1577
+ function generateValidationSuggestions(state) {
1578
+ const result = validateProject(state);
1579
+ if (result.errorCount === 0) return [];
1580
+ return [
1581
+ {
1582
+ id: "validate",
1583
+ kind: "action",
1584
+ title: "Run claudestory validate",
1585
+ category: "validation_errors",
1586
+ reason: `${result.errorCount} validation error${result.errorCount === 1 ? "" : "s"} \u2014 fix before other work`,
1587
+ score: 1e3
1588
+ }
1589
+ ];
1590
+ }
1591
+ function generateCriticalIssues(state) {
1592
+ const issues = state.issues.filter(
1593
+ (i) => i.status !== "resolved" && (i.severity === "critical" || i.severity === "high")
1594
+ ).sort((a, b) => {
1595
+ const sevDiff = SEVERITY_RANK[b.severity] - SEVERITY_RANK[a.severity];
1596
+ if (sevDiff !== 0) return sevDiff;
1597
+ return b.discoveredDate.localeCompare(a.discoveredDate);
1598
+ });
1599
+ return issues.map((issue, index) => ({
1600
+ id: issue.id,
1601
+ kind: "issue",
1602
+ title: issue.title,
1603
+ category: "critical_issue",
1604
+ reason: issue.status === "inprogress" ? `${capitalize(issue.severity)} severity issue \u2014 in-progress, ensure it's being addressed` : `${capitalize(issue.severity)} severity issue \u2014 address before new features`,
1605
+ score: 900 - Math.min(index, 99)
1606
+ }));
1607
+ }
1608
+ function generateInProgressTickets(state, phaseIndex) {
1609
+ const tickets = state.leafTickets.filter(
1610
+ (t) => t.status === "inprogress"
1611
+ );
1612
+ const sorted = sortByPhaseAndOrder(tickets, phaseIndex);
1613
+ return sorted.map((ticket, index) => ({
1614
+ id: ticket.id,
1615
+ kind: "ticket",
1616
+ title: ticket.title,
1617
+ category: "inprogress_ticket",
1618
+ reason: "In-progress \u2014 finish what's started",
1619
+ score: 800 - Math.min(index, 99)
1620
+ }));
1621
+ }
1622
+ function generateHighImpactUnblocks(state) {
1623
+ const candidates = [];
1624
+ for (const ticket of state.leafTickets) {
1625
+ if (ticket.status === "complete") continue;
1626
+ if (state.isBlocked(ticket)) continue;
1627
+ const wouldUnblock = ticketsUnblockedBy(ticket.id, state);
1628
+ if (wouldUnblock.length >= 2) {
1629
+ candidates.push({ ticket, unblockCount: wouldUnblock.length });
1630
+ }
1631
+ }
1632
+ candidates.sort((a, b) => b.unblockCount - a.unblockCount);
1633
+ return candidates.map(({ ticket, unblockCount }, index) => ({
1634
+ id: ticket.id,
1635
+ kind: "ticket",
1636
+ title: ticket.title,
1637
+ category: "high_impact_unblock",
1638
+ reason: `Completing this unblocks ${unblockCount} other ticket${unblockCount === 1 ? "" : "s"}`,
1639
+ score: 700 - Math.min(index, 99)
1640
+ }));
1641
+ }
1642
+ function generateNearCompleteUmbrellas(state, phaseIndex) {
1643
+ const candidates = [];
1644
+ for (const umbrellaId of state.umbrellaIDs) {
1645
+ const progress = umbrellaProgress(umbrellaId, state);
1646
+ if (!progress) continue;
1647
+ if (progress.total < 2) continue;
1648
+ if (progress.status === "complete") continue;
1649
+ const ratio = progress.complete / progress.total;
1650
+ if (ratio < 0.8) continue;
1651
+ const leaves = descendantLeaves(umbrellaId, state);
1652
+ const incomplete = leaves.filter((t) => t.status !== "complete");
1653
+ const sorted = sortByPhaseAndOrder(incomplete, phaseIndex);
1654
+ if (sorted.length === 0) continue;
1655
+ const umbrella = state.ticketByID(umbrellaId);
1656
+ candidates.push({
1657
+ umbrellaId,
1658
+ umbrellaTitle: umbrella?.title ?? umbrellaId,
1659
+ firstIncompleteLeaf: sorted[0],
1660
+ complete: progress.complete,
1661
+ total: progress.total,
1662
+ ratio
1663
+ });
1664
+ }
1665
+ candidates.sort((a, b) => b.ratio - a.ratio);
1666
+ return candidates.map((c, index) => ({
1667
+ id: c.firstIncompleteLeaf.id,
1668
+ kind: "ticket",
1669
+ title: c.firstIncompleteLeaf.title,
1670
+ category: "near_complete_umbrella",
1671
+ reason: `${c.complete}/${c.total} complete in umbrella ${c.umbrellaId} \u2014 close it out`,
1672
+ score: 600 - Math.min(index, 99)
1673
+ }));
1674
+ }
1675
+ function generatePhaseMomentum(state) {
1676
+ const outcome = nextTicket(state);
1677
+ if (outcome.kind !== "found") return [];
1678
+ const ticket = outcome.ticket;
1679
+ return [
1680
+ {
1681
+ id: ticket.id,
1682
+ kind: "ticket",
1683
+ title: ticket.title,
1684
+ category: "phase_momentum",
1685
+ reason: `Next in phase order (${ticket.phase ?? "none"})`,
1686
+ score: 500
1687
+ }
1688
+ ];
1689
+ }
1690
+ function generateQuickWins(state, phaseIndex) {
1691
+ const tickets = state.leafTickets.filter(
1692
+ (t) => t.status === "open" && t.type === "chore" && !state.isBlocked(t)
1693
+ );
1694
+ const sorted = sortByPhaseAndOrder(tickets, phaseIndex);
1695
+ return sorted.map((ticket, index) => ({
1696
+ id: ticket.id,
1697
+ kind: "ticket",
1698
+ title: ticket.title,
1699
+ category: "quick_win",
1700
+ reason: "Chore \u2014 quick win",
1701
+ score: 400 - Math.min(index, 99)
1702
+ }));
1703
+ }
1704
+ function generateOpenIssues(state) {
1705
+ const issues = state.issues.filter(
1706
+ (i) => i.status !== "resolved" && (i.severity === "medium" || i.severity === "low")
1707
+ ).sort((a, b) => {
1708
+ const sevDiff = SEVERITY_RANK[b.severity] - SEVERITY_RANK[a.severity];
1709
+ if (sevDiff !== 0) return sevDiff;
1710
+ return b.discoveredDate.localeCompare(a.discoveredDate);
1711
+ });
1712
+ return issues.map((issue, index) => ({
1713
+ id: issue.id,
1714
+ kind: "issue",
1715
+ title: issue.title,
1716
+ category: "open_issue",
1717
+ reason: issue.status === "inprogress" ? `${capitalize(issue.severity)} severity issue \u2014 in-progress` : `${capitalize(issue.severity)} severity issue`,
1718
+ score: 300 - Math.min(index, 99)
1719
+ }));
1720
+ }
1721
+ function capitalize(s) {
1722
+ return s.charAt(0).toUpperCase() + s.slice(1);
1723
+ }
1724
+ function buildPhaseIndex(state) {
1725
+ const index = /* @__PURE__ */ new Map();
1726
+ state.roadmap.phases.forEach((p, i) => index.set(p.id, i));
1727
+ return index;
1728
+ }
1729
+ function sortByPhaseAndOrder(tickets, phaseIndex) {
1730
+ return [...tickets].sort((a, b) => {
1731
+ const aPhase = (a.phase != null ? phaseIndex.get(a.phase) : void 0) ?? Number.MAX_SAFE_INTEGER;
1732
+ const bPhase = (b.phase != null ? phaseIndex.get(b.phase) : void 0) ?? Number.MAX_SAFE_INTEGER;
1733
+ if (aPhase !== bPhase) return aPhase - bPhase;
1734
+ return a.order - b.order;
1735
+ });
1736
+ }
1737
+
1738
+ // src/core/id-allocation.ts
1739
+ var TICKET_NUMERIC_REGEX = /^T-(\d+)[a-z]?$/;
1740
+ var ISSUE_NUMERIC_REGEX = /^ISS-(\d+)$/;
1741
+ var NOTE_NUMERIC_REGEX = /^N-(\d+)$/;
1742
+ function nextTicketID(tickets) {
1743
+ let max = 0;
1744
+ for (const t of tickets) {
1745
+ if (!TICKET_ID_REGEX.test(t.id)) continue;
1746
+ const match = t.id.match(TICKET_NUMERIC_REGEX);
1747
+ if (match?.[1]) {
1748
+ const num = parseInt(match[1], 10);
1749
+ if (num > max) max = num;
1750
+ }
1751
+ }
1752
+ return `T-${String(max + 1).padStart(3, "0")}`;
1753
+ }
1754
+ function nextIssueID(issues) {
1755
+ let max = 0;
1756
+ for (const i of issues) {
1757
+ if (!ISSUE_ID_REGEX.test(i.id)) continue;
1758
+ const match = i.id.match(ISSUE_NUMERIC_REGEX);
1759
+ if (match?.[1]) {
1760
+ const num = parseInt(match[1], 10);
1761
+ if (num > max) max = num;
1762
+ }
1763
+ }
1764
+ return `ISS-${String(max + 1).padStart(3, "0")}`;
1765
+ }
1766
+ function nextNoteID(notes) {
1767
+ let max = 0;
1768
+ for (const n of notes) {
1769
+ if (!NOTE_ID_REGEX.test(n.id)) continue;
1770
+ const match = n.id.match(NOTE_NUMERIC_REGEX);
1771
+ if (match?.[1]) {
1772
+ const num = parseInt(match[1], 10);
1773
+ if (num > max) max = num;
1774
+ }
1775
+ }
1776
+ return `N-${String(max + 1).padStart(3, "0")}`;
1777
+ }
1778
+ function nextOrder(phaseId, state) {
1779
+ const tickets = state.phaseTickets(phaseId);
1780
+ if (tickets.length === 0) return 10;
1781
+ return tickets[tickets.length - 1].order + 10;
1782
+ }
1783
+
1384
1784
  // src/core/init.ts
1385
- import { mkdir, stat as stat2, writeFile as writeFile2 } from "fs/promises";
1386
- import { existsSync as existsSync4 } from "fs";
1785
+ import { mkdir as mkdir2, stat as stat2 } from "fs/promises";
1387
1786
  import { join as join4, resolve as resolve3 } from "path";
1388
1787
  async function initProject(root, options) {
1389
1788
  const absRoot = resolve3(root);
@@ -1407,15 +1806,17 @@ async function initProject(root, options) {
1407
1806
  ".story/ already exists. Use --force to overwrite config and roadmap."
1408
1807
  );
1409
1808
  }
1410
- await mkdir(join4(wrapDir, "tickets"), { recursive: true });
1411
- await mkdir(join4(wrapDir, "issues"), { recursive: true });
1412
- await mkdir(join4(wrapDir, "handovers"), { recursive: true });
1809
+ await mkdir2(join4(wrapDir, "tickets"), { recursive: true });
1810
+ await mkdir2(join4(wrapDir, "issues"), { recursive: true });
1811
+ await mkdir2(join4(wrapDir, "handovers"), { recursive: true });
1812
+ await mkdir2(join4(wrapDir, "notes"), { recursive: true });
1413
1813
  const created = [
1414
1814
  ".story/config.json",
1415
1815
  ".story/roadmap.json",
1416
1816
  ".story/tickets/",
1417
1817
  ".story/issues/",
1418
- ".story/handovers/"
1818
+ ".story/handovers/",
1819
+ ".story/notes/"
1419
1820
  ];
1420
1821
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1421
1822
  const config = {
@@ -1447,13 +1848,6 @@ async function initProject(root, options) {
1447
1848
  };
1448
1849
  await writeConfig(config, absRoot);
1449
1850
  await writeRoadmap(roadmap, absRoot);
1450
- const skillDir = join4(absRoot, ".claude", "skills", "prime");
1451
- const skillPath = join4(skillDir, "SKILL.md");
1452
- if (!existsSync4(skillPath)) {
1453
- await mkdir(skillDir, { recursive: true });
1454
- await writeFile2(skillPath, PRIME_SKILL_CONTENT, "utf-8");
1455
- created.push(".claude/skills/prime/SKILL.md");
1456
- }
1457
1851
  const warnings = [];
1458
1852
  if (options.force && exists) {
1459
1853
  try {
@@ -1472,97 +1866,33 @@ async function initProject(root, options) {
1472
1866
  warnings
1473
1867
  };
1474
1868
  }
1475
- var PRIME_SKILL_CONTENT = `---
1476
- name: prime
1477
- description: Load full claudestory project context. Use at session start for any project with a .story/ directory.
1478
- ---
1479
-
1480
- # Prime: Load Project Context
1481
-
1482
- Get full project context in one command for any project using claudestory.
1483
-
1484
- ## Step 0: Check Setup
1485
-
1486
- First, check if the claudestory MCP tools are available by looking for \`claudestory_status\` in your available tools.
1487
-
1488
- **If MCP tools ARE available**, proceed to Step 1.
1489
-
1490
- **If MCP tools are NOT available**, help the user set up:
1491
-
1492
- 1. Check if the \`claudestory\` CLI is installed by running: \`claudestory --version\`
1493
- 2. If NOT installed, tell the user:
1494
- \`\`\`
1495
- claudestory CLI not found. To set up:
1496
- npm install -g @anthropologies/claudestory
1497
- claude mcp add claudestory -s user -- claudestory --mcp
1498
- Then restart Claude Code and run /prime again.
1499
- \`\`\`
1500
- 3. If CLI IS installed but MCP not registered, offer to register it for them.
1501
- With user permission, run: \`claude mcp add claudestory -s user -- claudestory --mcp\`
1502
- Tell the user to restart Claude Code and run /prime again.
1503
-
1504
- **If MCP tools are unavailable and user doesn't want to set up**, fall back to CLI:
1505
- - Run \`claudestory status\` via Bash
1506
- - Run \`claudestory recap\` via Bash
1507
- - Run \`claudestory handover latest\` via Bash
1508
- - Then continue to Steps 4-6 below.
1509
-
1510
- ## Step 1: Project Status
1511
- Call the \`claudestory_status\` MCP tool.
1512
-
1513
- ## Step 2: Session Recap
1514
- Call the \`claudestory_recap\` MCP tool.
1515
-
1516
- ## Step 3: Latest Handover
1517
- Call the \`claudestory_handover_latest\` MCP tool.
1518
-
1519
- ## Step 4: Development Rules
1520
- Read \`RULES.md\` if it exists in the project root.
1521
-
1522
- ## Step 5: Lessons Learned
1523
- Read \`WORK_STRATEGIES.md\` if it exists in the project root.
1524
-
1525
- ## Step 6: Recent Commits
1526
- Run \`git log --oneline -10\`.
1527
-
1528
- ## After Loading
1529
-
1530
- Present a concise summary:
1531
- - Project progress (X/Y tickets, current phase)
1532
- - What changed since last snapshot
1533
- - What the last session accomplished
1534
- - Next ticket to work on
1535
- - Any high-severity issues or blockers
1536
- - Key process rules (if WORK_STRATEGIES.md exists)
1537
-
1538
- Then ask: "What would you like to work on?"
1539
- `;
1540
1869
 
1541
1870
  // src/core/snapshot.ts
1542
- import { readdir as readdir3, readFile as readFile3, mkdir as mkdir2, unlink as unlink2 } from "fs/promises";
1543
- import { existsSync as existsSync5 } from "fs";
1871
+ import { readdir as readdir3, readFile as readFile3, mkdir as mkdir3, unlink as unlink2 } from "fs/promises";
1872
+ import { existsSync as existsSync4 } from "fs";
1544
1873
  import { join as join5, resolve as resolve4 } from "path";
1545
- import { z as z6 } from "zod";
1546
- var LoadWarningSchema = z6.object({
1547
- type: z6.string(),
1548
- file: z6.string(),
1549
- message: z6.string()
1874
+ import { z as z7 } from "zod";
1875
+ var LoadWarningSchema = z7.object({
1876
+ type: z7.string(),
1877
+ file: z7.string(),
1878
+ message: z7.string()
1550
1879
  });
1551
- var SnapshotV1Schema = z6.object({
1552
- version: z6.literal(1),
1553
- createdAt: z6.string().datetime({ offset: true }),
1554
- project: z6.string(),
1880
+ var SnapshotV1Schema = z7.object({
1881
+ version: z7.literal(1),
1882
+ createdAt: z7.string().datetime({ offset: true }),
1883
+ project: z7.string(),
1555
1884
  config: ConfigSchema,
1556
1885
  roadmap: RoadmapSchema,
1557
- tickets: z6.array(TicketSchema),
1558
- issues: z6.array(IssueSchema),
1559
- warnings: z6.array(LoadWarningSchema).optional()
1886
+ tickets: z7.array(TicketSchema),
1887
+ issues: z7.array(IssueSchema),
1888
+ notes: z7.array(NoteSchema).optional().default([]),
1889
+ warnings: z7.array(LoadWarningSchema).optional()
1560
1890
  });
1561
1891
  var MAX_SNAPSHOTS = 20;
1562
1892
  async function saveSnapshot(root, loadResult) {
1563
1893
  const absRoot = resolve4(root);
1564
1894
  const snapshotsDir = join5(absRoot, ".story", "snapshots");
1565
- await mkdir2(snapshotsDir, { recursive: true });
1895
+ await mkdir3(snapshotsDir, { recursive: true });
1566
1896
  const { state, warnings } = loadResult;
1567
1897
  const now = /* @__PURE__ */ new Date();
1568
1898
  const filename = formatSnapshotFilename(now);
@@ -1574,6 +1904,7 @@ async function saveSnapshot(root, loadResult) {
1574
1904
  roadmap: state.roadmap,
1575
1905
  tickets: [...state.tickets],
1576
1906
  issues: [...state.issues],
1907
+ notes: [...state.notes],
1577
1908
  ...warnings.length > 0 ? {
1578
1909
  warnings: warnings.map((w) => ({
1579
1910
  type: w.type,
@@ -1593,7 +1924,7 @@ async function saveSnapshot(root, loadResult) {
1593
1924
  }
1594
1925
  async function loadLatestSnapshot(root) {
1595
1926
  const snapshotsDir = join5(resolve4(root), ".story", "snapshots");
1596
- if (!existsSync5(snapshotsDir)) return null;
1927
+ if (!existsSync4(snapshotsDir)) return null;
1597
1928
  const files = await listSnapshotFiles(snapshotsDir);
1598
1929
  if (files.length === 0) return null;
1599
1930
  for (const filename of files) {
@@ -1614,12 +1945,18 @@ function diffStates(snapshotState, currentState) {
1614
1945
  const ticketsAdded = [];
1615
1946
  const ticketsRemoved = [];
1616
1947
  const ticketsStatusChanged = [];
1948
+ const ticketsDescriptionChanged = [];
1617
1949
  for (const [id, cur] of curTickets) {
1618
1950
  const snap = snapTickets.get(id);
1619
1951
  if (!snap) {
1620
1952
  ticketsAdded.push({ id, title: cur.title });
1621
- } else if (snap.status !== cur.status) {
1622
- ticketsStatusChanged.push({ id, title: cur.title, from: snap.status, to: cur.status });
1953
+ } else {
1954
+ if (snap.status !== cur.status) {
1955
+ ticketsStatusChanged.push({ id, title: cur.title, from: snap.status, to: cur.status });
1956
+ }
1957
+ if (snap.description !== cur.description) {
1958
+ ticketsDescriptionChanged.push({ id, title: cur.title });
1959
+ }
1623
1960
  }
1624
1961
  }
1625
1962
  for (const [id, snap] of snapTickets) {
@@ -1632,15 +1969,21 @@ function diffStates(snapshotState, currentState) {
1632
1969
  const issuesAdded = [];
1633
1970
  const issuesResolved = [];
1634
1971
  const issuesStatusChanged = [];
1972
+ const issuesImpactChanged = [];
1635
1973
  for (const [id, cur] of curIssues) {
1636
1974
  const snap = snapIssues.get(id);
1637
1975
  if (!snap) {
1638
1976
  issuesAdded.push({ id, title: cur.title });
1639
- } else if (snap.status !== cur.status) {
1640
- if (cur.status === "resolved") {
1641
- issuesResolved.push({ id, title: cur.title });
1642
- } else {
1643
- issuesStatusChanged.push({ id, title: cur.title, from: snap.status, to: cur.status });
1977
+ } else {
1978
+ if (snap.status !== cur.status) {
1979
+ if (cur.status === "resolved") {
1980
+ issuesResolved.push({ id, title: cur.title });
1981
+ } else {
1982
+ issuesStatusChanged.push({ id, title: cur.title, from: snap.status, to: cur.status });
1983
+ }
1984
+ }
1985
+ if (snap.impact !== cur.impact) {
1986
+ issuesImpactChanged.push({ id, title: cur.title });
1644
1987
  }
1645
1988
  }
1646
1989
  }
@@ -1689,11 +2032,37 @@ function diffStates(snapshotState, currentState) {
1689
2032
  phasesRemoved.push({ id, name: snapPhase.name });
1690
2033
  }
1691
2034
  }
2035
+ const snapNotes = new Map(snapshotState.notes.map((n) => [n.id, n]));
2036
+ const curNotes = new Map(currentState.notes.map((n) => [n.id, n]));
2037
+ const notesAdded = [];
2038
+ const notesRemoved = [];
2039
+ const notesUpdated = [];
2040
+ for (const [id, cur] of curNotes) {
2041
+ const snap = snapNotes.get(id);
2042
+ if (!snap) {
2043
+ notesAdded.push({ id, title: cur.title });
2044
+ } else {
2045
+ const changedFields = [];
2046
+ if (snap.title !== cur.title) changedFields.push("title");
2047
+ if (snap.content !== cur.content) changedFields.push("content");
2048
+ if (JSON.stringify([...snap.tags].sort()) !== JSON.stringify([...cur.tags].sort())) changedFields.push("tags");
2049
+ if (snap.status !== cur.status) changedFields.push("status");
2050
+ if (changedFields.length > 0) {
2051
+ notesUpdated.push({ id, title: cur.title, changedFields });
2052
+ }
2053
+ }
2054
+ }
2055
+ for (const [id, snap] of snapNotes) {
2056
+ if (!curNotes.has(id)) {
2057
+ notesRemoved.push({ id, title: snap.title });
2058
+ }
2059
+ }
1692
2060
  return {
1693
- tickets: { added: ticketsAdded, removed: ticketsRemoved, statusChanged: ticketsStatusChanged },
1694
- issues: { added: issuesAdded, resolved: issuesResolved, statusChanged: issuesStatusChanged },
2061
+ tickets: { added: ticketsAdded, removed: ticketsRemoved, statusChanged: ticketsStatusChanged, descriptionChanged: ticketsDescriptionChanged },
2062
+ issues: { added: issuesAdded, resolved: issuesResolved, statusChanged: issuesStatusChanged, impactChanged: issuesImpactChanged },
1695
2063
  blockers: { added: blockersAdded, cleared: blockersCleared },
1696
- phases: { added: phasesAdded, removed: phasesRemoved, statusChanged: phasesStatusChanged }
2064
+ phases: { added: phasesAdded, removed: phasesRemoved, statusChanged: phasesStatusChanged },
2065
+ notes: { added: notesAdded, removed: notesRemoved, updated: notesUpdated }
1697
2066
  };
1698
2067
  }
1699
2068
  function buildRecap(currentState, snapshotInfo) {
@@ -1718,6 +2087,7 @@ function buildRecap(currentState, snapshotInfo) {
1718
2087
  const snapshotState = new ProjectState({
1719
2088
  tickets: snapshot.tickets,
1720
2089
  issues: snapshot.issues,
2090
+ notes: snapshot.notes ?? [],
1721
2091
  roadmap: snapshot.roadmap,
1722
2092
  config: snapshot.config,
1723
2093
  handoverFilenames: []
@@ -1816,6 +2186,8 @@ function formatStatus(state, format) {
1816
2186
  openTickets: state.leafTicketCount - state.completeLeafTicketCount,
1817
2187
  blockedTickets: state.blockedCount,
1818
2188
  openIssues: state.openIssueCount,
2189
+ activeNotes: state.activeNoteCount,
2190
+ archivedNotes: state.archivedNoteCount,
1819
2191
  handovers: state.handoverFilenames.length,
1820
2192
  phases: phases.map((p) => ({
1821
2193
  id: p.phase.id,
@@ -1832,6 +2204,7 @@ function formatStatus(state, format) {
1832
2204
  "",
1833
2205
  `Tickets: ${state.completeLeafTicketCount}/${state.leafTicketCount} complete, ${state.blockedCount} blocked`,
1834
2206
  `Issues: ${state.openIssueCount} open`,
2207
+ `Notes: ${state.activeNoteCount} active, ${state.archivedNoteCount} archived`,
1835
2208
  `Handovers: ${state.handoverFilenames.length}`,
1836
2209
  "",
1837
2210
  "## Phases",
@@ -2056,6 +2429,7 @@ function formatInitResult(result, format) {
2056
2429
  if (result.warnings.length > 0) {
2057
2430
  lines.push("", `Warning: ${result.warnings.length} corrupt file(s) found. Run \`claudestory validate\` to inspect.`);
2058
2431
  }
2432
+ lines.push("", "Tip: Run `claudestory setup-skill` to install the /story skill for Claude Code.");
2059
2433
  return lines.join("\n");
2060
2434
  }
2061
2435
  function formatHandoverList(filenames, format) {
@@ -2119,7 +2493,7 @@ function formatRecap(recap, state, format) {
2119
2493
  }
2120
2494
  }
2121
2495
  const ticketChanges = changes.tickets;
2122
- if (ticketChanges.added.length > 0 || ticketChanges.removed.length > 0 || ticketChanges.statusChanged.length > 0) {
2496
+ if (ticketChanges.added.length > 0 || ticketChanges.removed.length > 0 || ticketChanges.statusChanged.length > 0 || ticketChanges.descriptionChanged.length > 0) {
2123
2497
  lines.push("");
2124
2498
  lines.push("## Tickets");
2125
2499
  for (const t of ticketChanges.statusChanged) {
@@ -2131,9 +2505,12 @@ function formatRecap(recap, state, format) {
2131
2505
  for (const t of ticketChanges.removed) {
2132
2506
  lines.push(`- ${t.id}: ${escapeMarkdownInline(t.title)} \u2014 **removed**`);
2133
2507
  }
2508
+ for (const t of ticketChanges.descriptionChanged) {
2509
+ lines.push(`- ${t.id}: description updated`);
2510
+ }
2134
2511
  }
2135
2512
  const issueChanges = changes.issues;
2136
- if (issueChanges.added.length > 0 || issueChanges.resolved.length > 0 || issueChanges.statusChanged.length > 0) {
2513
+ if (issueChanges.added.length > 0 || issueChanges.resolved.length > 0 || issueChanges.statusChanged.length > 0 || issueChanges.impactChanged.length > 0) {
2137
2514
  lines.push("");
2138
2515
  lines.push("## Issues");
2139
2516
  for (const i of issueChanges.resolved) {
@@ -2145,6 +2522,9 @@ function formatRecap(recap, state, format) {
2145
2522
  for (const i of issueChanges.added) {
2146
2523
  lines.push(`- ${i.id}: ${escapeMarkdownInline(i.title)} \u2014 **new**`);
2147
2524
  }
2525
+ for (const i of issueChanges.impactChanged) {
2526
+ lines.push(`- ${i.id}: impact updated`);
2527
+ }
2148
2528
  }
2149
2529
  if (changes.blockers.added.length > 0 || changes.blockers.cleared.length > 0) {
2150
2530
  lines.push("");
@@ -2156,6 +2536,19 @@ function formatRecap(recap, state, format) {
2156
2536
  lines.push(`- ${escapeMarkdownInline(name)} \u2014 **new**`);
2157
2537
  }
2158
2538
  }
2539
+ if (changes.notes && (changes.notes.added.length > 0 || changes.notes.removed.length > 0 || changes.notes.updated.length > 0)) {
2540
+ lines.push("");
2541
+ lines.push("## Notes");
2542
+ for (const n of changes.notes.added) {
2543
+ lines.push(`- ${n.id}: added`);
2544
+ }
2545
+ for (const n of changes.notes.removed) {
2546
+ lines.push(`- ${n.id}: removed`);
2547
+ }
2548
+ for (const n of changes.notes.updated) {
2549
+ lines.push(`- ${n.id}: updated (${n.changedFields.join(", ")})`);
2550
+ }
2551
+ }
2159
2552
  }
2160
2553
  }
2161
2554
  const actions = recap.suggestedActions;
@@ -2294,6 +2687,12 @@ function formatFullExport(state, format) {
2294
2687
  severity: i.severity,
2295
2688
  status: i.status
2296
2689
  })),
2690
+ notes: state.notes.map((n) => ({
2691
+ id: n.id,
2692
+ title: n.title,
2693
+ status: n.status,
2694
+ tags: n.tags
2695
+ })),
2297
2696
  blockers: state.roadmap.blockers.map((b) => ({
2298
2697
  name: b.name,
2299
2698
  cleared: isBlockerCleared(b),
@@ -2309,6 +2708,7 @@ function formatFullExport(state, format) {
2309
2708
  lines.push("");
2310
2709
  lines.push(`Tickets: ${state.completeLeafTicketCount}/${state.leafTicketCount} complete`);
2311
2710
  lines.push(`Issues: ${state.openIssueCount} open`);
2711
+ lines.push(`Notes: ${state.activeNoteCount} active, ${state.archivedNoteCount} archived`);
2312
2712
  lines.push("");
2313
2713
  lines.push("## Phases");
2314
2714
  for (const p of phases) {
@@ -2335,6 +2735,16 @@ function formatFullExport(state, format) {
2335
2735
  lines.push(`- ${i.id} [${i.severity}]: ${escapeMarkdownInline(i.title)}${resolved}`);
2336
2736
  }
2337
2737
  }
2738
+ const activeNotes = state.notes.filter((n) => n.status === "active");
2739
+ if (activeNotes.length > 0) {
2740
+ lines.push("");
2741
+ lines.push("## Notes");
2742
+ for (const n of activeNotes) {
2743
+ const title = n.title ?? n.id;
2744
+ const tagInfo = n.tags.length > 0 ? ` (${n.tags.join(", ")})` : "";
2745
+ lines.push(`- ${n.id}: ${escapeMarkdownInline(title)}${tagInfo}`);
2746
+ }
2747
+ }
2338
2748
  const blockers = state.roadmap.blockers;
2339
2749
  if (blockers.length > 0) {
2340
2750
  lines.push("");
@@ -2347,7 +2757,7 @@ function formatFullExport(state, format) {
2347
2757
  return lines.join("\n");
2348
2758
  }
2349
2759
  function hasAnyChanges(diff) {
2350
- return diff.tickets.added.length > 0 || diff.tickets.removed.length > 0 || diff.tickets.statusChanged.length > 0 || diff.issues.added.length > 0 || diff.issues.resolved.length > 0 || diff.issues.statusChanged.length > 0 || diff.blockers.added.length > 0 || diff.blockers.cleared.length > 0 || diff.phases.added.length > 0 || diff.phases.removed.length > 0 || diff.phases.statusChanged.length > 0;
2760
+ return diff.tickets.added.length > 0 || diff.tickets.removed.length > 0 || diff.tickets.statusChanged.length > 0 || diff.tickets.descriptionChanged.length > 0 || diff.issues.added.length > 0 || diff.issues.resolved.length > 0 || diff.issues.statusChanged.length > 0 || diff.issues.impactChanged.length > 0 || diff.blockers.added.length > 0 || diff.blockers.cleared.length > 0 || diff.phases.added.length > 0 || diff.phases.removed.length > 0 || diff.phases.statusChanged.length > 0 || (diff.notes?.added.length ?? 0) > 0 || (diff.notes?.removed.length ?? 0) > 0 || (diff.notes?.updated.length ?? 0) > 0;
2351
2761
  }
2352
2762
  function truncate(text, maxLen) {
2353
2763
  if (text.length <= maxLen) return text;
@@ -2358,6 +2768,29 @@ function formatTicketOneLiner(t, state) {
2358
2768
  const blocked = state.isBlocked(t) ? " [BLOCKED]" : "";
2359
2769
  return `${status} ${t.id}: ${escapeMarkdownInline(t.title)}${blocked}`;
2360
2770
  }
2771
+ function formatRecommendations(result, format) {
2772
+ if (format === "json") {
2773
+ return JSON.stringify(successEnvelope(result), null, 2);
2774
+ }
2775
+ if (result.recommendations.length === 0) {
2776
+ return "No recommendations \u2014 all work is complete or blocked.";
2777
+ }
2778
+ const lines = ["# Recommendations", ""];
2779
+ for (let i = 0; i < result.recommendations.length; i++) {
2780
+ const rec = result.recommendations[i];
2781
+ lines.push(
2782
+ `${i + 1}. **${escapeMarkdownInline(rec.id)}** (${rec.kind}) \u2014 ${escapeMarkdownInline(rec.title)}`
2783
+ );
2784
+ lines.push(` _${escapeMarkdownInline(rec.reason)}_`);
2785
+ lines.push("");
2786
+ }
2787
+ if (result.totalCandidates > result.recommendations.length) {
2788
+ lines.push(
2789
+ `Showing ${result.recommendations.length} of ${result.totalCandidates} candidates.`
2790
+ );
2791
+ }
2792
+ return lines.join("\n");
2793
+ }
2361
2794
  export {
2362
2795
  BlockerSchema,
2363
2796
  CURRENT_SCHEMA_VERSION,
@@ -2373,6 +2806,10 @@ export {
2373
2806
  ISSUE_STATUSES,
2374
2807
  IssueIdSchema,
2375
2808
  IssueSchema,
2809
+ NOTE_ID_REGEX,
2810
+ NOTE_STATUSES,
2811
+ NoteIdSchema,
2812
+ NoteSchema,
2376
2813
  OUTPUT_FORMATS,
2377
2814
  PhaseSchema,
2378
2815
  ProjectLoaderError,
@@ -2389,7 +2826,9 @@ export {
2389
2826
  buildRecap,
2390
2827
  currentPhase,
2391
2828
  deleteIssue,
2829
+ deleteNote,
2392
2830
  deleteTicket,
2831
+ descendantLeaves,
2393
2832
  diffStates,
2394
2833
  discoverProjectRoot,
2395
2834
  errorEnvelope,
@@ -2410,6 +2849,7 @@ export {
2410
2849
  formatPhaseList,
2411
2850
  formatPhaseTickets,
2412
2851
  formatRecap,
2852
+ formatRecommendations,
2413
2853
  formatSnapshotResult,
2414
2854
  formatStatus,
2415
2855
  formatTicket,
@@ -2423,12 +2863,15 @@ export {
2423
2863
  loadProject,
2424
2864
  mergeValidation,
2425
2865
  nextIssueID,
2866
+ nextNoteID,
2426
2867
  nextOrder,
2427
2868
  nextTicket,
2428
2869
  nextTicketID,
2870
+ nextTickets,
2429
2871
  partialEnvelope,
2430
2872
  phasesWithStatus,
2431
2873
  readHandover,
2874
+ recommend,
2432
2875
  runTransaction,
2433
2876
  runTransactionUnlocked,
2434
2877
  saveSnapshot,
@@ -2442,6 +2885,8 @@ export {
2442
2885
  writeConfig,
2443
2886
  writeIssue,
2444
2887
  writeIssueUnlocked,
2888
+ writeNote,
2889
+ writeNoteUnlocked,
2445
2890
  writeRoadmap,
2446
2891
  writeRoadmapUnlocked,
2447
2892
  writeTicket,