@anthropologies/claudestory 0.1.7 → 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/cli.js CHANGED
@@ -100,7 +100,7 @@ var init_project_root_discovery = __esm({
100
100
 
101
101
  // src/models/types.ts
102
102
  import { z } from "zod";
103
- var TICKET_ID_REGEX, ISSUE_ID_REGEX, TICKET_STATUSES, TICKET_TYPES, ISSUE_STATUSES, ISSUE_SEVERITIES, OUTPUT_FORMATS, DATE_REGEX, DateSchema, TicketIdSchema, IssueIdSchema;
103
+ var TICKET_ID_REGEX, ISSUE_ID_REGEX, TICKET_STATUSES, TICKET_TYPES, ISSUE_STATUSES, ISSUE_SEVERITIES, NOTE_STATUSES, NOTE_ID_REGEX, NoteIdSchema, OUTPUT_FORMATS, DATE_REGEX, DateSchema, TicketIdSchema, IssueIdSchema;
104
104
  var init_types = __esm({
105
105
  "src/models/types.ts"() {
106
106
  "use strict";
@@ -111,6 +111,9 @@ var init_types = __esm({
111
111
  TICKET_TYPES = ["task", "feature", "chore"];
112
112
  ISSUE_STATUSES = ["open", "inprogress", "resolved"];
113
113
  ISSUE_SEVERITIES = ["critical", "high", "medium", "low"];
114
+ NOTE_STATUSES = ["active", "archived"];
115
+ NOTE_ID_REGEX = /^N-\d+$/;
116
+ NoteIdSchema = z.string().regex(NOTE_ID_REGEX, "Note ID must match N-NNN");
114
117
  OUTPUT_FORMATS = ["json", "md"];
115
118
  DATE_REGEX = /^\d{4}-\d{2}-\d{2}$/;
116
119
  DateSchema = z.string().regex(DATE_REGEX, "Date must be YYYY-MM-DD").refine(
@@ -184,60 +187,86 @@ var init_issue = __esm({
184
187
  }
185
188
  });
186
189
 
187
- // src/models/roadmap.ts
190
+ // src/models/note.ts
188
191
  import { z as z4 } from "zod";
192
+ var NoteSchema;
193
+ var init_note = __esm({
194
+ "src/models/note.ts"() {
195
+ "use strict";
196
+ init_esm_shims();
197
+ init_types();
198
+ NoteSchema = z4.object({
199
+ id: NoteIdSchema,
200
+ title: z4.preprocess((v) => v ?? null, z4.string().nullable()),
201
+ content: z4.string().refine((v) => v.trim().length > 0, "Content cannot be empty"),
202
+ tags: z4.preprocess(
203
+ (v) => {
204
+ const raw = Array.isArray(v) ? v : [];
205
+ 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);
206
+ },
207
+ z4.array(z4.string())
208
+ ),
209
+ status: z4.enum(NOTE_STATUSES),
210
+ createdDate: DateSchema,
211
+ updatedDate: DateSchema
212
+ }).passthrough();
213
+ }
214
+ });
215
+
216
+ // src/models/roadmap.ts
217
+ import { z as z5 } from "zod";
189
218
  var BlockerSchema, PhaseSchema, RoadmapSchema;
190
219
  var init_roadmap = __esm({
191
220
  "src/models/roadmap.ts"() {
192
221
  "use strict";
193
222
  init_esm_shims();
194
223
  init_types();
195
- BlockerSchema = z4.object({
196
- name: z4.string().min(1),
224
+ BlockerSchema = z5.object({
225
+ name: z5.string().min(1),
197
226
  // Legacy format (pre-T-082)
198
- cleared: z4.boolean().optional(),
227
+ cleared: z5.boolean().optional(),
199
228
  // New date-based format (T-082 migration)
200
229
  createdDate: DateSchema.optional(),
201
230
  clearedDate: DateSchema.nullable().optional(),
202
231
  // Present in all current data but optional for future minimal blockers
203
- note: z4.string().nullable().optional()
232
+ note: z5.string().nullable().optional()
204
233
  }).passthrough();
205
- PhaseSchema = z4.object({
206
- id: z4.string().min(1),
207
- label: z4.string(),
208
- name: z4.string(),
209
- description: z4.string(),
210
- summary: z4.string().optional()
234
+ PhaseSchema = z5.object({
235
+ id: z5.string().min(1),
236
+ label: z5.string(),
237
+ name: z5.string(),
238
+ description: z5.string(),
239
+ summary: z5.string().optional()
211
240
  }).passthrough();
212
- RoadmapSchema = z4.object({
213
- title: z4.string(),
241
+ RoadmapSchema = z5.object({
242
+ title: z5.string(),
214
243
  date: DateSchema,
215
- phases: z4.array(PhaseSchema),
216
- blockers: z4.array(BlockerSchema)
244
+ phases: z5.array(PhaseSchema),
245
+ blockers: z5.array(BlockerSchema)
217
246
  }).passthrough();
218
247
  }
219
248
  });
220
249
 
221
250
  // src/models/config.ts
222
- import { z as z5 } from "zod";
251
+ import { z as z6 } from "zod";
223
252
  var FeaturesSchema, ConfigSchema;
224
253
  var init_config = __esm({
225
254
  "src/models/config.ts"() {
226
255
  "use strict";
227
256
  init_esm_shims();
228
- FeaturesSchema = z5.object({
229
- tickets: z5.boolean(),
230
- issues: z5.boolean(),
231
- handovers: z5.boolean(),
232
- roadmap: z5.boolean(),
233
- reviews: z5.boolean()
257
+ FeaturesSchema = z6.object({
258
+ tickets: z6.boolean(),
259
+ issues: z6.boolean(),
260
+ handovers: z6.boolean(),
261
+ roadmap: z6.boolean(),
262
+ reviews: z6.boolean()
234
263
  }).passthrough();
235
- ConfigSchema = z5.object({
236
- version: z5.number().int().min(1),
237
- schemaVersion: z5.number().int().optional(),
238
- project: z5.string().min(1),
239
- type: z5.string(),
240
- language: z5.string(),
264
+ ConfigSchema = z6.object({
265
+ version: z6.number().int().min(1),
266
+ schemaVersion: z6.number().int().optional(),
267
+ project: z6.string().min(1),
268
+ type: z6.string(),
269
+ language: z6.string(),
241
270
  features: FeaturesSchema
242
271
  }).passthrough();
243
272
  }
@@ -253,6 +282,7 @@ var init_project_state = __esm({
253
282
  // --- Public raw inputs (readonly) ---
254
283
  tickets;
255
284
  issues;
285
+ notes;
256
286
  roadmap;
257
287
  config;
258
288
  handoverFilenames;
@@ -267,15 +297,19 @@ var init_project_state = __esm({
267
297
  reverseBlocksMap;
268
298
  ticketsByID;
269
299
  issuesByID;
300
+ notesByID;
270
301
  // --- Counts ---
271
302
  totalTicketCount;
272
303
  openTicketCount;
273
304
  completeTicketCount;
274
305
  openIssueCount;
275
306
  issuesBySeverity;
307
+ activeNoteCount;
308
+ archivedNoteCount;
276
309
  constructor(input) {
277
310
  this.tickets = input.tickets;
278
311
  this.issues = input.issues;
312
+ this.notes = input.notes;
279
313
  this.roadmap = input.roadmap;
280
314
  this.config = input.config;
281
315
  this.handoverFilenames = input.handoverFilenames;
@@ -341,6 +375,11 @@ var init_project_state = __esm({
341
375
  iByID.set(i.id, i);
342
376
  }
343
377
  this.issuesByID = iByID;
378
+ const nByID = /* @__PURE__ */ new Map();
379
+ for (const n of input.notes) {
380
+ nByID.set(n.id, n);
381
+ }
382
+ this.notesByID = nByID;
344
383
  this.totalTicketCount = input.tickets.length;
345
384
  this.openTicketCount = input.tickets.filter(
346
385
  (t) => t.status !== "complete"
@@ -358,6 +397,12 @@ var init_project_state = __esm({
358
397
  }
359
398
  }
360
399
  this.issuesBySeverity = bySev;
400
+ this.activeNoteCount = input.notes.filter(
401
+ (n) => n.status === "active"
402
+ ).length;
403
+ this.archivedNoteCount = input.notes.filter(
404
+ (n) => n.status === "archived"
405
+ ).length;
361
406
  }
362
407
  // --- Query Methods ---
363
408
  isUmbrella(ticket) {
@@ -404,6 +449,9 @@ var init_project_state = __esm({
404
449
  issueByID(id) {
405
450
  return this.issuesByID.get(id);
406
451
  }
452
+ noteByID(id) {
453
+ return this.notesByID.get(id);
454
+ }
407
455
  // --- Deletion Safety ---
408
456
  /** IDs of tickets that list `ticketId` in their blockedBy. */
409
457
  ticketsBlocking(ticketId) {
@@ -523,7 +571,8 @@ import {
523
571
  stat,
524
572
  realpath,
525
573
  lstat,
526
- open
574
+ open,
575
+ mkdir
527
576
  } from "fs/promises";
528
577
  import { existsSync as existsSync3 } from "fs";
529
578
  import { join as join3, resolve as resolve2, relative as relative2, extname as extname2, dirname as dirname2, basename } from "path";
@@ -581,6 +630,12 @@ async function loadProject(root, options) {
581
630
  IssueSchema,
582
631
  warnings
583
632
  );
633
+ const notes = await loadDirectory(
634
+ join3(wrapDir, "notes"),
635
+ absRoot,
636
+ NoteSchema,
637
+ warnings
638
+ );
584
639
  const handoversDir = join3(wrapDir, "handovers");
585
640
  const handoverFilenames = await listHandovers(
586
641
  handoversDir,
@@ -601,6 +656,7 @@ async function loadProject(root, options) {
601
656
  const state = new ProjectState({
602
657
  tickets,
603
658
  issues,
659
+ notes,
604
660
  roadmap,
605
661
  config,
606
662
  handoverFilenames
@@ -727,6 +783,43 @@ async function deleteIssue(id, root) {
727
783
  await unlink(targetPath);
728
784
  });
729
785
  }
786
+ async function writeNoteUnlocked(note, root) {
787
+ const parsed = NoteSchema.parse(note);
788
+ if (!NOTE_ID_REGEX.test(parsed.id)) {
789
+ throw new ProjectLoaderError(
790
+ "invalid_input",
791
+ `Invalid note ID: ${parsed.id}`
792
+ );
793
+ }
794
+ const wrapDir = resolve2(root, ".story");
795
+ const targetPath = join3(wrapDir, "notes", `${parsed.id}.json`);
796
+ await mkdir(dirname2(targetPath), { recursive: true });
797
+ await guardPath(targetPath, wrapDir);
798
+ const json = serializeJSON(parsed);
799
+ await atomicWrite(targetPath, json);
800
+ }
801
+ async function deleteNote(id, root) {
802
+ if (!NOTE_ID_REGEX.test(id)) {
803
+ throw new ProjectLoaderError(
804
+ "invalid_input",
805
+ `Invalid note ID: ${id}`
806
+ );
807
+ }
808
+ const wrapDir = resolve2(root, ".story");
809
+ const targetPath = join3(wrapDir, "notes", `${id}.json`);
810
+ await guardPath(targetPath, wrapDir);
811
+ await withLock(wrapDir, async () => {
812
+ try {
813
+ await stat(targetPath);
814
+ } catch {
815
+ throw new ProjectLoaderError(
816
+ "not_found",
817
+ `Note file not found: notes/${id}.json`
818
+ );
819
+ }
820
+ await unlink(targetPath);
821
+ });
822
+ }
730
823
  async function withProjectLock(root, options, handler) {
731
824
  const absRoot = resolve2(root);
732
825
  const wrapDir = join3(absRoot, ".story");
@@ -884,8 +977,9 @@ async function loadProjectUnlocked(absRoot) {
884
977
  const warnings = [];
885
978
  const tickets = await loadDirectory(join3(wrapDir, "tickets"), absRoot, TicketSchema, warnings);
886
979
  const issues = await loadDirectory(join3(wrapDir, "issues"), absRoot, IssueSchema, warnings);
980
+ const notes = await loadDirectory(join3(wrapDir, "notes"), absRoot, NoteSchema, warnings);
887
981
  const handoverFilenames = await listHandovers(join3(wrapDir, "handovers"), absRoot, warnings);
888
- const state = new ProjectState({ tickets, issues, roadmap, config, handoverFilenames });
982
+ const state = new ProjectState({ tickets, issues, notes, roadmap, config, handoverFilenames });
889
983
  return { state, warnings };
890
984
  }
891
985
  async function loadSingletonFile(filename, wrapDir, root, schema) {
@@ -1076,6 +1170,7 @@ var init_project_loader = __esm({
1076
1170
  init_esm_shims();
1077
1171
  init_ticket();
1078
1172
  init_issue();
1173
+ init_note();
1079
1174
  init_roadmap();
1080
1175
  init_config();
1081
1176
  init_types();
@@ -1180,6 +1275,51 @@ async function parseHandoverFilename(raw, handoversDir) {
1180
1275
  }
1181
1276
  return raw;
1182
1277
  }
1278
+ function parseNoteId(raw) {
1279
+ const result = NoteIdSchema.safeParse(raw);
1280
+ if (!result.success) {
1281
+ throw new CliValidationError(
1282
+ "invalid_input",
1283
+ `Invalid note ID "${raw}": ${formatZodError(result.error)}`
1284
+ );
1285
+ }
1286
+ return result.data;
1287
+ }
1288
+ function normalizeTags(raw) {
1289
+ const seen = /* @__PURE__ */ new Set();
1290
+ const result = [];
1291
+ for (const item of raw) {
1292
+ if (typeof item !== "string") continue;
1293
+ const normalized = item.trim().toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "");
1294
+ if (normalized && !seen.has(normalized)) {
1295
+ seen.add(normalized);
1296
+ result.push(normalized);
1297
+ }
1298
+ }
1299
+ return result;
1300
+ }
1301
+ async function readStdinContent() {
1302
+ if (process.stdin.isTTY) {
1303
+ throw new CliValidationError(
1304
+ "invalid_input",
1305
+ "--stdin requires piped input, not a TTY"
1306
+ );
1307
+ }
1308
+ const chunks = [];
1309
+ for await (const chunk of process.stdin) {
1310
+ chunks.push(chunk);
1311
+ }
1312
+ const content = Buffer.concat(
1313
+ chunks.map((c) => Buffer.isBuffer(c) ? c : Buffer.from(c))
1314
+ ).toString("utf-8");
1315
+ if (!content.trim()) {
1316
+ throw new CliValidationError(
1317
+ "invalid_input",
1318
+ "Stdin content is empty"
1319
+ );
1320
+ }
1321
+ return content;
1322
+ }
1183
1323
  var CliValidationError;
1184
1324
  var init_helpers = __esm({
1185
1325
  "src/cli/helpers.ts"() {
@@ -1232,6 +1372,53 @@ function nextTicket(state) {
1232
1372
  }
1233
1373
  return { kind: "empty_project" };
1234
1374
  }
1375
+ function nextTickets(state, count) {
1376
+ const effectiveCount = Math.max(1, count);
1377
+ const phases = state.roadmap.phases;
1378
+ if (phases.length === 0 || state.leafTickets.length === 0) {
1379
+ return { kind: "empty_project" };
1380
+ }
1381
+ const candidates = [];
1382
+ const skippedBlockedPhases = [];
1383
+ let allPhasesComplete = true;
1384
+ for (const phase of phases) {
1385
+ if (candidates.length >= effectiveCount) break;
1386
+ const leaves = state.phaseTickets(phase.id);
1387
+ if (leaves.length === 0) continue;
1388
+ const status = state.phaseStatus(phase.id);
1389
+ if (status === "complete") continue;
1390
+ allPhasesComplete = false;
1391
+ const incompleteLeaves = leaves.filter((t) => t.status !== "complete");
1392
+ const unblocked = incompleteLeaves.filter((t) => !state.isBlocked(t));
1393
+ if (unblocked.length === 0) {
1394
+ skippedBlockedPhases.push({
1395
+ phaseId: phase.id,
1396
+ blockedCount: incompleteLeaves.length
1397
+ });
1398
+ continue;
1399
+ }
1400
+ const remaining = effectiveCount - candidates.length;
1401
+ for (const ticket of unblocked.slice(0, remaining)) {
1402
+ const impact = ticketsUnblockedBy(ticket.id, state);
1403
+ const progress = ticket.parentTicket ? umbrellaProgress(ticket.parentTicket, state) : null;
1404
+ candidates.push({
1405
+ ticket,
1406
+ unblockImpact: { ticketId: ticket.id, wouldUnblock: impact },
1407
+ umbrellaProgress: progress
1408
+ });
1409
+ }
1410
+ }
1411
+ if (candidates.length > 0) {
1412
+ return { kind: "found", candidates, skippedBlockedPhases };
1413
+ }
1414
+ if (skippedBlockedPhases.length > 0) {
1415
+ return { kind: "all_blocked", phases: skippedBlockedPhases };
1416
+ }
1417
+ if (allPhasesComplete) {
1418
+ return { kind: "all_complete" };
1419
+ }
1420
+ return { kind: "empty_project" };
1421
+ }
1235
1422
  function blockedTickets(state) {
1236
1423
  return state.leafTickets.filter(
1237
1424
  (t) => t.status !== "complete" && state.isBlocked(t)
@@ -1279,6 +1466,9 @@ function isBlockerCleared(blocker) {
1279
1466
  if (blocker.clearedDate != null) return true;
1280
1467
  return false;
1281
1468
  }
1469
+ function descendantLeaves(ticketId, state) {
1470
+ return collectDescendantLeaves(ticketId, state, /* @__PURE__ */ new Set());
1471
+ }
1282
1472
  function collectDescendantLeaves(ticketId, state, visited) {
1283
1473
  if (visited.has(ticketId)) return [];
1284
1474
  visited.add(ticketId);
@@ -1318,9 +1508,17 @@ __export(output_formatter_exports, {
1318
1508
  formatIssue: () => formatIssue,
1319
1509
  formatIssueList: () => formatIssueList,
1320
1510
  formatNextTicketOutcome: () => formatNextTicketOutcome,
1511
+ formatNextTicketsOutcome: () => formatNextTicketsOutcome,
1512
+ formatNote: () => formatNote,
1513
+ formatNoteCreateResult: () => formatNoteCreateResult,
1514
+ formatNoteDeleteResult: () => formatNoteDeleteResult,
1515
+ formatNoteList: () => formatNoteList,
1516
+ formatNoteUpdateResult: () => formatNoteUpdateResult,
1321
1517
  formatPhaseList: () => formatPhaseList,
1322
1518
  formatPhaseTickets: () => formatPhaseTickets,
1323
1519
  formatRecap: () => formatRecap,
1520
+ formatRecommendations: () => formatRecommendations,
1521
+ formatReference: () => formatReference,
1324
1522
  formatSnapshotResult: () => formatSnapshotResult,
1325
1523
  formatStatus: () => formatStatus,
1326
1524
  formatTicket: () => formatTicket,
@@ -1372,6 +1570,8 @@ function formatStatus(state, format) {
1372
1570
  openTickets: state.leafTicketCount - state.completeLeafTicketCount,
1373
1571
  blockedTickets: state.blockedCount,
1374
1572
  openIssues: state.openIssueCount,
1573
+ activeNotes: state.activeNoteCount,
1574
+ archivedNotes: state.archivedNoteCount,
1375
1575
  handovers: state.handoverFilenames.length,
1376
1576
  phases: phases.map((p) => ({
1377
1577
  id: p.phase.id,
@@ -1388,6 +1588,7 @@ function formatStatus(state, format) {
1388
1588
  "",
1389
1589
  `Tickets: ${state.completeLeafTicketCount}/${state.leafTicketCount} complete, ${state.blockedCount} blocked`,
1390
1590
  `Issues: ${state.openIssueCount} open`,
1591
+ `Notes: ${state.activeNoteCount} active, ${state.archivedNoteCount} archived`,
1391
1592
  `Handovers: ${state.handoverFilenames.length}`,
1392
1593
  "",
1393
1594
  "## Phases",
@@ -1484,6 +1685,52 @@ function formatNextTicketOutcome(outcome, state, format) {
1484
1685
  }
1485
1686
  }
1486
1687
  }
1688
+ function formatNextTicketsOutcome(outcome, state, format) {
1689
+ if (format === "json") {
1690
+ return JSON.stringify(successEnvelope(outcome), null, 2);
1691
+ }
1692
+ switch (outcome.kind) {
1693
+ case "empty_project":
1694
+ return "No phased tickets found.";
1695
+ case "all_complete":
1696
+ return "All phases complete.";
1697
+ case "all_blocked": {
1698
+ const details = outcome.phases.map((p) => `${escapeMarkdownInline(p.phaseId)} (${p.blockedCount} blocked)`).join(", ");
1699
+ return `All incomplete tickets are blocked across ${outcome.phases.length} phase${outcome.phases.length === 1 ? "" : "s"}: ${details}`;
1700
+ }
1701
+ case "found": {
1702
+ const { candidates, skippedBlockedPhases } = outcome;
1703
+ const lines = [];
1704
+ for (let i = 0; i < candidates.length; i++) {
1705
+ const c = candidates[i];
1706
+ const t = c.ticket;
1707
+ if (i > 0) lines.push("", "---", "");
1708
+ if (candidates.length === 1) {
1709
+ lines.push(`# Next: ${escapeMarkdownInline(t.id)} \u2014 ${escapeMarkdownInline(t.title)}`);
1710
+ } else {
1711
+ lines.push(`# ${i + 1}. ${escapeMarkdownInline(t.id)} \u2014 ${escapeMarkdownInline(t.title)}`);
1712
+ }
1713
+ lines.push("", `Phase: ${t.phase ?? "none"} | Order: ${t.order} | Type: ${t.type}`);
1714
+ if (c.unblockImpact.wouldUnblock.length > 0) {
1715
+ const ids = c.unblockImpact.wouldUnblock.map((u) => u.id).join(", ");
1716
+ lines.push(`Completing this unblocks: ${ids}`);
1717
+ }
1718
+ if (c.umbrellaProgress) {
1719
+ const p = c.umbrellaProgress;
1720
+ lines.push(`Parent progress: ${p.complete}/${p.total} complete (${p.status})`);
1721
+ }
1722
+ if (t.description) {
1723
+ lines.push("", fencedBlock(t.description));
1724
+ }
1725
+ }
1726
+ if (skippedBlockedPhases.length > 0) {
1727
+ const details = skippedBlockedPhases.map((p) => `${escapeMarkdownInline(p.phaseId)} (${p.blockedCount} blocked)`).join(", ");
1728
+ lines.push("", "---", "", `Skipped blocked phases: ${details}`);
1729
+ }
1730
+ return lines.join("\n");
1731
+ }
1732
+ }
1733
+ }
1487
1734
  function formatTicketList(tickets, format) {
1488
1735
  if (format === "json") {
1489
1736
  return JSON.stringify(successEnvelope(tickets), null, 2);
@@ -1598,6 +1845,56 @@ function formatBlockerList(roadmap, format) {
1598
1845
  }
1599
1846
  return lines.join("\n");
1600
1847
  }
1848
+ function formatNote(note, format) {
1849
+ if (format === "json") {
1850
+ return JSON.stringify(successEnvelope(note), null, 2);
1851
+ }
1852
+ const title = note.title ?? `${note.createdDate} \u2014 ${note.id}`;
1853
+ const statusBadge = note.status === "archived" ? " (archived)" : "";
1854
+ const lines = [
1855
+ `# ${escapeMarkdownInline(title)}${statusBadge}`,
1856
+ "",
1857
+ `Status: ${note.status}`
1858
+ ];
1859
+ if (note.tags.length > 0) {
1860
+ lines.push(`Tags: ${note.tags.join(", ")}`);
1861
+ }
1862
+ lines.push(`Created: ${note.createdDate} | Updated: ${note.updatedDate}`);
1863
+ lines.push("", fencedBlock(note.content));
1864
+ return lines.join("\n");
1865
+ }
1866
+ function formatNoteList(notes, format) {
1867
+ if (format === "json") {
1868
+ return JSON.stringify(successEnvelope(notes), null, 2);
1869
+ }
1870
+ if (notes.length === 0) return "No notes found.";
1871
+ const lines = [];
1872
+ for (const n of notes) {
1873
+ const title = n.title ?? n.id;
1874
+ const status = n.status === "archived" ? "[x]" : "[ ]";
1875
+ const tagInfo = n.status === "archived" ? " (archived)" : n.tags.length > 0 ? ` (${n.tags.join(", ")})` : "";
1876
+ lines.push(`${status} ${n.id}: ${escapeMarkdownInline(title)}${tagInfo}`);
1877
+ }
1878
+ return lines.join("\n");
1879
+ }
1880
+ function formatNoteCreateResult(note, format) {
1881
+ if (format === "json") {
1882
+ return JSON.stringify(successEnvelope(note), null, 2);
1883
+ }
1884
+ return `Created note ${note.id}: ${note.title ?? note.id}`;
1885
+ }
1886
+ function formatNoteUpdateResult(note, format) {
1887
+ if (format === "json") {
1888
+ return JSON.stringify(successEnvelope(note), null, 2);
1889
+ }
1890
+ return `Updated note ${note.id}: ${note.title ?? note.id}`;
1891
+ }
1892
+ function formatNoteDeleteResult(id, format) {
1893
+ if (format === "json") {
1894
+ return JSON.stringify(successEnvelope({ id, deleted: true }), null, 2);
1895
+ }
1896
+ return `Deleted note ${id}.`;
1897
+ }
1601
1898
  function formatError(code, message, format) {
1602
1899
  if (format === "json") {
1603
1900
  return JSON.stringify(errorEnvelope(code, message), null, 2);
@@ -1612,6 +1909,7 @@ function formatInitResult(result, format) {
1612
1909
  if (result.warnings.length > 0) {
1613
1910
  lines.push("", `Warning: ${result.warnings.length} corrupt file(s) found. Run \`claudestory validate\` to inspect.`);
1614
1911
  }
1912
+ lines.push("", "Tip: Run `claudestory setup-skill` to install the /story skill for Claude Code.");
1615
1913
  return lines.join("\n");
1616
1914
  }
1617
1915
  function formatHandoverList(filenames, format) {
@@ -1675,7 +1973,7 @@ function formatRecap(recap, state, format) {
1675
1973
  }
1676
1974
  }
1677
1975
  const ticketChanges = changes.tickets;
1678
- if (ticketChanges.added.length > 0 || ticketChanges.removed.length > 0 || ticketChanges.statusChanged.length > 0) {
1976
+ if (ticketChanges.added.length > 0 || ticketChanges.removed.length > 0 || ticketChanges.statusChanged.length > 0 || ticketChanges.descriptionChanged.length > 0) {
1679
1977
  lines.push("");
1680
1978
  lines.push("## Tickets");
1681
1979
  for (const t of ticketChanges.statusChanged) {
@@ -1687,9 +1985,12 @@ function formatRecap(recap, state, format) {
1687
1985
  for (const t of ticketChanges.removed) {
1688
1986
  lines.push(`- ${t.id}: ${escapeMarkdownInline(t.title)} \u2014 **removed**`);
1689
1987
  }
1988
+ for (const t of ticketChanges.descriptionChanged) {
1989
+ lines.push(`- ${t.id}: description updated`);
1990
+ }
1690
1991
  }
1691
1992
  const issueChanges = changes.issues;
1692
- if (issueChanges.added.length > 0 || issueChanges.resolved.length > 0 || issueChanges.statusChanged.length > 0) {
1993
+ if (issueChanges.added.length > 0 || issueChanges.resolved.length > 0 || issueChanges.statusChanged.length > 0 || issueChanges.impactChanged.length > 0) {
1693
1994
  lines.push("");
1694
1995
  lines.push("## Issues");
1695
1996
  for (const i of issueChanges.resolved) {
@@ -1701,6 +2002,9 @@ function formatRecap(recap, state, format) {
1701
2002
  for (const i of issueChanges.added) {
1702
2003
  lines.push(`- ${i.id}: ${escapeMarkdownInline(i.title)} \u2014 **new**`);
1703
2004
  }
2005
+ for (const i of issueChanges.impactChanged) {
2006
+ lines.push(`- ${i.id}: impact updated`);
2007
+ }
1704
2008
  }
1705
2009
  if (changes.blockers.added.length > 0 || changes.blockers.cleared.length > 0) {
1706
2010
  lines.push("");
@@ -1712,6 +2016,19 @@ function formatRecap(recap, state, format) {
1712
2016
  lines.push(`- ${escapeMarkdownInline(name)} \u2014 **new**`);
1713
2017
  }
1714
2018
  }
2019
+ if (changes.notes && (changes.notes.added.length > 0 || changes.notes.removed.length > 0 || changes.notes.updated.length > 0)) {
2020
+ lines.push("");
2021
+ lines.push("## Notes");
2022
+ for (const n of changes.notes.added) {
2023
+ lines.push(`- ${n.id}: added`);
2024
+ }
2025
+ for (const n of changes.notes.removed) {
2026
+ lines.push(`- ${n.id}: removed`);
2027
+ }
2028
+ for (const n of changes.notes.updated) {
2029
+ lines.push(`- ${n.id}: updated (${n.changedFields.join(", ")})`);
2030
+ }
2031
+ }
1715
2032
  }
1716
2033
  }
1717
2034
  const actions = recap.suggestedActions;
@@ -1850,6 +2167,12 @@ function formatFullExport(state, format) {
1850
2167
  severity: i.severity,
1851
2168
  status: i.status
1852
2169
  })),
2170
+ notes: state.notes.map((n) => ({
2171
+ id: n.id,
2172
+ title: n.title,
2173
+ status: n.status,
2174
+ tags: n.tags
2175
+ })),
1853
2176
  blockers: state.roadmap.blockers.map((b) => ({
1854
2177
  name: b.name,
1855
2178
  cleared: isBlockerCleared(b),
@@ -1865,6 +2188,7 @@ function formatFullExport(state, format) {
1865
2188
  lines.push("");
1866
2189
  lines.push(`Tickets: ${state.completeLeafTicketCount}/${state.leafTicketCount} complete`);
1867
2190
  lines.push(`Issues: ${state.openIssueCount} open`);
2191
+ lines.push(`Notes: ${state.activeNoteCount} active, ${state.archivedNoteCount} archived`);
1868
2192
  lines.push("");
1869
2193
  lines.push("## Phases");
1870
2194
  for (const p of phases) {
@@ -1891,6 +2215,16 @@ function formatFullExport(state, format) {
1891
2215
  lines.push(`- ${i.id} [${i.severity}]: ${escapeMarkdownInline(i.title)}${resolved}`);
1892
2216
  }
1893
2217
  }
2218
+ const activeNotes = state.notes.filter((n) => n.status === "active");
2219
+ if (activeNotes.length > 0) {
2220
+ lines.push("");
2221
+ lines.push("## Notes");
2222
+ for (const n of activeNotes) {
2223
+ const title = n.title ?? n.id;
2224
+ const tagInfo = n.tags.length > 0 ? ` (${n.tags.join(", ")})` : "";
2225
+ lines.push(`- ${n.id}: ${escapeMarkdownInline(title)}${tagInfo}`);
2226
+ }
2227
+ }
1894
2228
  const blockers = state.roadmap.blockers;
1895
2229
  if (blockers.length > 0) {
1896
2230
  lines.push("");
@@ -1903,7 +2237,7 @@ function formatFullExport(state, format) {
1903
2237
  return lines.join("\n");
1904
2238
  }
1905
2239
  function hasAnyChanges(diff) {
1906
- 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;
2240
+ 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;
1907
2241
  }
1908
2242
  function truncate(text, maxLen) {
1909
2243
  if (text.length <= maxLen) return text;
@@ -1914,6 +2248,79 @@ function formatTicketOneLiner(t, state) {
1914
2248
  const blocked = state.isBlocked(t) ? " [BLOCKED]" : "";
1915
2249
  return `${status} ${t.id}: ${escapeMarkdownInline(t.title)}${blocked}`;
1916
2250
  }
2251
+ function formatReference(commands, mcpTools, format) {
2252
+ if (format === "json") {
2253
+ return JSON.stringify(successEnvelope({ commands, mcpTools }), null, 2);
2254
+ }
2255
+ const lines = [];
2256
+ lines.push("# claudestory Reference");
2257
+ lines.push("");
2258
+ lines.push("## CLI Commands");
2259
+ lines.push("");
2260
+ for (const cmd of commands) {
2261
+ lines.push(`### ${cmd.name}`);
2262
+ lines.push(cmd.description);
2263
+ lines.push("");
2264
+ lines.push("```");
2265
+ lines.push(cmd.usage);
2266
+ lines.push("```");
2267
+ lines.push("");
2268
+ }
2269
+ lines.push("## MCP Tools");
2270
+ lines.push("");
2271
+ for (const tool of mcpTools) {
2272
+ const params = tool.params?.length ? ` (${tool.params.join(", ")})` : "";
2273
+ lines.push(`- **${tool.name}**${params} \u2014 ${tool.description}`);
2274
+ }
2275
+ lines.push("");
2276
+ lines.push("## Common Workflows");
2277
+ lines.push("");
2278
+ lines.push("### Session Start");
2279
+ lines.push("1. `claudestory status` \u2014 project overview");
2280
+ lines.push("2. `claudestory recap` \u2014 what changed since last snapshot");
2281
+ lines.push("3. `claudestory handover latest` \u2014 last session context");
2282
+ lines.push("4. `claudestory ticket next` \u2014 what to work on");
2283
+ lines.push("");
2284
+ lines.push("### Session End");
2285
+ lines.push("1. `claudestory snapshot` \u2014 save state for diffs");
2286
+ lines.push("2. `claudestory handover create --content <md>` \u2014 write session handover");
2287
+ lines.push("");
2288
+ lines.push("### Project Setup");
2289
+ lines.push("1. `npm install -g @anthropologies/claudestory` \u2014 install CLI");
2290
+ lines.push("2. `claudestory setup-skill` \u2014 install /story skill for Claude Code");
2291
+ lines.push("3. `claudestory init --name my-project` \u2014 initialize .story/ in your project");
2292
+ lines.push("");
2293
+ lines.push("## Troubleshooting");
2294
+ lines.push("");
2295
+ lines.push("- **MCP not connected:** Run `claude mcp add claudestory -s user -- claudestory --mcp`");
2296
+ lines.push("- **CLI not found:** Run `npm install -g @anthropologies/claudestory`");
2297
+ lines.push("- **Stale data:** Run `claudestory validate` to check integrity");
2298
+ lines.push("- **/story not available:** Run `claudestory setup-skill` to install the skill");
2299
+ return lines.join("\n");
2300
+ }
2301
+ function formatRecommendations(result, format) {
2302
+ if (format === "json") {
2303
+ return JSON.stringify(successEnvelope(result), null, 2);
2304
+ }
2305
+ if (result.recommendations.length === 0) {
2306
+ return "No recommendations \u2014 all work is complete or blocked.";
2307
+ }
2308
+ const lines = ["# Recommendations", ""];
2309
+ for (let i = 0; i < result.recommendations.length; i++) {
2310
+ const rec = result.recommendations[i];
2311
+ lines.push(
2312
+ `${i + 1}. **${escapeMarkdownInline(rec.id)}** (${rec.kind}) \u2014 ${escapeMarkdownInline(rec.title)}`
2313
+ );
2314
+ lines.push(` _${escapeMarkdownInline(rec.reason)}_`);
2315
+ lines.push("");
2316
+ }
2317
+ if (result.totalCandidates > result.recommendations.length) {
2318
+ lines.push(
2319
+ `Showing ${result.recommendations.length} of ${result.totalCandidates} candidates.`
2320
+ );
2321
+ }
2322
+ return lines.join("\n");
2323
+ }
1917
2324
  var ExitCode;
1918
2325
  var init_output_formatter = __esm({
1919
2326
  "src/core/output-formatter.ts"() {
@@ -1977,6 +2384,20 @@ function validateProject(state) {
1977
2384
  });
1978
2385
  }
1979
2386
  }
2387
+ const noteIDCounts = /* @__PURE__ */ new Map();
2388
+ for (const n of state.notes) {
2389
+ noteIDCounts.set(n.id, (noteIDCounts.get(n.id) ?? 0) + 1);
2390
+ }
2391
+ for (const [id, count] of noteIDCounts) {
2392
+ if (count > 1) {
2393
+ findings.push({
2394
+ level: "error",
2395
+ code: "duplicate_note_id",
2396
+ message: `Duplicate note ID: ${id} appears ${count} times.`,
2397
+ entity: id
2398
+ });
2399
+ }
2400
+ }
1980
2401
  const phaseIDCounts = /* @__PURE__ */ new Map();
1981
2402
  for (const p of state.roadmap.phases) {
1982
2403
  phaseIDCounts.set(p.id, (phaseIDCounts.get(p.id) ?? 0) + 1);
@@ -2207,12 +2628,13 @@ var init_validate = __esm({
2207
2628
  });
2208
2629
 
2209
2630
  // src/cli/commands/handover.ts
2210
- import { mkdir } from "fs/promises";
2631
+ import { existsSync as existsSync4 } from "fs";
2632
+ import { mkdir as mkdir2 } from "fs/promises";
2211
2633
  import { join as join4, resolve as resolve4 } from "path";
2212
2634
  function handleHandoverList(ctx) {
2213
2635
  return { output: formatHandoverList(ctx.state.handoverFilenames, ctx.format) };
2214
2636
  }
2215
- async function handleHandoverLatest(ctx) {
2637
+ async function handleHandoverLatest(ctx, count = 1) {
2216
2638
  if (ctx.state.handoverFilenames.length === 0) {
2217
2639
  return {
2218
2640
  output: formatError("not_found", "No handovers found", ctx.format),
@@ -2220,25 +2642,38 @@ async function handleHandoverLatest(ctx) {
2220
2642
  errorCode: "not_found"
2221
2643
  };
2222
2644
  }
2223
- const filename = ctx.state.handoverFilenames[0];
2224
- await parseHandoverFilename(filename, ctx.handoversDir);
2225
- try {
2226
- const content = await readHandover(ctx.handoversDir, filename);
2227
- return { output: formatHandoverContent(filename, content, ctx.format) };
2228
- } catch (err) {
2229
- if (err.code === "ENOENT") {
2645
+ const filenames = ctx.state.handoverFilenames.slice(0, count);
2646
+ const parts = [];
2647
+ for (const filename of filenames) {
2648
+ await parseHandoverFilename(filename, ctx.handoversDir);
2649
+ try {
2650
+ const content = await readHandover(ctx.handoversDir, filename);
2651
+ parts.push(formatHandoverContent(filename, content, ctx.format));
2652
+ } catch (err) {
2653
+ if (err.code === "ENOENT") {
2654
+ if (count > 1) continue;
2655
+ return {
2656
+ output: formatError("not_found", `Handover file not found: ${filename}`, ctx.format),
2657
+ exitCode: ExitCode.USER_ERROR,
2658
+ errorCode: "not_found"
2659
+ };
2660
+ }
2230
2661
  return {
2231
- output: formatError("not_found", `Handover file not found: ${filename}`, ctx.format),
2662
+ output: formatError("io_error", `Cannot read handover: ${err.message}`, ctx.format),
2232
2663
  exitCode: ExitCode.USER_ERROR,
2233
- errorCode: "not_found"
2664
+ errorCode: "io_error"
2234
2665
  };
2235
2666
  }
2667
+ }
2668
+ if (parts.length === 0) {
2236
2669
  return {
2237
- output: formatError("io_error", `Cannot read handover: ${err.message}`, ctx.format),
2670
+ output: formatError("not_found", "No handovers found", ctx.format),
2238
2671
  exitCode: ExitCode.USER_ERROR,
2239
- errorCode: "io_error"
2672
+ errorCode: "not_found"
2240
2673
  };
2241
2674
  }
2675
+ const separator = ctx.format === "json" ? "\n" : "\n\n---\n\n";
2676
+ return { output: parts.join(separator) };
2242
2677
  }
2243
2678
  async function handleHandoverGet(filename, ctx) {
2244
2679
  await parseHandoverFilename(filename, ctx.handoversDir);
@@ -2281,7 +2716,7 @@ async function handleHandoverCreate(content, slugRaw, format, root) {
2281
2716
  await withProjectLock(root, { strict: false }, async () => {
2282
2717
  const absRoot = resolve4(root);
2283
2718
  const handoversDir = join4(absRoot, ".story", "handovers");
2284
- await mkdir(handoversDir, { recursive: true });
2719
+ await mkdir2(handoversDir, { recursive: true });
2285
2720
  const wrapDir = join4(absRoot, ".story");
2286
2721
  const datePrefix = `${date}-`;
2287
2722
  const seqRegex = new RegExp(`^${date}-(\\d{2})-`);
@@ -2297,15 +2732,26 @@ async function handleHandoverCreate(content, slugRaw, format, root) {
2297
2732
  }
2298
2733
  } catch {
2299
2734
  }
2300
- const nextSeq = maxSeq + 1;
2735
+ let nextSeq = maxSeq + 1;
2301
2736
  if (nextSeq > 99) {
2302
2737
  throw new CliValidationError(
2303
2738
  "conflict",
2304
2739
  `Too many handovers for ${date}; limit is 99 per day`
2305
2740
  );
2306
2741
  }
2307
- const candidate = `${date}-${String(nextSeq).padStart(2, "0")}-${slug}.md`;
2308
- const candidatePath = join4(handoversDir, candidate);
2742
+ let candidate = `${date}-${String(nextSeq).padStart(2, "0")}-${slug}.md`;
2743
+ let candidatePath = join4(handoversDir, candidate);
2744
+ while (existsSync4(candidatePath)) {
2745
+ nextSeq++;
2746
+ if (nextSeq > 99) {
2747
+ throw new CliValidationError(
2748
+ "conflict",
2749
+ `Too many handovers for ${date}; limit is 99 per day`
2750
+ );
2751
+ }
2752
+ candidate = `${date}-${String(nextSeq).padStart(2, "0")}-${slug}.md`;
2753
+ candidatePath = join4(handoversDir, candidate);
2754
+ }
2309
2755
  await parseHandoverFilename(candidate, handoversDir);
2310
2756
  await guardPath(candidatePath, wrapDir);
2311
2757
  await atomicWrite(candidatePath, content);
@@ -2421,12 +2867,24 @@ function nextIssueID(issues) {
2421
2867
  }
2422
2868
  return `ISS-${String(max + 1).padStart(3, "0")}`;
2423
2869
  }
2870
+ function nextNoteID(notes) {
2871
+ let max = 0;
2872
+ for (const n of notes) {
2873
+ if (!NOTE_ID_REGEX.test(n.id)) continue;
2874
+ const match = n.id.match(NOTE_NUMERIC_REGEX);
2875
+ if (match?.[1]) {
2876
+ const num = parseInt(match[1], 10);
2877
+ if (num > max) max = num;
2878
+ }
2879
+ }
2880
+ return `N-${String(max + 1).padStart(3, "0")}`;
2881
+ }
2424
2882
  function nextOrder(phaseId, state) {
2425
2883
  const tickets = state.phaseTickets(phaseId);
2426
2884
  if (tickets.length === 0) return 10;
2427
2885
  return tickets[tickets.length - 1].order + 10;
2428
2886
  }
2429
- var TICKET_NUMERIC_REGEX, ISSUE_NUMERIC_REGEX;
2887
+ var TICKET_NUMERIC_REGEX, ISSUE_NUMERIC_REGEX, NOTE_NUMERIC_REGEX;
2430
2888
  var init_id_allocation = __esm({
2431
2889
  "src/core/id-allocation.ts"() {
2432
2890
  "use strict";
@@ -2434,6 +2892,7 @@ var init_id_allocation = __esm({
2434
2892
  init_types();
2435
2893
  TICKET_NUMERIC_REGEX = /^T-(\d+)[a-z]?$/;
2436
2894
  ISSUE_NUMERIC_REGEX = /^ISS-(\d+)$/;
2895
+ NOTE_NUMERIC_REGEX = /^N-(\d+)$/;
2437
2896
  }
2438
2897
  });
2439
2898
 
@@ -2474,13 +2933,15 @@ function handleTicketGet(id, ctx) {
2474
2933
  }
2475
2934
  return { output: formatTicket(ticket, ctx.state, ctx.format) };
2476
2935
  }
2477
- function handleTicketNext(ctx) {
2478
- const outcome = nextTicket(ctx.state);
2936
+ function handleTicketNext(ctx, count = 1) {
2937
+ if (count <= 1) {
2938
+ const outcome2 = nextTicket(ctx.state);
2939
+ const exitCode2 = outcome2.kind === "found" ? ExitCode.OK : ExitCode.USER_ERROR;
2940
+ return { output: formatNextTicketOutcome(outcome2, ctx.state, ctx.format), exitCode: exitCode2 };
2941
+ }
2942
+ const outcome = nextTickets(ctx.state, count);
2479
2943
  const exitCode = outcome.kind === "found" ? ExitCode.OK : ExitCode.USER_ERROR;
2480
- return {
2481
- output: formatNextTicketOutcome(outcome, ctx.state, ctx.format),
2482
- exitCode
2483
- };
2944
+ return { output: formatNextTicketsOutcome(outcome, ctx.state, ctx.format), exitCode };
2484
2945
  }
2485
2946
  function handleTicketBlocked(ctx) {
2486
2947
  const blocked = blockedTickets(ctx.state);
@@ -2525,6 +2986,7 @@ function validatePostWriteState(candidate, state, isCreate) {
2525
2986
  const postState = new ProjectState({
2526
2987
  tickets: existingTickets,
2527
2988
  issues: [...state.issues],
2989
+ notes: [...state.notes],
2528
2990
  roadmap: state.roadmap,
2529
2991
  config: state.config,
2530
2992
  handoverFilenames: [...state.handoverFilenames]
@@ -2673,6 +3135,9 @@ function handleIssueList(filters, ctx) {
2673
3135
  }
2674
3136
  issues = issues.filter((i) => i.severity === filters.severity);
2675
3137
  }
3138
+ if (filters.component) {
3139
+ issues = issues.filter((i) => i.components.includes(filters.component));
3140
+ }
2676
3141
  return { output: formatIssueList(issues, ctx.format) };
2677
3142
  }
2678
3143
  function handleIssueGet(id, ctx) {
@@ -2705,6 +3170,7 @@ function validatePostWriteIssueState(candidate, state, isCreate) {
2705
3170
  const postState = new ProjectState({
2706
3171
  tickets: [...state.tickets],
2707
3172
  issues: existingIssues,
3173
+ notes: [...state.notes],
2708
3174
  roadmap: state.roadmap,
2709
3175
  config: state.config,
2710
3176
  handoverFilenames: [...state.handoverFilenames]
@@ -2740,7 +3206,8 @@ async function handleIssueCreate(args, format, root) {
2740
3206
  location: args.location,
2741
3207
  discoveredDate: todayISO(),
2742
3208
  resolvedDate: null,
2743
- relatedTickets: args.relatedTickets
3209
+ relatedTickets: args.relatedTickets,
3210
+ phase: args.phase ?? null
2744
3211
  };
2745
3212
  validatePostWriteIssueState(issue, state, true);
2746
3213
  await writeIssueUnlocked(issue, root);
@@ -2822,14 +3289,14 @@ var init_issue2 = __esm({
2822
3289
  });
2823
3290
 
2824
3291
  // src/core/snapshot.ts
2825
- import { readdir as readdir3, readFile as readFile3, mkdir as mkdir2, unlink as unlink2 } from "fs/promises";
2826
- import { existsSync as existsSync4 } from "fs";
3292
+ import { readdir as readdir3, readFile as readFile3, mkdir as mkdir3, unlink as unlink2 } from "fs/promises";
3293
+ import { existsSync as existsSync5 } from "fs";
2827
3294
  import { join as join5, resolve as resolve5 } from "path";
2828
- import { z as z6 } from "zod";
3295
+ import { z as z7 } from "zod";
2829
3296
  async function saveSnapshot(root, loadResult) {
2830
3297
  const absRoot = resolve5(root);
2831
3298
  const snapshotsDir = join5(absRoot, ".story", "snapshots");
2832
- await mkdir2(snapshotsDir, { recursive: true });
3299
+ await mkdir3(snapshotsDir, { recursive: true });
2833
3300
  const { state, warnings } = loadResult;
2834
3301
  const now = /* @__PURE__ */ new Date();
2835
3302
  const filename = formatSnapshotFilename(now);
@@ -2841,6 +3308,7 @@ async function saveSnapshot(root, loadResult) {
2841
3308
  roadmap: state.roadmap,
2842
3309
  tickets: [...state.tickets],
2843
3310
  issues: [...state.issues],
3311
+ notes: [...state.notes],
2844
3312
  ...warnings.length > 0 ? {
2845
3313
  warnings: warnings.map((w) => ({
2846
3314
  type: w.type,
@@ -2860,7 +3328,7 @@ async function saveSnapshot(root, loadResult) {
2860
3328
  }
2861
3329
  async function loadLatestSnapshot(root) {
2862
3330
  const snapshotsDir = join5(resolve5(root), ".story", "snapshots");
2863
- if (!existsSync4(snapshotsDir)) return null;
3331
+ if (!existsSync5(snapshotsDir)) return null;
2864
3332
  const files = await listSnapshotFiles(snapshotsDir);
2865
3333
  if (files.length === 0) return null;
2866
3334
  for (const filename of files) {
@@ -2881,12 +3349,18 @@ function diffStates(snapshotState, currentState) {
2881
3349
  const ticketsAdded = [];
2882
3350
  const ticketsRemoved = [];
2883
3351
  const ticketsStatusChanged = [];
3352
+ const ticketsDescriptionChanged = [];
2884
3353
  for (const [id, cur] of curTickets) {
2885
3354
  const snap = snapTickets.get(id);
2886
3355
  if (!snap) {
2887
3356
  ticketsAdded.push({ id, title: cur.title });
2888
- } else if (snap.status !== cur.status) {
2889
- ticketsStatusChanged.push({ id, title: cur.title, from: snap.status, to: cur.status });
3357
+ } else {
3358
+ if (snap.status !== cur.status) {
3359
+ ticketsStatusChanged.push({ id, title: cur.title, from: snap.status, to: cur.status });
3360
+ }
3361
+ if (snap.description !== cur.description) {
3362
+ ticketsDescriptionChanged.push({ id, title: cur.title });
3363
+ }
2890
3364
  }
2891
3365
  }
2892
3366
  for (const [id, snap] of snapTickets) {
@@ -2899,15 +3373,21 @@ function diffStates(snapshotState, currentState) {
2899
3373
  const issuesAdded = [];
2900
3374
  const issuesResolved = [];
2901
3375
  const issuesStatusChanged = [];
3376
+ const issuesImpactChanged = [];
2902
3377
  for (const [id, cur] of curIssues) {
2903
3378
  const snap = snapIssues.get(id);
2904
3379
  if (!snap) {
2905
3380
  issuesAdded.push({ id, title: cur.title });
2906
- } else if (snap.status !== cur.status) {
2907
- if (cur.status === "resolved") {
2908
- issuesResolved.push({ id, title: cur.title });
2909
- } else {
2910
- issuesStatusChanged.push({ id, title: cur.title, from: snap.status, to: cur.status });
3381
+ } else {
3382
+ if (snap.status !== cur.status) {
3383
+ if (cur.status === "resolved") {
3384
+ issuesResolved.push({ id, title: cur.title });
3385
+ } else {
3386
+ issuesStatusChanged.push({ id, title: cur.title, from: snap.status, to: cur.status });
3387
+ }
3388
+ }
3389
+ if (snap.impact !== cur.impact) {
3390
+ issuesImpactChanged.push({ id, title: cur.title });
2911
3391
  }
2912
3392
  }
2913
3393
  }
@@ -2956,11 +3436,37 @@ function diffStates(snapshotState, currentState) {
2956
3436
  phasesRemoved.push({ id, name: snapPhase.name });
2957
3437
  }
2958
3438
  }
3439
+ const snapNotes = new Map(snapshotState.notes.map((n) => [n.id, n]));
3440
+ const curNotes = new Map(currentState.notes.map((n) => [n.id, n]));
3441
+ const notesAdded = [];
3442
+ const notesRemoved = [];
3443
+ const notesUpdated = [];
3444
+ for (const [id, cur] of curNotes) {
3445
+ const snap = snapNotes.get(id);
3446
+ if (!snap) {
3447
+ notesAdded.push({ id, title: cur.title });
3448
+ } else {
3449
+ const changedFields = [];
3450
+ if (snap.title !== cur.title) changedFields.push("title");
3451
+ if (snap.content !== cur.content) changedFields.push("content");
3452
+ if (JSON.stringify([...snap.tags].sort()) !== JSON.stringify([...cur.tags].sort())) changedFields.push("tags");
3453
+ if (snap.status !== cur.status) changedFields.push("status");
3454
+ if (changedFields.length > 0) {
3455
+ notesUpdated.push({ id, title: cur.title, changedFields });
3456
+ }
3457
+ }
3458
+ }
3459
+ for (const [id, snap] of snapNotes) {
3460
+ if (!curNotes.has(id)) {
3461
+ notesRemoved.push({ id, title: snap.title });
3462
+ }
3463
+ }
2959
3464
  return {
2960
- tickets: { added: ticketsAdded, removed: ticketsRemoved, statusChanged: ticketsStatusChanged },
2961
- issues: { added: issuesAdded, resolved: issuesResolved, statusChanged: issuesStatusChanged },
3465
+ tickets: { added: ticketsAdded, removed: ticketsRemoved, statusChanged: ticketsStatusChanged, descriptionChanged: ticketsDescriptionChanged },
3466
+ issues: { added: issuesAdded, resolved: issuesResolved, statusChanged: issuesStatusChanged, impactChanged: issuesImpactChanged },
2962
3467
  blockers: { added: blockersAdded, cleared: blockersCleared },
2963
- phases: { added: phasesAdded, removed: phasesRemoved, statusChanged: phasesStatusChanged }
3468
+ phases: { added: phasesAdded, removed: phasesRemoved, statusChanged: phasesStatusChanged },
3469
+ notes: { added: notesAdded, removed: notesRemoved, updated: notesUpdated }
2964
3470
  };
2965
3471
  }
2966
3472
  function buildRecap(currentState, snapshotInfo) {
@@ -2985,6 +3491,7 @@ function buildRecap(currentState, snapshotInfo) {
2985
3491
  const snapshotState = new ProjectState({
2986
3492
  tickets: snapshot.tickets,
2987
3493
  issues: snapshot.issues,
3494
+ notes: snapshot.notes ?? [],
2988
3495
  roadmap: snapshot.roadmap,
2989
3496
  config: snapshot.config,
2990
3497
  handoverFilenames: []
@@ -3039,25 +3546,27 @@ var init_snapshot = __esm({
3039
3546
  init_esm_shims();
3040
3547
  init_ticket();
3041
3548
  init_issue();
3549
+ init_note();
3042
3550
  init_roadmap();
3043
3551
  init_config();
3044
3552
  init_project_state();
3045
3553
  init_queries();
3046
3554
  init_project_loader();
3047
- LoadWarningSchema = z6.object({
3048
- type: z6.string(),
3049
- file: z6.string(),
3050
- message: z6.string()
3555
+ LoadWarningSchema = z7.object({
3556
+ type: z7.string(),
3557
+ file: z7.string(),
3558
+ message: z7.string()
3051
3559
  });
3052
- SnapshotV1Schema = z6.object({
3053
- version: z6.literal(1),
3054
- createdAt: z6.string().datetime({ offset: true }),
3055
- project: z6.string(),
3560
+ SnapshotV1Schema = z7.object({
3561
+ version: z7.literal(1),
3562
+ createdAt: z7.string().datetime({ offset: true }),
3563
+ project: z7.string(),
3056
3564
  config: ConfigSchema,
3057
3565
  roadmap: RoadmapSchema,
3058
- tickets: z6.array(TicketSchema),
3059
- issues: z6.array(IssueSchema),
3060
- warnings: z6.array(LoadWarningSchema).optional()
3566
+ tickets: z7.array(TicketSchema),
3567
+ issues: z7.array(IssueSchema),
3568
+ notes: z7.array(NoteSchema).optional().default([]),
3569
+ warnings: z7.array(LoadWarningSchema).optional()
3061
3570
  });
3062
3571
  MAX_SNAPSHOTS = 20;
3063
3572
  }
@@ -3078,12 +3587,390 @@ var init_recap = __esm({
3078
3587
  }
3079
3588
  });
3080
3589
 
3081
- // src/cli/commands/snapshot.ts
3082
- async function handleSnapshot(root, format) {
3083
- let result;
3084
- await withProjectLock(root, { strict: false }, async (loadResult) => {
3085
- result = await saveSnapshot(root, loadResult);
3590
+ // src/cli/commands/note.ts
3591
+ function handleNoteList(filters, ctx) {
3592
+ let notes = [...ctx.state.notes];
3593
+ if (filters.status) {
3594
+ if (!NOTE_STATUSES.includes(filters.status)) {
3595
+ throw new CliValidationError(
3596
+ "invalid_input",
3597
+ `Unknown note status "${filters.status}": must be one of ${NOTE_STATUSES.join(", ")}`
3598
+ );
3599
+ }
3600
+ notes = notes.filter((n) => n.status === filters.status);
3601
+ }
3602
+ if (filters.tag) {
3603
+ const normalized = normalizeTags([filters.tag]);
3604
+ if (normalized.length === 0) {
3605
+ notes = [];
3606
+ } else {
3607
+ const tag = normalized[0];
3608
+ notes = notes.filter((n) => n.tags.includes(tag));
3609
+ }
3610
+ }
3611
+ notes.sort((a, b) => {
3612
+ const dateCmp = b.updatedDate.localeCompare(a.updatedDate);
3613
+ if (dateCmp !== 0) return dateCmp;
3614
+ return a.id.localeCompare(b.id);
3086
3615
  });
3616
+ return { output: formatNoteList(notes, ctx.format) };
3617
+ }
3618
+ function handleNoteGet(id, ctx) {
3619
+ const note = ctx.state.noteByID(id);
3620
+ if (!note) {
3621
+ return {
3622
+ output: formatError("not_found", `Note ${id} not found`, ctx.format),
3623
+ exitCode: ExitCode.USER_ERROR,
3624
+ errorCode: "not_found"
3625
+ };
3626
+ }
3627
+ return { output: formatNote(note, ctx.format) };
3628
+ }
3629
+ async function handleNoteCreate(args, format, root) {
3630
+ if (!args.content.trim()) {
3631
+ throw new CliValidationError("invalid_input", "Note content cannot be empty");
3632
+ }
3633
+ let createdNote;
3634
+ await withProjectLock(root, { strict: true }, async ({ state }) => {
3635
+ const id = nextNoteID(state.notes);
3636
+ const today = todayISO();
3637
+ const tags = args.tags ? normalizeTags(args.tags) : [];
3638
+ const note = {
3639
+ id,
3640
+ title: args.title && args.title.trim() !== "" ? args.title : null,
3641
+ content: args.content,
3642
+ tags,
3643
+ status: "active",
3644
+ createdDate: today,
3645
+ updatedDate: today
3646
+ };
3647
+ await writeNoteUnlocked(note, root);
3648
+ createdNote = note;
3649
+ });
3650
+ if (!createdNote) throw new Error("Note not created");
3651
+ return { output: formatNoteCreateResult(createdNote, format) };
3652
+ }
3653
+ async function handleNoteUpdate(id, updates, format, root) {
3654
+ if (updates.content !== void 0 && !updates.content.trim()) {
3655
+ throw new CliValidationError("invalid_input", "Note content cannot be empty");
3656
+ }
3657
+ if (updates.status && !NOTE_STATUSES.includes(updates.status)) {
3658
+ throw new CliValidationError(
3659
+ "invalid_input",
3660
+ `Unknown note status "${updates.status}": must be one of ${NOTE_STATUSES.join(", ")}`
3661
+ );
3662
+ }
3663
+ let updatedNote;
3664
+ await withProjectLock(root, { strict: true }, async ({ state }) => {
3665
+ const existing = state.noteByID(id);
3666
+ if (!existing) {
3667
+ throw new CliValidationError("not_found", `Note ${id} not found`);
3668
+ }
3669
+ const note = { ...existing };
3670
+ if (updates.content !== void 0) {
3671
+ note.content = updates.content;
3672
+ }
3673
+ if (updates.title !== void 0) {
3674
+ const trimmed = updates.title?.trim();
3675
+ note.title = !trimmed ? null : updates.title;
3676
+ }
3677
+ if (updates.clearTags) {
3678
+ note.tags = [];
3679
+ } else if (updates.tags !== void 0) {
3680
+ note.tags = normalizeTags(updates.tags);
3681
+ }
3682
+ if (updates.status !== void 0) {
3683
+ note.status = updates.status;
3684
+ }
3685
+ note.updatedDate = todayISO();
3686
+ await writeNoteUnlocked(note, root);
3687
+ updatedNote = note;
3688
+ });
3689
+ if (!updatedNote) throw new Error("Note not updated");
3690
+ return { output: formatNoteUpdateResult(updatedNote, format) };
3691
+ }
3692
+ async function handleNoteDelete(id, format, root) {
3693
+ await deleteNote(id, root);
3694
+ return { output: formatNoteDeleteResult(id, format) };
3695
+ }
3696
+ var init_note2 = __esm({
3697
+ "src/cli/commands/note.ts"() {
3698
+ "use strict";
3699
+ init_esm_shims();
3700
+ init_project_loader();
3701
+ init_id_allocation();
3702
+ init_output_formatter();
3703
+ init_types();
3704
+ init_helpers();
3705
+ }
3706
+ });
3707
+
3708
+ // src/core/recommend.ts
3709
+ function recommend(state, count) {
3710
+ const effectiveCount = Math.max(1, Math.min(10, count));
3711
+ const dedup = /* @__PURE__ */ new Map();
3712
+ const phaseIndex = buildPhaseIndex(state);
3713
+ const generators = [
3714
+ () => generateValidationSuggestions(state),
3715
+ () => generateCriticalIssues(state),
3716
+ () => generateInProgressTickets(state, phaseIndex),
3717
+ () => generateHighImpactUnblocks(state),
3718
+ () => generateNearCompleteUmbrellas(state, phaseIndex),
3719
+ () => generatePhaseMomentum(state),
3720
+ () => generateQuickWins(state, phaseIndex),
3721
+ () => generateOpenIssues(state)
3722
+ ];
3723
+ for (const gen of generators) {
3724
+ for (const rec of gen()) {
3725
+ const existing = dedup.get(rec.id);
3726
+ if (!existing || rec.score > existing.score) {
3727
+ dedup.set(rec.id, rec);
3728
+ }
3729
+ }
3730
+ }
3731
+ const curPhase = currentPhase(state);
3732
+ const curPhaseIdx = curPhase ? phaseIndex.get(curPhase.id) ?? 0 : 0;
3733
+ for (const [id, rec] of dedup) {
3734
+ if (rec.kind !== "ticket") continue;
3735
+ const ticket = state.ticketByID(id);
3736
+ if (!ticket || ticket.phase == null) continue;
3737
+ const ticketPhaseIdx = phaseIndex.get(ticket.phase);
3738
+ if (ticketPhaseIdx === void 0) continue;
3739
+ const phasesAhead = ticketPhaseIdx - curPhaseIdx;
3740
+ if (phasesAhead > 0) {
3741
+ const penalty = Math.min(phasesAhead * PHASE_DISTANCE_PENALTY, MAX_PHASE_PENALTY);
3742
+ dedup.set(id, {
3743
+ ...rec,
3744
+ score: rec.score - penalty,
3745
+ reason: rec.reason + " (future phase)"
3746
+ });
3747
+ }
3748
+ }
3749
+ const all = [...dedup.values()].sort((a, b) => {
3750
+ if (b.score !== a.score) return b.score - a.score;
3751
+ const catDiff = CATEGORY_PRIORITY[a.category] - CATEGORY_PRIORITY[b.category];
3752
+ if (catDiff !== 0) return catDiff;
3753
+ return a.id.localeCompare(b.id);
3754
+ });
3755
+ return {
3756
+ recommendations: all.slice(0, effectiveCount),
3757
+ totalCandidates: all.length
3758
+ };
3759
+ }
3760
+ function generateValidationSuggestions(state) {
3761
+ const result = validateProject(state);
3762
+ if (result.errorCount === 0) return [];
3763
+ return [
3764
+ {
3765
+ id: "validate",
3766
+ kind: "action",
3767
+ title: "Run claudestory validate",
3768
+ category: "validation_errors",
3769
+ reason: `${result.errorCount} validation error${result.errorCount === 1 ? "" : "s"} \u2014 fix before other work`,
3770
+ score: 1e3
3771
+ }
3772
+ ];
3773
+ }
3774
+ function generateCriticalIssues(state) {
3775
+ const issues = state.issues.filter(
3776
+ (i) => i.status !== "resolved" && (i.severity === "critical" || i.severity === "high")
3777
+ ).sort((a, b) => {
3778
+ const sevDiff = SEVERITY_RANK[b.severity] - SEVERITY_RANK[a.severity];
3779
+ if (sevDiff !== 0) return sevDiff;
3780
+ return b.discoveredDate.localeCompare(a.discoveredDate);
3781
+ });
3782
+ return issues.map((issue, index) => ({
3783
+ id: issue.id,
3784
+ kind: "issue",
3785
+ title: issue.title,
3786
+ category: "critical_issue",
3787
+ 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`,
3788
+ score: 900 - Math.min(index, 99)
3789
+ }));
3790
+ }
3791
+ function generateInProgressTickets(state, phaseIndex) {
3792
+ const tickets = state.leafTickets.filter(
3793
+ (t) => t.status === "inprogress"
3794
+ );
3795
+ const sorted = sortByPhaseAndOrder(tickets, phaseIndex);
3796
+ return sorted.map((ticket, index) => ({
3797
+ id: ticket.id,
3798
+ kind: "ticket",
3799
+ title: ticket.title,
3800
+ category: "inprogress_ticket",
3801
+ reason: "In-progress \u2014 finish what's started",
3802
+ score: 800 - Math.min(index, 99)
3803
+ }));
3804
+ }
3805
+ function generateHighImpactUnblocks(state) {
3806
+ const candidates = [];
3807
+ for (const ticket of state.leafTickets) {
3808
+ if (ticket.status === "complete") continue;
3809
+ if (state.isBlocked(ticket)) continue;
3810
+ const wouldUnblock = ticketsUnblockedBy(ticket.id, state);
3811
+ if (wouldUnblock.length >= 2) {
3812
+ candidates.push({ ticket, unblockCount: wouldUnblock.length });
3813
+ }
3814
+ }
3815
+ candidates.sort((a, b) => b.unblockCount - a.unblockCount);
3816
+ return candidates.map(({ ticket, unblockCount }, index) => ({
3817
+ id: ticket.id,
3818
+ kind: "ticket",
3819
+ title: ticket.title,
3820
+ category: "high_impact_unblock",
3821
+ reason: `Completing this unblocks ${unblockCount} other ticket${unblockCount === 1 ? "" : "s"}`,
3822
+ score: 700 - Math.min(index, 99)
3823
+ }));
3824
+ }
3825
+ function generateNearCompleteUmbrellas(state, phaseIndex) {
3826
+ const candidates = [];
3827
+ for (const umbrellaId of state.umbrellaIDs) {
3828
+ const progress = umbrellaProgress(umbrellaId, state);
3829
+ if (!progress) continue;
3830
+ if (progress.total < 2) continue;
3831
+ if (progress.status === "complete") continue;
3832
+ const ratio = progress.complete / progress.total;
3833
+ if (ratio < 0.8) continue;
3834
+ const leaves = descendantLeaves(umbrellaId, state);
3835
+ const incomplete = leaves.filter((t) => t.status !== "complete");
3836
+ const sorted = sortByPhaseAndOrder(incomplete, phaseIndex);
3837
+ if (sorted.length === 0) continue;
3838
+ const umbrella = state.ticketByID(umbrellaId);
3839
+ candidates.push({
3840
+ umbrellaId,
3841
+ umbrellaTitle: umbrella?.title ?? umbrellaId,
3842
+ firstIncompleteLeaf: sorted[0],
3843
+ complete: progress.complete,
3844
+ total: progress.total,
3845
+ ratio
3846
+ });
3847
+ }
3848
+ candidates.sort((a, b) => b.ratio - a.ratio);
3849
+ return candidates.map((c, index) => ({
3850
+ id: c.firstIncompleteLeaf.id,
3851
+ kind: "ticket",
3852
+ title: c.firstIncompleteLeaf.title,
3853
+ category: "near_complete_umbrella",
3854
+ reason: `${c.complete}/${c.total} complete in umbrella ${c.umbrellaId} \u2014 close it out`,
3855
+ score: 600 - Math.min(index, 99)
3856
+ }));
3857
+ }
3858
+ function generatePhaseMomentum(state) {
3859
+ const outcome = nextTicket(state);
3860
+ if (outcome.kind !== "found") return [];
3861
+ const ticket = outcome.ticket;
3862
+ return [
3863
+ {
3864
+ id: ticket.id,
3865
+ kind: "ticket",
3866
+ title: ticket.title,
3867
+ category: "phase_momentum",
3868
+ reason: `Next in phase order (${ticket.phase ?? "none"})`,
3869
+ score: 500
3870
+ }
3871
+ ];
3872
+ }
3873
+ function generateQuickWins(state, phaseIndex) {
3874
+ const tickets = state.leafTickets.filter(
3875
+ (t) => t.status === "open" && t.type === "chore" && !state.isBlocked(t)
3876
+ );
3877
+ const sorted = sortByPhaseAndOrder(tickets, phaseIndex);
3878
+ return sorted.map((ticket, index) => ({
3879
+ id: ticket.id,
3880
+ kind: "ticket",
3881
+ title: ticket.title,
3882
+ category: "quick_win",
3883
+ reason: "Chore \u2014 quick win",
3884
+ score: 400 - Math.min(index, 99)
3885
+ }));
3886
+ }
3887
+ function generateOpenIssues(state) {
3888
+ const issues = state.issues.filter(
3889
+ (i) => i.status !== "resolved" && (i.severity === "medium" || i.severity === "low")
3890
+ ).sort((a, b) => {
3891
+ const sevDiff = SEVERITY_RANK[b.severity] - SEVERITY_RANK[a.severity];
3892
+ if (sevDiff !== 0) return sevDiff;
3893
+ return b.discoveredDate.localeCompare(a.discoveredDate);
3894
+ });
3895
+ return issues.map((issue, index) => ({
3896
+ id: issue.id,
3897
+ kind: "issue",
3898
+ title: issue.title,
3899
+ category: "open_issue",
3900
+ reason: issue.status === "inprogress" ? `${capitalize(issue.severity)} severity issue \u2014 in-progress` : `${capitalize(issue.severity)} severity issue`,
3901
+ score: 300 - Math.min(index, 99)
3902
+ }));
3903
+ }
3904
+ function capitalize(s) {
3905
+ return s.charAt(0).toUpperCase() + s.slice(1);
3906
+ }
3907
+ function buildPhaseIndex(state) {
3908
+ const index = /* @__PURE__ */ new Map();
3909
+ state.roadmap.phases.forEach((p, i) => index.set(p.id, i));
3910
+ return index;
3911
+ }
3912
+ function sortByPhaseAndOrder(tickets, phaseIndex) {
3913
+ return [...tickets].sort((a, b) => {
3914
+ const aPhase = (a.phase != null ? phaseIndex.get(a.phase) : void 0) ?? Number.MAX_SAFE_INTEGER;
3915
+ const bPhase = (b.phase != null ? phaseIndex.get(b.phase) : void 0) ?? Number.MAX_SAFE_INTEGER;
3916
+ if (aPhase !== bPhase) return aPhase - bPhase;
3917
+ return a.order - b.order;
3918
+ });
3919
+ }
3920
+ var SEVERITY_RANK, PHASE_DISTANCE_PENALTY, MAX_PHASE_PENALTY, CATEGORY_PRIORITY;
3921
+ var init_recommend = __esm({
3922
+ "src/core/recommend.ts"() {
3923
+ "use strict";
3924
+ init_esm_shims();
3925
+ init_queries();
3926
+ init_validation();
3927
+ SEVERITY_RANK = {
3928
+ critical: 4,
3929
+ high: 3,
3930
+ medium: 2,
3931
+ low: 1
3932
+ };
3933
+ PHASE_DISTANCE_PENALTY = 100;
3934
+ MAX_PHASE_PENALTY = 400;
3935
+ CATEGORY_PRIORITY = {
3936
+ validation_errors: 1,
3937
+ critical_issue: 2,
3938
+ inprogress_ticket: 3,
3939
+ high_impact_unblock: 4,
3940
+ near_complete_umbrella: 5,
3941
+ phase_momentum: 6,
3942
+ quick_win: 7,
3943
+ open_issue: 8
3944
+ };
3945
+ }
3946
+ });
3947
+
3948
+ // src/cli/commands/recommend.ts
3949
+ function handleRecommend(ctx, count) {
3950
+ const result = recommend(ctx.state, count);
3951
+ return { output: formatRecommendations(result, ctx.format) };
3952
+ }
3953
+ var init_recommend2 = __esm({
3954
+ "src/cli/commands/recommend.ts"() {
3955
+ "use strict";
3956
+ init_esm_shims();
3957
+ init_recommend();
3958
+ init_output_formatter();
3959
+ }
3960
+ });
3961
+
3962
+ // src/cli/commands/snapshot.ts
3963
+ async function handleSnapshot(root, format, options) {
3964
+ let result;
3965
+ await withProjectLock(root, { strict: false }, async (loadResult) => {
3966
+ result = await saveSnapshot(root, loadResult);
3967
+ });
3968
+ if (!result) {
3969
+ throw new Error("snapshot: withProjectLock completed without setting result");
3970
+ }
3971
+ if (options?.quiet) {
3972
+ return { output: "" };
3973
+ }
3087
3974
  return { output: formatSnapshotResult(result, format) };
3088
3975
  }
3089
3976
  var init_snapshot2 = __esm({
@@ -3354,7 +4241,7 @@ var init_phase = __esm({
3354
4241
  });
3355
4242
 
3356
4243
  // src/mcp/tools.ts
3357
- import { z as z7 } from "zod";
4244
+ import { z as z8 } from "zod";
3358
4245
  import { join as join7 } from "path";
3359
4246
  function formatMcpError(code, message) {
3360
4247
  return `[${code}] ${message}`;
@@ -3424,8 +4311,14 @@ function registerAllTools(server, pinnedRoot) {
3424
4311
  description: "First non-complete phase with its description"
3425
4312
  }, () => runMcpReadTool(pinnedRoot, handlePhaseCurrent));
3426
4313
  server.registerTool("claudestory_ticket_next", {
3427
- description: "Highest-priority unblocked ticket with unblock impact and umbrella progress"
3428
- }, () => runMcpReadTool(pinnedRoot, handleTicketNext));
4314
+ description: "Highest-priority unblocked ticket(s) with unblock impact and umbrella progress",
4315
+ inputSchema: {
4316
+ count: z8.number().int().min(1).max(10).optional().describe("Number of candidates to return (default: 1)")
4317
+ }
4318
+ }, (args) => runMcpReadTool(
4319
+ pinnedRoot,
4320
+ (ctx) => handleTicketNext(ctx, args.count ?? 1)
4321
+ ));
3429
4322
  server.registerTool("claudestory_ticket_blocked", {
3430
4323
  description: "All blocked tickets with their blocking dependencies"
3431
4324
  }, () => runMcpReadTool(pinnedRoot, handleTicketBlocked));
@@ -3433,8 +4326,14 @@ function registerAllTools(server, pinnedRoot) {
3433
4326
  description: "List handover filenames (newest first)"
3434
4327
  }, () => runMcpReadTool(pinnedRoot, handleHandoverList));
3435
4328
  server.registerTool("claudestory_handover_latest", {
3436
- description: "Content of the most recent handover document"
3437
- }, () => runMcpReadTool(pinnedRoot, handleHandoverLatest));
4329
+ description: "Content of the most recent handover document(s)",
4330
+ inputSchema: {
4331
+ count: z8.number().int().min(1).max(10).optional().describe("Number of recent handovers to return (default: 1)")
4332
+ }
4333
+ }, (args) => runMcpReadTool(
4334
+ pinnedRoot,
4335
+ (ctx) => handleHandoverLatest(ctx, args.count ?? 1)
4336
+ ));
3438
4337
  server.registerTool("claudestory_blocker_list", {
3439
4338
  description: "All roadmap blockers with dates and status"
3440
4339
  }, () => runMcpReadTool(pinnedRoot, handleBlockerList));
@@ -3444,7 +4343,7 @@ function registerAllTools(server, pinnedRoot) {
3444
4343
  server.registerTool("claudestory_phase_tickets", {
3445
4344
  description: "Leaf tickets for a specific phase, sorted by order",
3446
4345
  inputSchema: {
3447
- phaseId: z7.string().describe("Phase ID (e.g. p5b, dogfood)")
4346
+ phaseId: z8.string().describe("Phase ID (e.g. p5b, dogfood)")
3448
4347
  }
3449
4348
  }, (args) => runMcpReadTool(pinnedRoot, (ctx) => {
3450
4349
  const phaseExists = ctx.state.roadmap.phases.some((p) => p.id === args.phaseId);
@@ -3460,9 +4359,9 @@ function registerAllTools(server, pinnedRoot) {
3460
4359
  server.registerTool("claudestory_ticket_list", {
3461
4360
  description: "List leaf tickets with optional filters",
3462
4361
  inputSchema: {
3463
- status: z7.enum(TICKET_STATUSES).optional().describe("Filter by status: open, inprogress, complete"),
3464
- phase: z7.string().optional().describe("Filter by phase ID"),
3465
- type: z7.enum(TICKET_TYPES).optional().describe("Filter by type: task, feature, chore")
4362
+ status: z8.enum(TICKET_STATUSES).optional().describe("Filter by status: open, inprogress, complete"),
4363
+ phase: z8.string().optional().describe("Filter by phase ID"),
4364
+ type: z8.enum(TICKET_TYPES).optional().describe("Filter by type: task, feature, chore")
3466
4365
  }
3467
4366
  }, (args) => runMcpReadTool(pinnedRoot, (ctx) => {
3468
4367
  if (args.phase) {
@@ -3483,42 +4382,52 @@ function registerAllTools(server, pinnedRoot) {
3483
4382
  server.registerTool("claudestory_ticket_get", {
3484
4383
  description: "Get a ticket by ID (includes umbrella tickets)",
3485
4384
  inputSchema: {
3486
- id: z7.string().regex(TICKET_ID_REGEX).describe("Ticket ID (e.g. T-001, T-079b)")
4385
+ id: z8.string().regex(TICKET_ID_REGEX).describe("Ticket ID (e.g. T-001, T-079b)")
3487
4386
  }
3488
4387
  }, (args) => runMcpReadTool(pinnedRoot, (ctx) => handleTicketGet(args.id, ctx)));
3489
4388
  server.registerTool("claudestory_issue_list", {
3490
4389
  description: "List issues with optional filters",
3491
4390
  inputSchema: {
3492
- status: z7.enum(ISSUE_STATUSES).optional().describe("Filter by status: open, inprogress, resolved"),
3493
- severity: z7.enum(ISSUE_SEVERITIES).optional().describe("Filter by severity: critical, high, medium, low")
4391
+ status: z8.enum(ISSUE_STATUSES).optional().describe("Filter by status: open, inprogress, resolved"),
4392
+ severity: z8.enum(ISSUE_SEVERITIES).optional().describe("Filter by severity: critical, high, medium, low"),
4393
+ component: z8.string().optional().describe("Filter by component name")
3494
4394
  }
3495
4395
  }, (args) => runMcpReadTool(
3496
4396
  pinnedRoot,
3497
- (ctx) => handleIssueList({ status: args.status, severity: args.severity }, ctx)
4397
+ (ctx) => handleIssueList({ status: args.status, severity: args.severity, component: args.component }, ctx)
3498
4398
  ));
3499
4399
  server.registerTool("claudestory_issue_get", {
3500
4400
  description: "Get an issue by ID",
3501
4401
  inputSchema: {
3502
- id: z7.string().regex(ISSUE_ID_REGEX).describe("Issue ID (e.g. ISS-001)")
4402
+ id: z8.string().regex(ISSUE_ID_REGEX).describe("Issue ID (e.g. ISS-001)")
3503
4403
  }
3504
4404
  }, (args) => runMcpReadTool(pinnedRoot, (ctx) => handleIssueGet(args.id, ctx)));
3505
4405
  server.registerTool("claudestory_handover_get", {
3506
4406
  description: "Content of a specific handover document by filename",
3507
4407
  inputSchema: {
3508
- filename: z7.string().describe("Handover filename (e.g. 2026-03-20-session.md)")
4408
+ filename: z8.string().describe("Handover filename (e.g. 2026-03-20-session.md)")
3509
4409
  }
3510
4410
  }, (args) => runMcpReadTool(pinnedRoot, (ctx) => handleHandoverGet(args.filename, ctx)));
3511
4411
  server.registerTool("claudestory_recap", {
3512
4412
  description: "Session diff \u2014 changes since last snapshot + suggested next actions. Shows what changed and what to work on."
3513
4413
  }, () => runMcpReadTool(pinnedRoot, handleRecap));
4414
+ server.registerTool("claudestory_recommend", {
4415
+ description: "Context-aware ranked work suggestions mixing tickets and issues",
4416
+ inputSchema: {
4417
+ count: z8.number().int().min(1).max(10).optional().describe("Number of recommendations (default: 5)")
4418
+ }
4419
+ }, (args) => runMcpReadTool(
4420
+ pinnedRoot,
4421
+ (ctx) => handleRecommend(ctx, args.count ?? 5)
4422
+ ));
3514
4423
  server.registerTool("claudestory_snapshot", {
3515
4424
  description: "Save current project state for session diffs. Creates a snapshot in .story/snapshots/."
3516
4425
  }, () => runMcpWriteTool(pinnedRoot, handleSnapshot));
3517
4426
  server.registerTool("claudestory_export", {
3518
4427
  description: "Self-contained project document for sharing",
3519
4428
  inputSchema: {
3520
- phase: z7.string().optional().describe("Export a single phase by ID"),
3521
- all: z7.boolean().optional().describe("Export entire project")
4429
+ phase: z8.string().optional().describe("Export a single phase by ID"),
4430
+ all: z8.boolean().optional().describe("Export entire project")
3522
4431
  }
3523
4432
  }, (args) => {
3524
4433
  if (!args.phase && !args.all) {
@@ -3540,8 +4449,8 @@ function registerAllTools(server, pinnedRoot) {
3540
4449
  server.registerTool("claudestory_handover_create", {
3541
4450
  description: "Create a handover document from markdown content",
3542
4451
  inputSchema: {
3543
- content: z7.string().describe("Markdown content of the handover"),
3544
- slug: z7.string().optional().describe("Slug for filename (e.g. phase5b-wrapup). Default: session")
4452
+ content: z8.string().describe("Markdown content of the handover"),
4453
+ slug: z8.string().optional().describe("Slug for filename (e.g. phase5b-wrapup). Default: session")
3545
4454
  }
3546
4455
  }, (args) => {
3547
4456
  if (!args.content?.trim()) {
@@ -3555,6 +4464,177 @@ function registerAllTools(server, pinnedRoot) {
3555
4464
  (root) => handleHandoverCreate(args.content, args.slug ?? "session", "md", root)
3556
4465
  );
3557
4466
  });
4467
+ server.registerTool("claudestory_ticket_create", {
4468
+ description: "Create a new ticket",
4469
+ inputSchema: {
4470
+ title: z8.string().describe("Ticket title"),
4471
+ type: z8.enum(TICKET_TYPES).describe("Ticket type: task, feature, chore"),
4472
+ phase: z8.string().optional().describe("Phase ID"),
4473
+ description: z8.string().optional().describe("Ticket description"),
4474
+ blockedBy: z8.array(z8.string().regex(TICKET_ID_REGEX)).optional().describe("IDs of blocking tickets"),
4475
+ parentTicket: z8.string().regex(TICKET_ID_REGEX).optional().describe("Parent ticket ID (makes this a sub-ticket)")
4476
+ }
4477
+ }, (args) => runMcpWriteTool(
4478
+ pinnedRoot,
4479
+ (root, format) => handleTicketCreate(
4480
+ {
4481
+ title: args.title,
4482
+ type: args.type,
4483
+ phase: args.phase ?? null,
4484
+ description: args.description ?? "",
4485
+ blockedBy: args.blockedBy ?? [],
4486
+ parentTicket: args.parentTicket ?? null
4487
+ },
4488
+ format,
4489
+ root
4490
+ )
4491
+ ));
4492
+ server.registerTool("claudestory_ticket_update", {
4493
+ description: "Update an existing ticket",
4494
+ inputSchema: {
4495
+ id: z8.string().regex(TICKET_ID_REGEX).describe("Ticket ID (e.g. T-001)"),
4496
+ status: z8.enum(TICKET_STATUSES).optional().describe("New status: open, inprogress, complete"),
4497
+ title: z8.string().optional().describe("New title"),
4498
+ order: z8.number().optional().describe("New sort order"),
4499
+ description: z8.string().optional().describe("New description"),
4500
+ phase: z8.string().nullable().optional().describe("New phase ID (null to clear)"),
4501
+ parentTicket: z8.string().regex(TICKET_ID_REGEX).nullable().optional().describe("Parent ticket ID (null to clear)"),
4502
+ blockedBy: z8.array(z8.string().regex(TICKET_ID_REGEX)).optional().describe("IDs of blocking tickets")
4503
+ }
4504
+ }, (args) => runMcpWriteTool(
4505
+ pinnedRoot,
4506
+ (root, format) => handleTicketUpdate(
4507
+ args.id,
4508
+ {
4509
+ status: args.status,
4510
+ title: args.title,
4511
+ order: args.order,
4512
+ description: args.description,
4513
+ phase: args.phase,
4514
+ parentTicket: args.parentTicket,
4515
+ blockedBy: args.blockedBy
4516
+ },
4517
+ format,
4518
+ root
4519
+ )
4520
+ ));
4521
+ server.registerTool("claudestory_issue_create", {
4522
+ description: "Create a new issue",
4523
+ inputSchema: {
4524
+ title: z8.string().describe("Issue title"),
4525
+ severity: z8.enum(ISSUE_SEVERITIES).describe("Issue severity: critical, high, medium, low"),
4526
+ impact: z8.string().describe("Impact description"),
4527
+ components: z8.array(z8.string()).optional().describe("Affected components"),
4528
+ relatedTickets: z8.array(z8.string().regex(TICKET_ID_REGEX)).optional().describe("Related ticket IDs"),
4529
+ location: z8.array(z8.string()).optional().describe("File locations"),
4530
+ phase: z8.string().optional().describe("Phase ID")
4531
+ }
4532
+ }, (args) => runMcpWriteTool(
4533
+ pinnedRoot,
4534
+ (root, format) => handleIssueCreate(
4535
+ {
4536
+ title: args.title,
4537
+ severity: args.severity,
4538
+ impact: args.impact,
4539
+ components: args.components ?? [],
4540
+ relatedTickets: args.relatedTickets ?? [],
4541
+ location: args.location ?? [],
4542
+ phase: args.phase
4543
+ },
4544
+ format,
4545
+ root
4546
+ )
4547
+ ));
4548
+ server.registerTool("claudestory_issue_update", {
4549
+ description: "Update an existing issue",
4550
+ inputSchema: {
4551
+ id: z8.string().regex(ISSUE_ID_REGEX).describe("Issue ID (e.g. ISS-001)"),
4552
+ status: z8.enum(ISSUE_STATUSES).optional().describe("New status: open, inprogress, resolved"),
4553
+ title: z8.string().optional().describe("New title"),
4554
+ severity: z8.enum(ISSUE_SEVERITIES).optional().describe("New severity"),
4555
+ impact: z8.string().optional().describe("New impact description"),
4556
+ resolution: z8.string().nullable().optional().describe("Resolution description (null to clear)"),
4557
+ components: z8.array(z8.string()).optional().describe("Affected components"),
4558
+ relatedTickets: z8.array(z8.string().regex(TICKET_ID_REGEX)).optional().describe("Related ticket IDs"),
4559
+ location: z8.array(z8.string()).optional().describe("File locations")
4560
+ }
4561
+ }, (args) => runMcpWriteTool(
4562
+ pinnedRoot,
4563
+ (root, format) => handleIssueUpdate(
4564
+ args.id,
4565
+ {
4566
+ status: args.status,
4567
+ title: args.title,
4568
+ severity: args.severity,
4569
+ impact: args.impact,
4570
+ resolution: args.resolution,
4571
+ components: args.components,
4572
+ relatedTickets: args.relatedTickets,
4573
+ location: args.location
4574
+ },
4575
+ format,
4576
+ root
4577
+ )
4578
+ ));
4579
+ server.registerTool("claudestory_note_list", {
4580
+ description: "List notes with optional status/tag filters",
4581
+ inputSchema: {
4582
+ status: z8.enum(NOTE_STATUSES).optional().describe("Filter by status: active, archived"),
4583
+ tag: z8.string().optional().describe("Filter by tag")
4584
+ }
4585
+ }, (args) => runMcpReadTool(
4586
+ pinnedRoot,
4587
+ (ctx) => handleNoteList({ status: args.status, tag: args.tag }, ctx)
4588
+ ));
4589
+ server.registerTool("claudestory_note_get", {
4590
+ description: "Get a note by ID",
4591
+ inputSchema: {
4592
+ id: z8.string().regex(NOTE_ID_REGEX).describe("Note ID (e.g. N-001)")
4593
+ }
4594
+ }, (args) => runMcpReadTool(pinnedRoot, (ctx) => handleNoteGet(args.id, ctx)));
4595
+ server.registerTool("claudestory_note_create", {
4596
+ description: "Create a new note",
4597
+ inputSchema: {
4598
+ content: z8.string().describe("Note content"),
4599
+ title: z8.string().optional().describe("Note title"),
4600
+ tags: z8.array(z8.string()).optional().describe("Tags for the note")
4601
+ }
4602
+ }, (args) => runMcpWriteTool(
4603
+ pinnedRoot,
4604
+ (root, format) => handleNoteCreate(
4605
+ {
4606
+ content: args.content,
4607
+ title: args.title ?? null,
4608
+ tags: args.tags ?? []
4609
+ },
4610
+ format,
4611
+ root
4612
+ )
4613
+ ));
4614
+ server.registerTool("claudestory_note_update", {
4615
+ description: "Update an existing note",
4616
+ inputSchema: {
4617
+ id: z8.string().regex(NOTE_ID_REGEX).describe("Note ID (e.g. N-001)"),
4618
+ content: z8.string().optional().describe("New content"),
4619
+ title: z8.string().nullable().optional().describe("New title (null to clear)"),
4620
+ tags: z8.array(z8.string()).optional().describe("New tags (replaces existing)"),
4621
+ status: z8.enum(NOTE_STATUSES).optional().describe("New status: active, archived")
4622
+ }
4623
+ }, (args) => runMcpWriteTool(
4624
+ pinnedRoot,
4625
+ (root, format) => handleNoteUpdate(
4626
+ args.id,
4627
+ {
4628
+ content: args.content,
4629
+ title: args.title,
4630
+ tags: args.tags,
4631
+ clearTags: args.tags !== void 0 && args.tags.length === 0,
4632
+ status: args.status
4633
+ },
4634
+ format,
4635
+ root
4636
+ )
4637
+ ));
3558
4638
  }
3559
4639
  var INFRASTRUCTURE_ERROR_CODES;
3560
4640
  var init_tools = __esm({
@@ -3572,6 +4652,8 @@ var init_tools = __esm({
3572
4652
  init_ticket2();
3573
4653
  init_issue2();
3574
4654
  init_recap();
4655
+ init_note2();
4656
+ init_recommend2();
3575
4657
  init_snapshot2();
3576
4658
  init_export();
3577
4659
  init_handover();
@@ -3586,7 +4668,7 @@ var init_tools = __esm({
3586
4668
 
3587
4669
  // src/mcp/index.ts
3588
4670
  var mcp_exports = {};
3589
- import { realpathSync, existsSync as existsSync5 } from "fs";
4671
+ import { realpathSync, existsSync as existsSync6 } from "fs";
3590
4672
  import { resolve as resolve7, join as join8, isAbsolute } from "path";
3591
4673
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3592
4674
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -3601,7 +4683,7 @@ function tryDiscoverRoot() {
3601
4683
  const resolved = resolve7(envRoot);
3602
4684
  try {
3603
4685
  const canonical = realpathSync(resolved);
3604
- if (existsSync5(join8(canonical, CONFIG_PATH2))) {
4686
+ if (existsSync6(join8(canonical, CONFIG_PATH2))) {
3605
4687
  return canonical;
3606
4688
  }
3607
4689
  process.stderr.write(`Warning: No .story/config.json at ${canonical}
@@ -3652,7 +4734,7 @@ var init_mcp = __esm({
3652
4734
  init_tools();
3653
4735
  ENV_VAR2 = "CLAUDESTORY_PROJECT_ROOT";
3654
4736
  CONFIG_PATH2 = ".story/config.json";
3655
- version = "0.1.7";
4737
+ version = "0.1.9";
3656
4738
  main().catch((err) => {
3657
4739
  process.stderr.write(`Fatal: ${err instanceof Error ? err.message : String(err)}
3658
4740
  `);
@@ -3662,7 +4744,7 @@ var init_mcp = __esm({
3662
4744
  });
3663
4745
 
3664
4746
  // src/core/init.ts
3665
- import { mkdir as mkdir3, stat as stat2 } from "fs/promises";
4747
+ import { mkdir as mkdir4, stat as stat2 } from "fs/promises";
3666
4748
  import { join as join9, resolve as resolve8 } from "path";
3667
4749
  async function initProject(root, options) {
3668
4750
  const absRoot = resolve8(root);
@@ -3686,9 +4768,18 @@ async function initProject(root, options) {
3686
4768
  ".story/ already exists. Use --force to overwrite config and roadmap."
3687
4769
  );
3688
4770
  }
3689
- await mkdir3(join9(wrapDir, "tickets"), { recursive: true });
3690
- await mkdir3(join9(wrapDir, "issues"), { recursive: true });
3691
- await mkdir3(join9(wrapDir, "handovers"), { recursive: true });
4771
+ await mkdir4(join9(wrapDir, "tickets"), { recursive: true });
4772
+ await mkdir4(join9(wrapDir, "issues"), { recursive: true });
4773
+ await mkdir4(join9(wrapDir, "handovers"), { recursive: true });
4774
+ await mkdir4(join9(wrapDir, "notes"), { recursive: true });
4775
+ const created = [
4776
+ ".story/config.json",
4777
+ ".story/roadmap.json",
4778
+ ".story/tickets/",
4779
+ ".story/issues/",
4780
+ ".story/handovers/",
4781
+ ".story/notes/"
4782
+ ];
3692
4783
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
3693
4784
  const config = {
3694
4785
  version: 2,
@@ -3733,13 +4824,7 @@ async function initProject(root, options) {
3733
4824
  }
3734
4825
  return {
3735
4826
  root: absRoot,
3736
- created: [
3737
- ".story/config.json",
3738
- ".story/roadmap.json",
3739
- ".story/tickets/",
3740
- ".story/issues/",
3741
- ".story/handovers/"
3742
- ],
4827
+ created,
3743
4828
  warnings
3744
4829
  };
3745
4830
  }
@@ -3763,6 +4848,7 @@ var init_core = __esm({
3763
4848
  init_handover_parser();
3764
4849
  init_errors();
3765
4850
  init_queries();
4851
+ init_recommend();
3766
4852
  init_id_allocation();
3767
4853
  init_validation();
3768
4854
  init_init();
@@ -3953,18 +5039,510 @@ function registerInitCommand(yargs) {
3953
5039
  process.exitCode = ExitCode.USER_ERROR;
3954
5040
  }
3955
5041
  }
3956
- );
5042
+ );
5043
+ }
5044
+ var init_init2 = __esm({
5045
+ "src/cli/commands/init.ts"() {
5046
+ "use strict";
5047
+ init_esm_shims();
5048
+ init_init();
5049
+ init_errors();
5050
+ init_output_formatter();
5051
+ init_project_root_discovery();
5052
+ init_helpers();
5053
+ init_run();
5054
+ }
5055
+ });
5056
+
5057
+ // src/cli/commands/reference.ts
5058
+ function handleReference(format) {
5059
+ return formatReference(COMMANDS, MCP_TOOLS, format);
5060
+ }
5061
+ var COMMANDS, MCP_TOOLS;
5062
+ var init_reference = __esm({
5063
+ "src/cli/commands/reference.ts"() {
5064
+ "use strict";
5065
+ init_esm_shims();
5066
+ init_output_formatter();
5067
+ COMMANDS = [
5068
+ {
5069
+ name: "init",
5070
+ description: "Initialize a new .story/ project",
5071
+ usage: "claudestory init [--name <name>] [--type <type>] [--language <lang>] [--force] [--format json|md]",
5072
+ flags: ["--name", "--type", "--language", "--force"]
5073
+ },
5074
+ {
5075
+ name: "status",
5076
+ description: "Project summary: phase statuses, ticket/issue counts, blockers",
5077
+ usage: "claudestory status [--format json|md]"
5078
+ },
5079
+ {
5080
+ name: "ticket list",
5081
+ description: "List tickets with optional filters",
5082
+ usage: "claudestory ticket list [--status <s>] [--phase <p>] [--type <t>] [--format json|md]",
5083
+ flags: ["--status", "--phase", "--type"]
5084
+ },
5085
+ {
5086
+ name: "ticket get",
5087
+ description: "Get ticket details by ID",
5088
+ usage: "claudestory ticket get <id> [--format json|md]"
5089
+ },
5090
+ {
5091
+ name: "ticket next",
5092
+ description: "Suggest next ticket(s) to work on",
5093
+ usage: "claudestory ticket next [--count N] [--format json|md]"
5094
+ },
5095
+ {
5096
+ name: "ticket blocked",
5097
+ description: "List blocked tickets with their blocking dependencies",
5098
+ usage: "claudestory ticket blocked [--format json|md]"
5099
+ },
5100
+ {
5101
+ name: "ticket create",
5102
+ description: "Create a new ticket",
5103
+ usage: "claudestory ticket create --title <t> --type <type> [--phase <p>] [--description <d>] [--blocked-by <ids>] [--parent-ticket <id>] [--format json|md]",
5104
+ flags: ["--title", "--type", "--phase", "--description", "--blocked-by", "--parent-ticket"]
5105
+ },
5106
+ {
5107
+ name: "ticket update",
5108
+ description: "Update a ticket",
5109
+ usage: "claudestory ticket update <id> [--status <s>] [--title <t>] [--phase <p>] [--order <n>] [--description <d>] [--blocked-by <ids>] [--parent-ticket <id>] [--format json|md]",
5110
+ flags: ["--status", "--title", "--phase", "--order", "--description", "--blocked-by", "--parent-ticket"]
5111
+ },
5112
+ {
5113
+ name: "ticket delete",
5114
+ description: "Delete a ticket",
5115
+ usage: "claudestory ticket delete <id> [--force] [--format json|md]",
5116
+ flags: ["--force"]
5117
+ },
5118
+ {
5119
+ name: "issue list",
5120
+ description: "List issues with optional filters",
5121
+ usage: "claudestory issue list [--status <s>] [--severity <sev>] [--format json|md]",
5122
+ flags: ["--status", "--severity"]
5123
+ },
5124
+ {
5125
+ name: "issue get",
5126
+ description: "Get issue details by ID",
5127
+ usage: "claudestory issue get <id> [--format json|md]"
5128
+ },
5129
+ {
5130
+ name: "issue create",
5131
+ description: "Create a new issue",
5132
+ usage: "claudestory issue create --title <t> --severity <s> --impact <i> [--components <c>] [--related-tickets <ids>] [--location <locs>] [--format json|md]",
5133
+ flags: ["--title", "--severity", "--impact", "--components", "--related-tickets", "--location"]
5134
+ },
5135
+ {
5136
+ name: "issue update",
5137
+ description: "Update an issue",
5138
+ usage: "claudestory issue update <id> [--status <s>] [--title <t>] [--severity <sev>] [--impact <i>] [--resolution <r>] [--components <c>] [--related-tickets <ids>] [--location <locs>] [--format json|md]",
5139
+ flags: ["--status", "--title", "--severity", "--impact", "--resolution", "--components", "--related-tickets", "--location"]
5140
+ },
5141
+ {
5142
+ name: "issue delete",
5143
+ description: "Delete an issue",
5144
+ usage: "claudestory issue delete <id> [--format json|md]"
5145
+ },
5146
+ {
5147
+ name: "phase list",
5148
+ description: "List all phases with derived status",
5149
+ usage: "claudestory phase list [--format json|md]"
5150
+ },
5151
+ {
5152
+ name: "phase current",
5153
+ description: "Show current (first non-complete) phase",
5154
+ usage: "claudestory phase current [--format json|md]"
5155
+ },
5156
+ {
5157
+ name: "phase tickets",
5158
+ description: "List tickets in a specific phase",
5159
+ usage: "claudestory phase tickets --phase <id> [--format json|md]",
5160
+ flags: ["--phase"]
5161
+ },
5162
+ {
5163
+ name: "phase create",
5164
+ description: "Create a new phase",
5165
+ usage: "claudestory phase create --id <id> --name <n> --label <l> --description <d> [--summary <s>] [--after <id>] [--at-start] [--format json|md]",
5166
+ flags: ["--id", "--name", "--label", "--description", "--summary", "--after", "--at-start"]
5167
+ },
5168
+ {
5169
+ name: "phase rename",
5170
+ description: "Rename/update phase metadata",
5171
+ usage: "claudestory phase rename <id> [--name <n>] [--label <l>] [--description <d>] [--summary <s>] [--format json|md]",
5172
+ flags: ["--name", "--label", "--description", "--summary"]
5173
+ },
5174
+ {
5175
+ name: "phase move",
5176
+ description: "Move a phase to a new position",
5177
+ usage: "claudestory phase move <id> [--after <id>] [--at-start] [--format json|md]",
5178
+ flags: ["--after", "--at-start"]
5179
+ },
5180
+ {
5181
+ name: "phase delete",
5182
+ description: "Delete a phase",
5183
+ usage: "claudestory phase delete <id> [--reassign <phase-id>] [--format json|md]",
5184
+ flags: ["--reassign"]
5185
+ },
5186
+ {
5187
+ name: "handover list",
5188
+ description: "List handover filenames (newest first)",
5189
+ usage: "claudestory handover list [--format json|md]"
5190
+ },
5191
+ {
5192
+ name: "handover latest",
5193
+ description: "Content of most recent handover",
5194
+ usage: "claudestory handover latest [--format json|md]"
5195
+ },
5196
+ {
5197
+ name: "handover get",
5198
+ description: "Content of a specific handover",
5199
+ usage: "claudestory handover get <filename> [--format json|md]"
5200
+ },
5201
+ {
5202
+ name: "handover create",
5203
+ description: "Create a new handover document",
5204
+ usage: "claudestory handover create [--content <md>] [--stdin] [--slug <slug>] [--format json|md]",
5205
+ flags: ["--content", "--stdin", "--slug"]
5206
+ },
5207
+ {
5208
+ name: "blocker list",
5209
+ description: "List all roadmap blockers",
5210
+ usage: "claudestory blocker list [--format json|md]"
5211
+ },
5212
+ {
5213
+ name: "blocker add",
5214
+ description: "Add a new blocker",
5215
+ usage: "claudestory blocker add --name <n> [--note <note>] [--format json|md]",
5216
+ flags: ["--name", "--note"]
5217
+ },
5218
+ {
5219
+ name: "blocker clear",
5220
+ description: "Clear (resolve) a blocker",
5221
+ usage: "claudestory blocker clear --name <n> [--note <note>] [--format json|md]",
5222
+ flags: ["--name", "--note"]
5223
+ },
5224
+ {
5225
+ name: "note list",
5226
+ description: "List notes with optional status/tag filters",
5227
+ usage: "claudestory note list [--status <s>] [--tag <t>] [--format json|md]",
5228
+ flags: ["--status", "--tag"]
5229
+ },
5230
+ {
5231
+ name: "note get",
5232
+ description: "Get a note by ID",
5233
+ usage: "claudestory note get <id> [--format json|md]"
5234
+ },
5235
+ {
5236
+ name: "note create",
5237
+ description: "Create a new note",
5238
+ usage: "claudestory note create --content <c> [--title <t>] [--tags <tags>] [--format json|md]",
5239
+ flags: ["--content", "--title", "--tags"]
5240
+ },
5241
+ {
5242
+ name: "note update",
5243
+ description: "Update a note",
5244
+ usage: "claudestory note update <id> [--content <c>] [--title <t>] [--tags <tags>] [--clear-tags] [--status <s>] [--format json|md]",
5245
+ flags: ["--content", "--title", "--tags", "--clear-tags", "--status"]
5246
+ },
5247
+ {
5248
+ name: "note delete",
5249
+ description: "Delete a note",
5250
+ usage: "claudestory note delete <id> [--format json|md]"
5251
+ },
5252
+ {
5253
+ name: "validate",
5254
+ description: "Reference integrity + schema checks on all .story/ files",
5255
+ usage: "claudestory validate [--format json|md]"
5256
+ },
5257
+ {
5258
+ name: "snapshot",
5259
+ description: "Save current project state for session diffs",
5260
+ usage: "claudestory snapshot [--quiet] [--format json|md]",
5261
+ flags: ["--quiet"]
5262
+ },
5263
+ {
5264
+ name: "recap",
5265
+ description: "Session diff \u2014 changes since last snapshot + suggested actions",
5266
+ usage: "claudestory recap [--format json|md]"
5267
+ },
5268
+ {
5269
+ name: "export",
5270
+ description: "Self-contained project document for sharing",
5271
+ usage: "claudestory export [--phase <id>] [--all] [--format json|md]",
5272
+ flags: ["--phase", "--all"]
5273
+ },
5274
+ {
5275
+ name: "recommend",
5276
+ description: "Context-aware work suggestions",
5277
+ usage: "claudestory recommend [--count N] [--format json|md]"
5278
+ },
5279
+ {
5280
+ name: "reference",
5281
+ description: "Print CLI command and MCP tool reference",
5282
+ usage: "claudestory reference [--format json|md]"
5283
+ },
5284
+ {
5285
+ name: "setup-skill",
5286
+ description: "Install the /story skill globally for Claude Code",
5287
+ usage: "claudestory setup-skill"
5288
+ }
5289
+ ];
5290
+ MCP_TOOLS = [
5291
+ { name: "claudestory_status", description: "Project summary: phase statuses, ticket/issue counts, blockers" },
5292
+ { name: "claudestory_phase_list", description: "All phases with derived status" },
5293
+ { name: "claudestory_phase_current", description: "First non-complete phase" },
5294
+ { name: "claudestory_phase_tickets", description: "Leaf tickets for a specific phase", params: ["phaseId"] },
5295
+ { name: "claudestory_ticket_list", description: "List leaf tickets with optional filters", params: ["status?", "phase?", "type?"] },
5296
+ { name: "claudestory_ticket_get", description: "Get a ticket by ID", params: ["id"] },
5297
+ { name: "claudestory_ticket_next", description: "Highest-priority unblocked ticket(s)", params: ["count?"] },
5298
+ { name: "claudestory_ticket_blocked", description: "All blocked tickets with dependencies" },
5299
+ { name: "claudestory_issue_list", description: "List issues with optional filters", params: ["status?", "severity?"] },
5300
+ { name: "claudestory_issue_get", description: "Get an issue by ID", params: ["id"] },
5301
+ { name: "claudestory_handover_list", description: "List handover filenames (newest first)" },
5302
+ { name: "claudestory_handover_latest", description: "Content of most recent handover" },
5303
+ { name: "claudestory_handover_get", description: "Content of a specific handover", params: ["filename"] },
5304
+ { name: "claudestory_handover_create", description: "Create a handover from markdown content", params: ["content", "slug?"] },
5305
+ { name: "claudestory_blocker_list", description: "All roadmap blockers with status" },
5306
+ { name: "claudestory_validate", description: "Reference integrity + schema checks" },
5307
+ { name: "claudestory_recap", description: "Session diff \u2014 changes since last snapshot" },
5308
+ { name: "claudestory_recommend", description: "Context-aware ranked work suggestions", params: ["count?"] },
5309
+ { name: "claudestory_snapshot", description: "Save current project state snapshot" },
5310
+ { name: "claudestory_export", description: "Self-contained project document", params: ["phase?", "all?"] },
5311
+ { name: "claudestory_note_list", description: "List notes", params: ["status?", "tag?"] },
5312
+ { name: "claudestory_note_get", description: "Get note by ID", params: ["id"] },
5313
+ { name: "claudestory_note_create", description: "Create note", params: ["content", "title?", "tags?"] },
5314
+ { name: "claudestory_note_update", description: "Update note", params: ["id", "content?", "title?", "tags?", "status?"] },
5315
+ { name: "claudestory_ticket_create", description: "Create ticket", params: ["title", "type", "phase?", "description?", "blockedBy?", "parentTicket?"] },
5316
+ { name: "claudestory_ticket_update", description: "Update ticket", params: ["id", "status?", "title?", "order?", "description?", "phase?", "parentTicket?"] },
5317
+ { name: "claudestory_issue_create", description: "Create issue", params: ["title", "severity", "impact", "components?", "relatedTickets?", "location?", "phase?"] },
5318
+ { name: "claudestory_issue_update", description: "Update issue", params: ["id", "status?", "title?", "severity?", "impact?", "resolution?", "components?", "relatedTickets?", "location?"] }
5319
+ ];
5320
+ }
5321
+ });
5322
+
5323
+ // src/cli/commands/setup-skill.ts
5324
+ var setup_skill_exports = {};
5325
+ __export(setup_skill_exports, {
5326
+ handleSetupSkill: () => handleSetupSkill,
5327
+ registerPreCompactHook: () => registerPreCompactHook,
5328
+ resolveSkillSourceDir: () => resolveSkillSourceDir
5329
+ });
5330
+ import { mkdir as mkdir5, writeFile as writeFile2, readFile as readFile4, rm, rename as rename2, unlink as unlink3 } from "fs/promises";
5331
+ import { existsSync as existsSync7 } from "fs";
5332
+ import { join as join11, dirname as dirname3 } from "path";
5333
+ import { homedir } from "os";
5334
+ import { execFileSync } from "child_process";
5335
+ import { fileURLToPath as fileURLToPath2 } from "url";
5336
+ function log(msg) {
5337
+ process.stdout.write(msg + "\n");
5338
+ }
5339
+ function resolveSkillSourceDir() {
5340
+ const thisDir = dirname3(fileURLToPath2(import.meta.url));
5341
+ const bundledPath = join11(thisDir, "..", "src", "skill");
5342
+ if (existsSync7(join11(bundledPath, "SKILL.md"))) return bundledPath;
5343
+ const sourcePath = join11(thisDir, "..", "..", "skill");
5344
+ if (existsSync7(join11(sourcePath, "SKILL.md"))) return sourcePath;
5345
+ throw new Error(
5346
+ `Cannot find bundled skill files. Checked:
5347
+ ${bundledPath}
5348
+ ${sourcePath}`
5349
+ );
5350
+ }
5351
+ function isOurHook(entry) {
5352
+ if (typeof entry !== "object" || entry === null) return false;
5353
+ const e = entry;
5354
+ return e.type === "command" && typeof e.command === "string" && e.command.trim() === HOOK_COMMAND;
5355
+ }
5356
+ async function registerPreCompactHook(settingsPath) {
5357
+ const path2 = settingsPath ?? join11(homedir(), ".claude", "settings.json");
5358
+ let raw = "{}";
5359
+ if (existsSync7(path2)) {
5360
+ try {
5361
+ raw = await readFile4(path2, "utf-8");
5362
+ } catch {
5363
+ process.stderr.write(`Could not read ${path2} \u2014 skipping hook registration.
5364
+ `);
5365
+ return "skipped";
5366
+ }
5367
+ }
5368
+ let settings;
5369
+ try {
5370
+ settings = JSON.parse(raw);
5371
+ if (typeof settings !== "object" || settings === null || Array.isArray(settings)) {
5372
+ process.stderr.write(`${path2} is not a JSON object \u2014 skipping hook registration.
5373
+ `);
5374
+ return "skipped";
5375
+ }
5376
+ } catch {
5377
+ process.stderr.write(`${path2} contains invalid JSON \u2014 skipping hook registration.
5378
+ `);
5379
+ process.stderr.write(" Fix the file manually or delete it to reset.\n");
5380
+ return "skipped";
5381
+ }
5382
+ if ("hooks" in settings) {
5383
+ if (typeof settings.hooks !== "object" || settings.hooks === null || Array.isArray(settings.hooks)) {
5384
+ process.stderr.write(`${path2} has unexpected hooks format \u2014 skipping hook registration.
5385
+ `);
5386
+ return "skipped";
5387
+ }
5388
+ } else {
5389
+ settings.hooks = {};
5390
+ }
5391
+ const hooks = settings.hooks;
5392
+ if ("PreCompact" in hooks) {
5393
+ if (!Array.isArray(hooks.PreCompact)) {
5394
+ process.stderr.write(`${path2} has unexpected hooks.PreCompact format \u2014 skipping hook registration.
5395
+ `);
5396
+ return "skipped";
5397
+ }
5398
+ } else {
5399
+ hooks.PreCompact = [];
5400
+ }
5401
+ const preCompact = hooks.PreCompact;
5402
+ for (const group of preCompact) {
5403
+ if (typeof group !== "object" || group === null) continue;
5404
+ const g = group;
5405
+ if (!Array.isArray(g.hooks)) continue;
5406
+ for (const entry of g.hooks) {
5407
+ if (isOurHook(entry)) return "exists";
5408
+ }
5409
+ }
5410
+ const ourEntry = { type: "command", command: HOOK_COMMAND };
5411
+ let appended = false;
5412
+ for (const group of preCompact) {
5413
+ if (typeof group !== "object" || group === null) continue;
5414
+ const g = group;
5415
+ if (g.matcher === "" && Array.isArray(g.hooks)) {
5416
+ g.hooks.push(ourEntry);
5417
+ appended = true;
5418
+ break;
5419
+ }
5420
+ }
5421
+ if (!appended) {
5422
+ preCompact.push({ matcher: "", hooks: [ourEntry] });
5423
+ }
5424
+ const tmpPath = `${path2}.${process.pid}.tmp`;
5425
+ try {
5426
+ const dir = dirname3(path2);
5427
+ await mkdir5(dir, { recursive: true });
5428
+ await writeFile2(tmpPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
5429
+ await rename2(tmpPath, path2);
5430
+ } catch (err) {
5431
+ try {
5432
+ await unlink3(tmpPath);
5433
+ } catch {
5434
+ }
5435
+ const message = err instanceof Error ? err.message : String(err);
5436
+ process.stderr.write(`Failed to write settings.json: ${message}
5437
+ `);
5438
+ return "skipped";
5439
+ }
5440
+ return "registered";
5441
+ }
5442
+ async function handleSetupSkill(options = {}) {
5443
+ const { skipHooks = false } = options;
5444
+ const skillDir = join11(homedir(), ".claude", "skills", "story");
5445
+ await mkdir5(skillDir, { recursive: true });
5446
+ let srcSkillDir;
5447
+ try {
5448
+ srcSkillDir = resolveSkillSourceDir();
5449
+ } catch (err) {
5450
+ const message = err instanceof Error ? err.message : String(err);
5451
+ process.stderr.write(`Error: ${message}
5452
+ `);
5453
+ process.stderr.write("This may indicate a corrupt installation. Try: npm install -g @anthropologies/claudestory\n");
5454
+ process.exitCode = 1;
5455
+ return;
5456
+ }
5457
+ const oldPrimeDir = join11(homedir(), ".claude", "skills", "prime");
5458
+ if (existsSync7(oldPrimeDir)) {
5459
+ await rm(oldPrimeDir, { recursive: true, force: true });
5460
+ log("Removed old /prime skill (migrated to /story)");
5461
+ }
5462
+ const existed = existsSync7(join11(skillDir, "SKILL.md"));
5463
+ const skillContent = await readFile4(join11(srcSkillDir, "SKILL.md"), "utf-8");
5464
+ await writeFile2(join11(skillDir, "SKILL.md"), skillContent, "utf-8");
5465
+ let referenceWritten = false;
5466
+ const refSrcPath = join11(srcSkillDir, "reference.md");
5467
+ if (existsSync7(refSrcPath)) {
5468
+ const refContent = await readFile4(refSrcPath, "utf-8");
5469
+ await writeFile2(join11(skillDir, "reference.md"), refContent, "utf-8");
5470
+ referenceWritten = true;
5471
+ }
5472
+ log(`${existed ? "Updated" : "Installed"} /story skill at ${skillDir}/`);
5473
+ if (referenceWritten) {
5474
+ log(" SKILL.md + reference.md written");
5475
+ } else {
5476
+ log(" SKILL.md written (reference.md not found \u2014 generate with `claudestory reference --format md`)");
5477
+ }
5478
+ let mcpRegistered = false;
5479
+ let cliInPath = false;
5480
+ try {
5481
+ execFileSync("claudestory", ["--version"], { stdio: "pipe", timeout: 5e3 });
5482
+ cliInPath = true;
5483
+ } catch {
5484
+ }
5485
+ if (cliInPath) {
5486
+ try {
5487
+ execFileSync("claude", ["mcp", "add", "claudestory", "-s", "user", "--", "claudestory", "--mcp"], {
5488
+ stdio: "pipe",
5489
+ timeout: 1e4
5490
+ });
5491
+ mcpRegistered = true;
5492
+ log(" MCP server registered globally");
5493
+ } catch (err) {
5494
+ const message = err instanceof Error ? err.message : String(err);
5495
+ const isAlreadyRegistered = message.includes("already exists");
5496
+ const isNotFound = message.includes("ENOENT") || message.includes("not found");
5497
+ if (isAlreadyRegistered) {
5498
+ mcpRegistered = true;
5499
+ log(" MCP server already registered globally");
5500
+ } else if (isNotFound) {
5501
+ log("");
5502
+ log("MCP registration skipped \u2014 `claude` CLI not found in PATH.");
5503
+ log(" To register manually: claude mcp add claudestory -s user -- claudestory --mcp");
5504
+ } else {
5505
+ log("");
5506
+ log(`MCP registration failed: ${message.split("\n")[0]}`);
5507
+ log(" To register manually: claude mcp add claudestory -s user -- claudestory --mcp");
5508
+ }
5509
+ }
5510
+ } else {
5511
+ log("");
5512
+ log("MCP registration skipped \u2014 `claudestory` not found in PATH.");
5513
+ log("Install globally first, then register MCP:");
5514
+ log(" npm install -g @anthropologies/claudestory");
5515
+ log(" claude mcp add claudestory -s user -- claudestory --mcp");
5516
+ }
5517
+ if (cliInPath && !skipHooks) {
5518
+ const result = await registerPreCompactHook();
5519
+ switch (result) {
5520
+ case "registered":
5521
+ log(" PreCompact hook registered \u2014 snapshots auto-taken before context compaction");
5522
+ break;
5523
+ case "exists":
5524
+ log(" PreCompact hook already configured");
5525
+ break;
5526
+ case "skipped":
5527
+ break;
5528
+ }
5529
+ } else if (!cliInPath) {
5530
+ } else if (skipHooks) {
5531
+ log(" Hook registration skipped (--skip-hooks)");
5532
+ }
5533
+ log("");
5534
+ if (mcpRegistered) {
5535
+ log("Done! Restart Claude Code, then type /story in any project.");
5536
+ } else {
5537
+ log("Skill installed. After registering MCP, restart Claude Code and type /story.");
5538
+ }
3957
5539
  }
3958
- var init_init2 = __esm({
3959
- "src/cli/commands/init.ts"() {
5540
+ var HOOK_COMMAND;
5541
+ var init_setup_skill = __esm({
5542
+ "src/cli/commands/setup-skill.ts"() {
3960
5543
  "use strict";
3961
5544
  init_esm_shims();
3962
- init_init();
3963
- init_errors();
3964
- init_output_formatter();
3965
- init_project_root_discovery();
3966
- init_helpers();
3967
- init_run();
5545
+ HOOK_COMMAND = "claudestory snapshot --quiet";
3968
5546
  }
3969
5547
  });
3970
5548
 
@@ -3976,8 +5554,12 @@ __export(register_exports, {
3976
5554
  registerHandoverCommand: () => registerHandoverCommand,
3977
5555
  registerInitCommand: () => registerInitCommand,
3978
5556
  registerIssueCommand: () => registerIssueCommand,
5557
+ registerNoteCommand: () => registerNoteCommand,
3979
5558
  registerPhaseCommand: () => registerPhaseCommand,
3980
5559
  registerRecapCommand: () => registerRecapCommand,
5560
+ registerRecommendCommand: () => registerRecommendCommand,
5561
+ registerReferenceCommand: () => registerReferenceCommand,
5562
+ registerSetupSkillCommand: () => registerSetupSkillCommand,
3981
5563
  registerSnapshotCommand: () => registerSnapshotCommand,
3982
5564
  registerStatusCommand: () => registerStatusCommand,
3983
5565
  registerTicketCommand: () => registerTicketCommand,
@@ -4019,11 +5601,21 @@ function registerHandoverCommand(yargs) {
4019
5601
  }
4020
5602
  ).command(
4021
5603
  "latest",
4022
- "Content of most recent handover",
4023
- (y2) => addFormatOption(y2),
5604
+ "Content of most recent handover(s)",
5605
+ (y2) => addFormatOption(
5606
+ y2.option("count", {
5607
+ type: "number",
5608
+ default: 1,
5609
+ describe: "Number of recent handovers to return (default: 1)"
5610
+ })
5611
+ ),
4024
5612
  async (argv) => {
4025
5613
  const format = parseOutputFormat(argv.format);
4026
- await runReadCommand(format, handleHandoverLatest);
5614
+ const count = Math.max(1, Math.floor(argv.count));
5615
+ await runReadCommand(
5616
+ format,
5617
+ (ctx) => handleHandoverLatest(ctx, count)
5618
+ );
4027
5619
  }
4028
5620
  ).command(
4029
5621
  "get <filename>",
@@ -4300,10 +5892,16 @@ function registerTicketCommand(yargs) {
4300
5892
  ).command(
4301
5893
  "next",
4302
5894
  "Suggest next ticket to work on",
4303
- (y2) => addFormatOption(y2),
5895
+ (y2) => addFormatOption(y2).option("count", {
5896
+ type: "number",
5897
+ default: 1,
5898
+ describe: "Number of candidates to suggest (1-10)"
5899
+ }),
4304
5900
  async (argv) => {
4305
5901
  const format = parseOutputFormat(argv.format);
4306
- await runReadCommand(format, handleTicketNext);
5902
+ const raw = Number(argv.count) || 1;
5903
+ const count = Math.max(1, Math.min(10, Math.floor(raw)));
5904
+ await runReadCommand(format, (ctx) => handleTicketNext(ctx, count));
4307
5905
  }
4308
5906
  ).command(
4309
5907
  "blocked",
@@ -4330,8 +5928,10 @@ function registerTicketCommand(yargs) {
4330
5928
  describe: "Phase ID"
4331
5929
  }).option("description", {
4332
5930
  type: "string",
4333
- default: "",
4334
5931
  describe: "Ticket description"
5932
+ }).option("stdin", {
5933
+ type: "boolean",
5934
+ describe: "Read description from stdin"
4335
5935
  }).option("blocked-by", {
4336
5936
  type: "string",
4337
5937
  array: true,
@@ -4339,7 +5939,7 @@ function registerTicketCommand(yargs) {
4339
5939
  }).option("parent-ticket", {
4340
5940
  type: "string",
4341
5941
  describe: "Parent ticket ID (makes this a sub-ticket)"
4342
- })
5942
+ }).conflicts("description", "stdin")
4343
5943
  ),
4344
5944
  async (argv) => {
4345
5945
  const format = parseOutputFormat(argv.format);
@@ -4356,12 +5956,16 @@ function registerTicketCommand(yargs) {
4356
5956
  return;
4357
5957
  }
4358
5958
  try {
5959
+ let description = argv.description ?? "";
5960
+ if (argv.stdin) {
5961
+ description = await readStdinContent();
5962
+ }
4359
5963
  const result = await handleTicketCreate(
4360
5964
  {
4361
5965
  title: argv.title,
4362
5966
  type: argv.type,
4363
5967
  phase: argv.phase === "" ? null : argv.phase ?? null,
4364
- description: argv.description,
5968
+ description,
4365
5969
  blockedBy: normalizeArrayOption(
4366
5970
  argv["blocked-by"]
4367
5971
  ),
@@ -4412,6 +6016,9 @@ function registerTicketCommand(yargs) {
4412
6016
  }).option("description", {
4413
6017
  type: "string",
4414
6018
  describe: "New description"
6019
+ }).option("stdin", {
6020
+ type: "boolean",
6021
+ describe: "Read description from stdin"
4415
6022
  }).option("blocked-by", {
4416
6023
  type: "string",
4417
6024
  array: true,
@@ -4419,7 +6026,7 @@ function registerTicketCommand(yargs) {
4419
6026
  }).option("parent-ticket", {
4420
6027
  type: "string",
4421
6028
  describe: "Parent ticket ID"
4422
- })
6029
+ }).conflicts("description", "stdin")
4423
6030
  ),
4424
6031
  async (argv) => {
4425
6032
  const format = parseOutputFormat(argv.format);
@@ -4437,6 +6044,10 @@ function registerTicketCommand(yargs) {
4437
6044
  return;
4438
6045
  }
4439
6046
  try {
6047
+ let description = argv.description;
6048
+ if (argv.stdin) {
6049
+ description = await readStdinContent();
6050
+ }
4440
6051
  const result = await handleTicketUpdate(
4441
6052
  id,
4442
6053
  {
@@ -4444,7 +6055,7 @@ function registerTicketCommand(yargs) {
4444
6055
  title: argv.title,
4445
6056
  phase: argv.phase === "" ? null : argv.phase,
4446
6057
  order: argv.order,
4447
- description: argv.description,
6058
+ description,
4448
6059
  blockedBy: argv["blocked-by"] ? normalizeArrayOption(argv["blocked-by"]) : void 0,
4449
6060
  parentTicket: argv["parent-ticket"] === "" ? null : argv["parent-ticket"]
4450
6061
  },
@@ -4516,6 +6127,9 @@ function registerIssueCommand(yargs) {
4516
6127
  }).option("severity", {
4517
6128
  type: "string",
4518
6129
  describe: "Filter by severity"
6130
+ }).option("component", {
6131
+ type: "string",
6132
+ describe: "Filter by component"
4519
6133
  })
4520
6134
  ),
4521
6135
  async (argv) => {
@@ -4525,7 +6139,8 @@ function registerIssueCommand(yargs) {
4525
6139
  (ctx) => handleIssueList(
4526
6140
  {
4527
6141
  status: argv.status,
4528
- severity: argv.severity
6142
+ severity: argv.severity,
6143
+ component: argv.component
4529
6144
  },
4530
6145
  ctx
4531
6146
  )
@@ -4560,8 +6175,13 @@ function registerIssueCommand(yargs) {
4560
6175
  describe: "Issue severity"
4561
6176
  }).option("impact", {
4562
6177
  type: "string",
4563
- demandOption: true,
4564
6178
  describe: "Impact description"
6179
+ }).option("stdin", {
6180
+ type: "boolean",
6181
+ describe: "Read impact from stdin"
6182
+ }).option("phase", {
6183
+ type: "string",
6184
+ describe: "Phase ID"
4565
6185
  }).option("components", {
4566
6186
  type: "string",
4567
6187
  array: true,
@@ -4574,6 +6194,11 @@ function registerIssueCommand(yargs) {
4574
6194
  type: "string",
4575
6195
  array: true,
4576
6196
  describe: "File locations"
6197
+ }).conflicts("impact", "stdin").check((a) => {
6198
+ if (!a.impact && !a.stdin) {
6199
+ throw new Error("Specify either --impact or --stdin");
6200
+ }
6201
+ return true;
4577
6202
  })
4578
6203
  ),
4579
6204
  async (argv) => {
@@ -4591,11 +6216,15 @@ function registerIssueCommand(yargs) {
4591
6216
  return;
4592
6217
  }
4593
6218
  try {
6219
+ let impact = argv.impact ?? "";
6220
+ if (argv.stdin) {
6221
+ impact = await readStdinContent();
6222
+ }
4594
6223
  const result = await handleIssueCreate(
4595
6224
  {
4596
6225
  title: argv.title,
4597
6226
  severity: argv.severity,
4598
- impact: argv.impact,
6227
+ impact,
4599
6228
  components: normalizeArrayOption(
4600
6229
  argv.components
4601
6230
  ),
@@ -4604,7 +6233,8 @@ function registerIssueCommand(yargs) {
4604
6233
  ),
4605
6234
  location: normalizeArrayOption(
4606
6235
  argv.location
4607
- )
6236
+ ),
6237
+ phase: argv.phase
4608
6238
  },
4609
6239
  format,
4610
6240
  root
@@ -4648,6 +6278,9 @@ function registerIssueCommand(yargs) {
4648
6278
  }).option("impact", {
4649
6279
  type: "string",
4650
6280
  describe: "New impact description"
6281
+ }).option("stdin", {
6282
+ type: "boolean",
6283
+ describe: "Read impact from stdin"
4651
6284
  }).option("resolution", {
4652
6285
  type: "string",
4653
6286
  describe: "Resolution description"
@@ -4663,7 +6296,7 @@ function registerIssueCommand(yargs) {
4663
6296
  type: "string",
4664
6297
  array: true,
4665
6298
  describe: "File locations"
4666
- })
6299
+ }).conflicts("impact", "stdin")
4667
6300
  ),
4668
6301
  async (argv) => {
4669
6302
  const format = parseOutputFormat(argv.format);
@@ -4681,13 +6314,17 @@ function registerIssueCommand(yargs) {
4681
6314
  return;
4682
6315
  }
4683
6316
  try {
6317
+ let impact = argv.impact;
6318
+ if (argv.stdin) {
6319
+ impact = await readStdinContent();
6320
+ }
4684
6321
  const result = await handleIssueUpdate(
4685
6322
  id,
4686
6323
  {
4687
6324
  status: argv.status,
4688
6325
  title: argv.title,
4689
6326
  severity: argv.severity,
4690
- impact: argv.impact,
6327
+ impact,
4691
6328
  resolution: argv.resolution === "" ? null : argv.resolution,
4692
6329
  components: argv.components ? normalizeArrayOption(argv.components) : void 0,
4693
6330
  relatedTickets: argv["related-tickets"] ? normalizeArrayOption(argv["related-tickets"]) : void 0,
@@ -5054,11 +6691,23 @@ function registerSnapshotCommand(yargs) {
5054
6691
  return yargs.command(
5055
6692
  "snapshot",
5056
6693
  "Save current project state for session diffs",
5057
- (y) => addFormatOption(y),
6694
+ (y) => addFormatOption(
6695
+ y.option("quiet", {
6696
+ type: "boolean",
6697
+ default: false,
6698
+ describe: "Suppress output (for hook usage)"
6699
+ })
6700
+ ),
5058
6701
  async (argv) => {
5059
6702
  const format = parseOutputFormat(argv.format);
6703
+ const quiet = argv.quiet;
5060
6704
  const root = (await Promise.resolve().then(() => (init_project_root_discovery(), project_root_discovery_exports))).discoverProjectRoot();
5061
6705
  if (!root) {
6706
+ if (quiet) {
6707
+ process.stderr.write("No .story/ project found.\n");
6708
+ process.exitCode = ExitCode.USER_ERROR;
6709
+ return;
6710
+ }
5062
6711
  writeOutput(
5063
6712
  formatError("not_found", "No .story/ project found.", format)
5064
6713
  );
@@ -5066,10 +6715,18 @@ function registerSnapshotCommand(yargs) {
5066
6715
  return;
5067
6716
  }
5068
6717
  try {
5069
- const result = await handleSnapshot(root, format);
5070
- writeOutput(result.output);
6718
+ const result = await handleSnapshot(root, format, { quiet });
6719
+ if (!quiet && result.output) {
6720
+ writeOutput(result.output);
6721
+ }
5071
6722
  process.exitCode = result.exitCode ?? ExitCode.OK;
5072
6723
  } catch (err) {
6724
+ if (quiet) {
6725
+ const message2 = err instanceof Error ? err.message : String(err);
6726
+ process.stderr.write(message2 + "\n");
6727
+ process.exitCode = ExitCode.USER_ERROR;
6728
+ return;
6729
+ }
5073
6730
  if (err instanceof CliValidationError) {
5074
6731
  writeOutput(formatError(err.code, err.message, format));
5075
6732
  process.exitCode = ExitCode.USER_ERROR;
@@ -5130,6 +6787,271 @@ function registerExportCommand(yargs) {
5130
6787
  }
5131
6788
  );
5132
6789
  }
6790
+ function registerNoteCommand(yargs) {
6791
+ return yargs.command(
6792
+ "note",
6793
+ "Manage notes",
6794
+ (y) => y.command(
6795
+ "list",
6796
+ "List notes",
6797
+ (y2) => addFormatOption(
6798
+ y2.option("status", {
6799
+ type: "string",
6800
+ choices: ["active", "archived"],
6801
+ describe: "Filter by status"
6802
+ }).option("tag", {
6803
+ type: "string",
6804
+ describe: "Filter by tag"
6805
+ })
6806
+ ),
6807
+ async (argv) => {
6808
+ const format = parseOutputFormat(argv.format);
6809
+ await runReadCommand(
6810
+ format,
6811
+ (ctx) => handleNoteList(
6812
+ {
6813
+ status: argv.status,
6814
+ tag: argv.tag
6815
+ },
6816
+ ctx
6817
+ )
6818
+ );
6819
+ }
6820
+ ).command(
6821
+ "get <id>",
6822
+ "Get a note",
6823
+ (y2) => addFormatOption(
6824
+ y2.positional("id", {
6825
+ type: "string",
6826
+ demandOption: true,
6827
+ describe: "Note ID (e.g. N-001)"
6828
+ })
6829
+ ),
6830
+ async (argv) => {
6831
+ const format = parseOutputFormat(argv.format);
6832
+ const id = parseNoteId(argv.id);
6833
+ await runReadCommand(format, (ctx) => handleNoteGet(id, ctx));
6834
+ }
6835
+ ).command(
6836
+ "create",
6837
+ "Create a note",
6838
+ (y2) => addFormatOption(
6839
+ y2.option("content", {
6840
+ type: "string",
6841
+ describe: "Note content"
6842
+ }).option("title", {
6843
+ type: "string",
6844
+ describe: "Note title"
6845
+ }).option("tags", {
6846
+ type: "array",
6847
+ describe: "Tags for the note"
6848
+ }).option("stdin", {
6849
+ type: "boolean",
6850
+ describe: "Read content from stdin"
6851
+ }).conflicts("content", "stdin").check((argv) => {
6852
+ if (!argv.content && !argv.stdin) {
6853
+ throw new Error(
6854
+ "Specify either --content or --stdin"
6855
+ );
6856
+ }
6857
+ return true;
6858
+ })
6859
+ ),
6860
+ async (argv) => {
6861
+ const format = parseOutputFormat(argv.format);
6862
+ const root = (await Promise.resolve().then(() => (init_project_root_discovery(), project_root_discovery_exports))).discoverProjectRoot();
6863
+ if (!root) {
6864
+ writeOutput(
6865
+ formatError("not_found", "No .story/ project found.", format)
6866
+ );
6867
+ process.exitCode = ExitCode.USER_ERROR;
6868
+ return;
6869
+ }
6870
+ try {
6871
+ let content;
6872
+ if (argv.stdin) {
6873
+ content = await readStdinContent();
6874
+ } else {
6875
+ content = argv.content;
6876
+ }
6877
+ const result = await handleNoteCreate(
6878
+ {
6879
+ content,
6880
+ title: argv.title ?? null,
6881
+ tags: argv.tags
6882
+ },
6883
+ format,
6884
+ root
6885
+ );
6886
+ writeOutput(result.output);
6887
+ process.exitCode = result.exitCode ?? ExitCode.OK;
6888
+ } catch (err) {
6889
+ if (err instanceof CliValidationError) {
6890
+ writeOutput(formatError(err.code, err.message, format));
6891
+ process.exitCode = ExitCode.USER_ERROR;
6892
+ return;
6893
+ }
6894
+ const { ProjectLoaderError: ProjectLoaderError2 } = await Promise.resolve().then(() => (init_errors(), errors_exports));
6895
+ if (err instanceof ProjectLoaderError2) {
6896
+ writeOutput(formatError(err.code, err.message, format));
6897
+ process.exitCode = ExitCode.USER_ERROR;
6898
+ return;
6899
+ }
6900
+ const message = err instanceof Error ? err.message : String(err);
6901
+ writeOutput(formatError("io_error", message, format));
6902
+ process.exitCode = ExitCode.USER_ERROR;
6903
+ }
6904
+ }
6905
+ ).command(
6906
+ "update <id>",
6907
+ "Update a note",
6908
+ (y2) => addFormatOption(
6909
+ y2.positional("id", {
6910
+ type: "string",
6911
+ demandOption: true,
6912
+ describe: "Note ID (e.g. N-001)"
6913
+ }).option("content", {
6914
+ type: "string",
6915
+ describe: "New content"
6916
+ }).option("title", {
6917
+ type: "string",
6918
+ describe: "New title"
6919
+ }).option("tags", {
6920
+ type: "array",
6921
+ describe: "New tags (replaces existing)"
6922
+ }).option("clear-tags", {
6923
+ type: "boolean",
6924
+ default: false,
6925
+ describe: "Clear all tags"
6926
+ }).option("status", {
6927
+ type: "string",
6928
+ choices: ["active", "archived"],
6929
+ describe: "New status"
6930
+ }).option("stdin", {
6931
+ type: "boolean",
6932
+ describe: "Read content from stdin"
6933
+ }).conflicts("content", "stdin").conflicts("tags", "clear-tags")
6934
+ ),
6935
+ async (argv) => {
6936
+ const format = parseOutputFormat(argv.format);
6937
+ const id = parseNoteId(argv.id);
6938
+ const root = (await Promise.resolve().then(() => (init_project_root_discovery(), project_root_discovery_exports))).discoverProjectRoot();
6939
+ if (!root) {
6940
+ writeOutput(
6941
+ formatError("not_found", "No .story/ project found.", format)
6942
+ );
6943
+ process.exitCode = ExitCode.USER_ERROR;
6944
+ return;
6945
+ }
6946
+ let content;
6947
+ if (argv.stdin) {
6948
+ content = await readStdinContent();
6949
+ } else {
6950
+ content = argv.content;
6951
+ }
6952
+ try {
6953
+ const result = await handleNoteUpdate(
6954
+ id,
6955
+ {
6956
+ content,
6957
+ title: argv.title === "" ? null : argv.title,
6958
+ tags: argv.tags,
6959
+ clearTags: argv["clear-tags"],
6960
+ status: argv.status
6961
+ },
6962
+ format,
6963
+ root
6964
+ );
6965
+ writeOutput(result.output);
6966
+ process.exitCode = result.exitCode ?? ExitCode.OK;
6967
+ } catch (err) {
6968
+ if (err instanceof CliValidationError) {
6969
+ writeOutput(formatError(err.code, err.message, format));
6970
+ process.exitCode = ExitCode.USER_ERROR;
6971
+ return;
6972
+ }
6973
+ const { ProjectLoaderError: ProjectLoaderError2 } = await Promise.resolve().then(() => (init_errors(), errors_exports));
6974
+ if (err instanceof ProjectLoaderError2) {
6975
+ writeOutput(formatError(err.code, err.message, format));
6976
+ process.exitCode = ExitCode.USER_ERROR;
6977
+ return;
6978
+ }
6979
+ const message = err instanceof Error ? err.message : String(err);
6980
+ writeOutput(formatError("io_error", message, format));
6981
+ process.exitCode = ExitCode.USER_ERROR;
6982
+ }
6983
+ }
6984
+ ).command(
6985
+ "delete <id>",
6986
+ "Delete a note",
6987
+ (y2) => addFormatOption(
6988
+ y2.positional("id", {
6989
+ type: "string",
6990
+ demandOption: true,
6991
+ describe: "Note ID (e.g. N-001)"
6992
+ })
6993
+ ),
6994
+ async (argv) => {
6995
+ const format = parseOutputFormat(argv.format);
6996
+ const id = parseNoteId(argv.id);
6997
+ await runDeleteCommand(
6998
+ format,
6999
+ false,
7000
+ async (ctx) => handleNoteDelete(id, format, ctx.root)
7001
+ );
7002
+ }
7003
+ ).demandCommand(
7004
+ 1,
7005
+ "Specify a note subcommand: list, get, create, update, delete"
7006
+ ).strict(),
7007
+ () => {
7008
+ }
7009
+ );
7010
+ }
7011
+ function registerReferenceCommand(yargs) {
7012
+ return yargs.command(
7013
+ "reference",
7014
+ "Print CLI command and MCP tool reference",
7015
+ (y) => addFormatOption(y),
7016
+ async (argv) => {
7017
+ const format = parseOutputFormat(argv.format);
7018
+ const output = handleReference(format);
7019
+ writeOutput(output);
7020
+ }
7021
+ );
7022
+ }
7023
+ function registerRecommendCommand(yargs) {
7024
+ return yargs.command(
7025
+ "recommend",
7026
+ "Context-aware work suggestions",
7027
+ (y) => addFormatOption(y).option("count", {
7028
+ type: "number",
7029
+ default: 5,
7030
+ describe: "Number of recommendations (1-10)"
7031
+ }),
7032
+ async (argv) => {
7033
+ const format = parseOutputFormat(argv.format);
7034
+ const raw = Number(argv.count) || 5;
7035
+ const count = Math.max(1, Math.min(10, Math.floor(raw)));
7036
+ await runReadCommand(format, (ctx) => handleRecommend(ctx, count));
7037
+ }
7038
+ );
7039
+ }
7040
+ function registerSetupSkillCommand(yargs) {
7041
+ return yargs.command(
7042
+ "setup-skill",
7043
+ "Install the /story skill globally for Claude Code",
7044
+ (y) => y.option("skip-hooks", {
7045
+ type: "boolean",
7046
+ default: false,
7047
+ description: "Skip PreCompact hook registration"
7048
+ }),
7049
+ async (argv) => {
7050
+ const { handleSetupSkill: handleSetupSkill2 } = await Promise.resolve().then(() => (init_setup_skill(), setup_skill_exports));
7051
+ await handleSetupSkill2({ skipHooks: argv["skip-hooks"] === true });
7052
+ }
7053
+ );
7054
+ }
5133
7055
  var init_register = __esm({
5134
7056
  "src/cli/register.ts"() {
5135
7057
  "use strict";
@@ -5143,11 +7065,14 @@ var init_register = __esm({
5143
7065
  init_blocker();
5144
7066
  init_ticket2();
5145
7067
  init_issue2();
7068
+ init_note2();
7069
+ init_recommend2();
5146
7070
  init_phase();
5147
7071
  init_init2();
5148
7072
  init_recap();
5149
7073
  init_export();
5150
7074
  init_snapshot2();
7075
+ init_reference();
5151
7076
  }
5152
7077
  });
5153
7078
 
@@ -5174,9 +7099,13 @@ async function runCli() {
5174
7099
  registerValidateCommand: registerValidateCommand2,
5175
7100
  registerSnapshotCommand: registerSnapshotCommand2,
5176
7101
  registerRecapCommand: registerRecapCommand2,
5177
- registerExportCommand: registerExportCommand2
7102
+ registerExportCommand: registerExportCommand2,
7103
+ registerNoteCommand: registerNoteCommand2,
7104
+ registerRecommendCommand: registerRecommendCommand2,
7105
+ registerReferenceCommand: registerReferenceCommand2,
7106
+ registerSetupSkillCommand: registerSetupSkillCommand2
5178
7107
  } = await Promise.resolve().then(() => (init_register(), register_exports));
5179
- const version2 = "0.1.7";
7108
+ const version2 = "0.1.9";
5180
7109
  class HandledError extends Error {
5181
7110
  constructor() {
5182
7111
  super("HANDLED_ERROR");
@@ -5203,12 +7132,16 @@ async function runCli() {
5203
7132
  cli = registerPhaseCommand2(cli);
5204
7133
  cli = registerTicketCommand2(cli);
5205
7134
  cli = registerIssueCommand2(cli);
7135
+ cli = registerNoteCommand2(cli);
5206
7136
  cli = registerHandoverCommand2(cli);
5207
7137
  cli = registerBlockerCommand2(cli);
5208
7138
  cli = registerValidateCommand2(cli);
5209
7139
  cli = registerSnapshotCommand2(cli);
5210
7140
  cli = registerRecapCommand2(cli);
5211
7141
  cli = registerExportCommand2(cli);
7142
+ cli = registerRecommendCommand2(cli);
7143
+ cli = registerReferenceCommand2(cli);
7144
+ cli = registerSetupSkillCommand2(cli);
5212
7145
  function handleUnexpectedError(err) {
5213
7146
  if (err instanceof HandledError) return;
5214
7147
  const message = err instanceof Error ? err.message : String(err);