@anthropologies/claudestory 0.1.0

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 ADDED
@@ -0,0 +1,3792 @@
1
+ #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res) => function __init() {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ };
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+
12
+ // node_modules/tsup/assets/esm_shims.js
13
+ import path from "path";
14
+ import { fileURLToPath } from "url";
15
+ var init_esm_shims = __esm({
16
+ "node_modules/tsup/assets/esm_shims.js"() {
17
+ "use strict";
18
+ }
19
+ });
20
+
21
+ // src/core/errors.ts
22
+ var errors_exports = {};
23
+ __export(errors_exports, {
24
+ CURRENT_SCHEMA_VERSION: () => CURRENT_SCHEMA_VERSION,
25
+ INTEGRITY_WARNING_TYPES: () => INTEGRITY_WARNING_TYPES,
26
+ ProjectLoaderError: () => ProjectLoaderError
27
+ });
28
+ var CURRENT_SCHEMA_VERSION, ProjectLoaderError, INTEGRITY_WARNING_TYPES;
29
+ var init_errors = __esm({
30
+ "src/core/errors.ts"() {
31
+ "use strict";
32
+ init_esm_shims();
33
+ CURRENT_SCHEMA_VERSION = 1;
34
+ ProjectLoaderError = class extends Error {
35
+ constructor(code, message, cause) {
36
+ super(message);
37
+ this.code = code;
38
+ this.cause = cause;
39
+ }
40
+ name = "ProjectLoaderError";
41
+ };
42
+ INTEGRITY_WARNING_TYPES = [
43
+ "parse_error",
44
+ "schema_error",
45
+ "duplicate_id"
46
+ ];
47
+ }
48
+ });
49
+
50
+ // src/core/project-root-discovery.ts
51
+ var project_root_discovery_exports = {};
52
+ __export(project_root_discovery_exports, {
53
+ discoverProjectRoot: () => discoverProjectRoot
54
+ });
55
+ import { existsSync as existsSync3 } from "fs";
56
+ import { resolve as resolve2, dirname as dirname2, join as join3 } from "path";
57
+ function discoverProjectRoot(startDir) {
58
+ const envRoot = process.env[ENV_VAR];
59
+ if (envRoot) {
60
+ const resolved = resolve2(envRoot);
61
+ if (existsSync3(join3(resolved, CONFIG_PATH))) {
62
+ return resolved;
63
+ }
64
+ return null;
65
+ }
66
+ let current = resolve2(startDir ?? process.cwd());
67
+ for (; ; ) {
68
+ if (existsSync3(join3(current, CONFIG_PATH))) {
69
+ return current;
70
+ }
71
+ const parent = dirname2(current);
72
+ if (parent === current) break;
73
+ current = parent;
74
+ }
75
+ return null;
76
+ }
77
+ var ENV_VAR, CONFIG_PATH;
78
+ var init_project_root_discovery = __esm({
79
+ "src/core/project-root-discovery.ts"() {
80
+ "use strict";
81
+ init_esm_shims();
82
+ ENV_VAR = "CLAUDESTORY_PROJECT_ROOT";
83
+ CONFIG_PATH = ".story/config.json";
84
+ }
85
+ });
86
+
87
+ // src/cli/index.ts
88
+ init_esm_shims();
89
+ import yargs from "yargs";
90
+ import { hideBin } from "yargs/helpers";
91
+
92
+ // src/core/output-formatter.ts
93
+ init_esm_shims();
94
+
95
+ // src/core/queries.ts
96
+ init_esm_shims();
97
+ function nextTicket(state) {
98
+ const phases = state.roadmap.phases;
99
+ if (phases.length === 0 || state.leafTickets.length === 0) {
100
+ return { kind: "empty_project" };
101
+ }
102
+ let allPhasesComplete = true;
103
+ for (const phase of phases) {
104
+ const leaves = state.phaseTickets(phase.id);
105
+ if (leaves.length === 0) continue;
106
+ const status = state.phaseStatus(phase.id);
107
+ if (status === "complete") continue;
108
+ allPhasesComplete = false;
109
+ const incompleteLeaves = leaves.filter((t) => t.status !== "complete");
110
+ const candidate = incompleteLeaves.find((t) => !state.isBlocked(t));
111
+ if (candidate) {
112
+ const impact = ticketsUnblockedBy(candidate.id, state);
113
+ const progress = candidate.parentTicket ? umbrellaProgress(candidate.parentTicket, state) : null;
114
+ return {
115
+ kind: "found",
116
+ ticket: candidate,
117
+ unblockImpact: { ticketId: candidate.id, wouldUnblock: impact },
118
+ umbrellaProgress: progress
119
+ };
120
+ }
121
+ return {
122
+ kind: "all_blocked",
123
+ phaseId: phase.id,
124
+ blockedCount: incompleteLeaves.length
125
+ };
126
+ }
127
+ if (allPhasesComplete) {
128
+ return { kind: "all_complete" };
129
+ }
130
+ return { kind: "empty_project" };
131
+ }
132
+ function blockedTickets(state) {
133
+ return state.leafTickets.filter(
134
+ (t) => t.status !== "complete" && state.isBlocked(t)
135
+ );
136
+ }
137
+ function ticketsUnblockedBy(ticketId, state) {
138
+ const blocked = state.reverseBlocks(ticketId);
139
+ return blocked.filter((t) => {
140
+ if (t.status === "complete") return false;
141
+ return t.blockedBy.every((bid) => {
142
+ if (bid === ticketId) return true;
143
+ const blocker = state.ticketByID(bid);
144
+ if (!blocker) return false;
145
+ return blocker.status === "complete";
146
+ });
147
+ });
148
+ }
149
+ function umbrellaProgress(ticketId, state) {
150
+ if (!state.umbrellaIDs.has(ticketId)) return null;
151
+ const leaves = collectDescendantLeaves(ticketId, state, /* @__PURE__ */ new Set());
152
+ const complete = leaves.filter((t) => t.status === "complete").length;
153
+ return {
154
+ total: leaves.length,
155
+ complete,
156
+ status: state.umbrellaStatus(ticketId)
157
+ };
158
+ }
159
+ function currentPhase(state) {
160
+ for (const phase of state.roadmap.phases) {
161
+ const leaves = state.phaseTickets(phase.id);
162
+ if (leaves.length === 0) continue;
163
+ if (state.phaseStatus(phase.id) !== "complete") return phase;
164
+ }
165
+ return null;
166
+ }
167
+ function phasesWithStatus(state) {
168
+ return state.roadmap.phases.map((phase) => ({
169
+ phase,
170
+ status: state.phaseStatus(phase.id),
171
+ leafCount: state.phaseTickets(phase.id).length
172
+ }));
173
+ }
174
+ function isBlockerCleared(blocker) {
175
+ if (blocker.cleared === true) return true;
176
+ if (blocker.clearedDate != null) return true;
177
+ return false;
178
+ }
179
+ function collectDescendantLeaves(ticketId, state, visited) {
180
+ if (visited.has(ticketId)) return [];
181
+ visited.add(ticketId);
182
+ const children = state.umbrellaChildren(ticketId);
183
+ const leaves = [];
184
+ for (const child of children) {
185
+ if (state.umbrellaIDs.has(child.id)) {
186
+ leaves.push(...collectDescendantLeaves(child.id, state, visited));
187
+ } else {
188
+ leaves.push(child);
189
+ }
190
+ }
191
+ return leaves;
192
+ }
193
+
194
+ // src/core/output-formatter.ts
195
+ var ExitCode = {
196
+ OK: 0,
197
+ USER_ERROR: 1,
198
+ VALIDATION_ERROR: 2,
199
+ PARTIAL: 3
200
+ };
201
+ function successEnvelope(data) {
202
+ return { version: 1, data };
203
+ }
204
+ function errorEnvelope(code, message) {
205
+ return { version: 1, error: { code, message } };
206
+ }
207
+ function escapeMarkdownInline(text) {
208
+ return text.replace(/\\/g, "\\\\").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/([`*_~\[\]()|])/g, "\\$1").replace(/(^|\n)([#\-+*])/g, "$1\\$2").replace(/(^|\n)(\d+)\./g, "$1$2\\.");
209
+ }
210
+ function fencedBlock(content, lang) {
211
+ let maxTicks = 2;
212
+ const matches = content.match(/`+/g);
213
+ if (matches) {
214
+ for (const m of matches) {
215
+ if (m.length > maxTicks) maxTicks = m.length;
216
+ }
217
+ }
218
+ const fence = "`".repeat(maxTicks + 1);
219
+ return `${fence}${lang ?? ""}
220
+ ${content}
221
+ ${fence}`;
222
+ }
223
+ function formatStatus(state, format) {
224
+ const phases = phasesWithStatus(state);
225
+ const data = {
226
+ project: state.config.project,
227
+ totalTickets: state.totalTicketCount,
228
+ completeTickets: state.completeTicketCount,
229
+ openTickets: state.openTicketCount,
230
+ blockedTickets: state.blockedCount,
231
+ openIssues: state.openIssueCount,
232
+ handovers: state.handoverFilenames.length,
233
+ phases: phases.map((p) => ({
234
+ id: p.phase.id,
235
+ name: p.phase.name,
236
+ status: p.status,
237
+ leafCount: p.leafCount
238
+ }))
239
+ };
240
+ if (format === "json") {
241
+ return JSON.stringify(successEnvelope(data), null, 2);
242
+ }
243
+ const lines = [
244
+ `# ${escapeMarkdownInline(state.config.project)}`,
245
+ "",
246
+ `Tickets: ${state.completeTicketCount}/${state.totalTicketCount} complete, ${state.blockedCount} blocked`,
247
+ `Issues: ${state.openIssueCount} open`,
248
+ `Handovers: ${state.handoverFilenames.length}`,
249
+ "",
250
+ "## Phases",
251
+ ""
252
+ ];
253
+ for (const p of phases) {
254
+ const indicator = p.status === "complete" ? "[x]" : p.status === "inprogress" ? "[~]" : "[ ]";
255
+ const summary = p.phase.summary ?? truncate(p.phase.description, 80);
256
+ lines.push(`${indicator} **${escapeMarkdownInline(p.phase.name)}** (${p.leafCount} tickets) \u2014 ${escapeMarkdownInline(summary)}`);
257
+ }
258
+ return lines.join("\n");
259
+ }
260
+ function formatPhaseList(state, format) {
261
+ const phases = phasesWithStatus(state);
262
+ const data = phases.map((p) => ({
263
+ id: p.phase.id,
264
+ label: p.phase.label,
265
+ name: p.phase.name,
266
+ description: p.phase.summary ?? p.phase.description,
267
+ status: p.status,
268
+ leafCount: p.leafCount
269
+ }));
270
+ if (format === "json") {
271
+ return JSON.stringify(successEnvelope(data), null, 2);
272
+ }
273
+ const lines = [];
274
+ for (const p of data) {
275
+ const indicator = p.status === "complete" ? "[x]" : p.status === "inprogress" ? "[~]" : "[ ]";
276
+ lines.push(`${indicator} **${escapeMarkdownInline(p.name)}** (${p.id}) \u2014 ${p.leafCount} tickets \u2014 ${escapeMarkdownInline(truncate(p.description, 80))}`);
277
+ }
278
+ return lines.join("\n");
279
+ }
280
+ function formatPhaseTickets(phaseId, state, format) {
281
+ const tickets = state.phaseTickets(phaseId);
282
+ if (format === "json") {
283
+ return JSON.stringify(successEnvelope(tickets), null, 2);
284
+ }
285
+ if (tickets.length === 0) return "No tickets in this phase.";
286
+ return tickets.map((t) => formatTicketOneLiner(t, state)).join("\n");
287
+ }
288
+ function formatTicket(ticket, state, format) {
289
+ if (format === "json") {
290
+ return JSON.stringify(successEnvelope(ticket), null, 2);
291
+ }
292
+ const blocked = state.isBlocked(ticket) ? " [BLOCKED]" : "";
293
+ const lines = [
294
+ `# ${escapeMarkdownInline(ticket.id)}: ${escapeMarkdownInline(ticket.title)}${blocked}`,
295
+ "",
296
+ `Status: ${ticket.status} | Type: ${ticket.type} | Phase: ${ticket.phase ?? "none"} | Order: ${ticket.order}`,
297
+ `Created: ${ticket.createdDate}${ticket.completedDate ? ` | Completed: ${ticket.completedDate}` : ""}`
298
+ ];
299
+ if (ticket.blockedBy.length > 0) {
300
+ lines.push(`Blocked by: ${ticket.blockedBy.join(", ")}`);
301
+ }
302
+ if (ticket.parentTicket) {
303
+ lines.push(`Parent: ${ticket.parentTicket}`);
304
+ }
305
+ if (ticket.description) {
306
+ lines.push("", "## Description", "", fencedBlock(ticket.description));
307
+ }
308
+ return lines.join("\n");
309
+ }
310
+ function formatNextTicketOutcome(outcome, state, format) {
311
+ if (format === "json") {
312
+ return JSON.stringify(successEnvelope(outcome), null, 2);
313
+ }
314
+ switch (outcome.kind) {
315
+ case "empty_project":
316
+ return "No phased tickets found.";
317
+ case "all_complete":
318
+ return "All phases complete.";
319
+ case "all_blocked": {
320
+ return `All ${outcome.blockedCount} incomplete tickets in phase "${escapeMarkdownInline(outcome.phaseId)}" are blocked.`;
321
+ }
322
+ case "found": {
323
+ const t = outcome.ticket;
324
+ const lines = [
325
+ `# Next: ${escapeMarkdownInline(t.id)} \u2014 ${escapeMarkdownInline(t.title)}`,
326
+ "",
327
+ `Phase: ${t.phase ?? "none"} | Order: ${t.order} | Type: ${t.type}`
328
+ ];
329
+ if (outcome.unblockImpact.wouldUnblock.length > 0) {
330
+ const ids = outcome.unblockImpact.wouldUnblock.map((u) => u.id).join(", ");
331
+ lines.push(`Completing this unblocks: ${ids}`);
332
+ }
333
+ if (outcome.umbrellaProgress) {
334
+ const p = outcome.umbrellaProgress;
335
+ lines.push(`Parent progress: ${p.complete}/${p.total} complete (${p.status})`);
336
+ }
337
+ if (t.description) {
338
+ lines.push("", fencedBlock(t.description));
339
+ }
340
+ return lines.join("\n");
341
+ }
342
+ }
343
+ }
344
+ function formatTicketList(tickets, format) {
345
+ if (format === "json") {
346
+ return JSON.stringify(successEnvelope(tickets), null, 2);
347
+ }
348
+ if (tickets.length === 0) return "No tickets found.";
349
+ const lines = [];
350
+ for (const t of tickets) {
351
+ const status = t.status === "complete" ? "[x]" : t.status === "inprogress" ? "[~]" : "[ ]";
352
+ lines.push(`${status} ${t.id}: ${escapeMarkdownInline(t.title)} (${t.phase ?? "none"})`);
353
+ }
354
+ return lines.join("\n");
355
+ }
356
+ function formatIssue(issue, format) {
357
+ if (format === "json") {
358
+ return JSON.stringify(successEnvelope(issue), null, 2);
359
+ }
360
+ const lines = [
361
+ `# ${escapeMarkdownInline(issue.id)}: ${escapeMarkdownInline(issue.title)}`,
362
+ "",
363
+ `Status: ${issue.status} | Severity: ${issue.severity}`,
364
+ `Components: ${issue.components.join(", ") || "none"}`,
365
+ `Discovered: ${issue.discoveredDate}${issue.resolvedDate ? ` | Resolved: ${issue.resolvedDate}` : ""}`
366
+ ];
367
+ if (issue.relatedTickets.length > 0) {
368
+ lines.push(`Related: ${issue.relatedTickets.join(", ")}`);
369
+ }
370
+ lines.push("", "## Impact", "", fencedBlock(issue.impact));
371
+ if (issue.resolution) {
372
+ lines.push("", "## Resolution", "", fencedBlock(issue.resolution));
373
+ }
374
+ return lines.join("\n");
375
+ }
376
+ function formatIssueList(issues, format) {
377
+ if (format === "json") {
378
+ return JSON.stringify(successEnvelope(issues), null, 2);
379
+ }
380
+ if (issues.length === 0) return "No issues found.";
381
+ const lines = [];
382
+ for (const i of issues) {
383
+ const status = i.status === "resolved" ? "[x]" : "[ ]";
384
+ lines.push(`${status} ${i.id} [${i.severity}]: ${escapeMarkdownInline(i.title)}`);
385
+ }
386
+ return lines.join("\n");
387
+ }
388
+ function formatBlockedTickets(tickets, state, format) {
389
+ if (format === "json") {
390
+ return JSON.stringify(
391
+ successEnvelope(
392
+ tickets.map((t) => ({
393
+ ...t,
394
+ blockers: t.blockedBy.map((bid) => ({
395
+ id: bid,
396
+ status: state.ticketByID(bid)?.status ?? "unknown"
397
+ }))
398
+ }))
399
+ ),
400
+ null,
401
+ 2
402
+ );
403
+ }
404
+ if (tickets.length === 0) return "No blocked tickets.";
405
+ const lines = [];
406
+ for (const t of tickets) {
407
+ const blockerInfo = t.blockedBy.map((bid) => {
408
+ const b = state.ticketByID(bid);
409
+ return b ? `${bid} (${b.status})` : `${bid} (unknown)`;
410
+ }).join(", ");
411
+ lines.push(`${t.id}: ${escapeMarkdownInline(t.title)} \u2014 blocked by: ${blockerInfo}`);
412
+ }
413
+ return lines.join("\n");
414
+ }
415
+ function formatValidation(result, format) {
416
+ if (format === "json") {
417
+ return JSON.stringify(successEnvelope(result), null, 2);
418
+ }
419
+ const lines = [
420
+ result.valid ? "Validation passed." : "Validation failed.",
421
+ `Errors: ${result.errorCount} | Warnings: ${result.warningCount} | Info: ${result.infoCount}`
422
+ ];
423
+ if (result.findings.length > 0) {
424
+ lines.push("");
425
+ for (const f of result.findings) {
426
+ const prefix = f.level === "error" ? "ERROR" : f.level === "warning" ? "WARN" : "INFO";
427
+ const entity = f.entity ? `[${escapeMarkdownInline(f.entity)}] ` : "";
428
+ lines.push(`${prefix}: ${entity}${escapeMarkdownInline(f.message)}`);
429
+ }
430
+ }
431
+ return lines.join("\n");
432
+ }
433
+ function formatBlockerList(roadmap, format) {
434
+ if (format === "json") {
435
+ return JSON.stringify(
436
+ successEnvelope(
437
+ roadmap.blockers.map((b) => ({
438
+ name: b.name,
439
+ cleared: isBlockerCleared(b),
440
+ note: b.note ?? null,
441
+ createdDate: b.createdDate ?? null,
442
+ clearedDate: b.clearedDate ?? null
443
+ }))
444
+ ),
445
+ null,
446
+ 2
447
+ );
448
+ }
449
+ if (roadmap.blockers.length === 0) return "No blockers.";
450
+ const lines = [];
451
+ for (const b of roadmap.blockers) {
452
+ const status = isBlockerCleared(b) ? "[x]" : "[ ]";
453
+ const note = b.note ? ` \u2014 ${escapeMarkdownInline(b.note)}` : "";
454
+ lines.push(`${status} ${escapeMarkdownInline(b.name)}${note}`);
455
+ }
456
+ return lines.join("\n");
457
+ }
458
+ function formatError(code, message, format) {
459
+ if (format === "json") {
460
+ return JSON.stringify(errorEnvelope(code, message), null, 2);
461
+ }
462
+ return `Error [${code}]: ${escapeMarkdownInline(message)}`;
463
+ }
464
+ function formatInitResult(result, format) {
465
+ if (format === "json") {
466
+ return JSON.stringify(successEnvelope(result), null, 2);
467
+ }
468
+ return [`Initialized .story/ at ${escapeMarkdownInline(result.root)}`, "", ...result.created.map((f) => ` ${f}`)].join("\n");
469
+ }
470
+ function formatHandoverList(filenames, format) {
471
+ if (format === "json") {
472
+ return JSON.stringify(successEnvelope(filenames), null, 2);
473
+ }
474
+ if (filenames.length === 0) return "No handovers found.";
475
+ return filenames.join("\n");
476
+ }
477
+ function formatHandoverContent(filename, content, format) {
478
+ if (format === "json") {
479
+ return JSON.stringify(successEnvelope({ filename, content }), null, 2);
480
+ }
481
+ return content;
482
+ }
483
+ function truncate(text, maxLen) {
484
+ if (text.length <= maxLen) return text;
485
+ return text.slice(0, maxLen - 3) + "...";
486
+ }
487
+ function formatTicketOneLiner(t, state) {
488
+ const status = t.status === "complete" ? "[x]" : t.status === "inprogress" ? "[~]" : "[ ]";
489
+ const blocked = state.isBlocked(t) ? " [BLOCKED]" : "";
490
+ return `${status} ${t.id}: ${escapeMarkdownInline(t.title)}${blocked}`;
491
+ }
492
+
493
+ // src/cli/run.ts
494
+ init_esm_shims();
495
+ import { join as join5 } from "path";
496
+
497
+ // src/core/index.ts
498
+ init_esm_shims();
499
+
500
+ // src/core/project-state.ts
501
+ init_esm_shims();
502
+ var ProjectState = class _ProjectState {
503
+ // --- Public raw inputs (readonly) ---
504
+ tickets;
505
+ issues;
506
+ roadmap;
507
+ config;
508
+ handoverFilenames;
509
+ // --- Derived (public readonly) ---
510
+ umbrellaIDs;
511
+ leafTickets;
512
+ // --- Derived (private) ---
513
+ leafTicketsByPhase;
514
+ childrenByParent;
515
+ reverseBlocksMap;
516
+ ticketsByID;
517
+ issuesByID;
518
+ // --- Counts ---
519
+ totalTicketCount;
520
+ openTicketCount;
521
+ completeTicketCount;
522
+ openIssueCount;
523
+ issuesBySeverity;
524
+ constructor(input) {
525
+ this.tickets = input.tickets;
526
+ this.issues = input.issues;
527
+ this.roadmap = input.roadmap;
528
+ this.config = input.config;
529
+ this.handoverFilenames = input.handoverFilenames;
530
+ const parentIDs = /* @__PURE__ */ new Set();
531
+ for (const t of input.tickets) {
532
+ if (t.parentTicket != null) {
533
+ parentIDs.add(t.parentTicket);
534
+ }
535
+ }
536
+ this.umbrellaIDs = parentIDs;
537
+ this.leafTickets = input.tickets.filter((t) => !parentIDs.has(t.id));
538
+ const byPhase = /* @__PURE__ */ new Map();
539
+ for (const t of this.leafTickets) {
540
+ const phase = t.phase;
541
+ const arr = byPhase.get(phase);
542
+ if (arr) {
543
+ arr.push(t);
544
+ } else {
545
+ byPhase.set(phase, [t]);
546
+ }
547
+ }
548
+ for (const [, arr] of byPhase) {
549
+ arr.sort((a, b) => a.order - b.order);
550
+ }
551
+ this.leafTicketsByPhase = byPhase;
552
+ const children = /* @__PURE__ */ new Map();
553
+ for (const t of input.tickets) {
554
+ if (t.parentTicket != null) {
555
+ const arr = children.get(t.parentTicket);
556
+ if (arr) {
557
+ arr.push(t);
558
+ } else {
559
+ children.set(t.parentTicket, [t]);
560
+ }
561
+ }
562
+ }
563
+ this.childrenByParent = children;
564
+ const reverseBlocks = /* @__PURE__ */ new Map();
565
+ for (const t of input.tickets) {
566
+ for (const blockerID of t.blockedBy) {
567
+ const arr = reverseBlocks.get(blockerID);
568
+ if (arr) {
569
+ arr.push(t);
570
+ } else {
571
+ reverseBlocks.set(blockerID, [t]);
572
+ }
573
+ }
574
+ }
575
+ this.reverseBlocksMap = reverseBlocks;
576
+ const tByID = /* @__PURE__ */ new Map();
577
+ for (const t of input.tickets) {
578
+ if (!tByID.has(t.id)) {
579
+ tByID.set(t.id, t);
580
+ }
581
+ }
582
+ this.ticketsByID = tByID;
583
+ const iByID = /* @__PURE__ */ new Map();
584
+ for (const i of input.issues) {
585
+ iByID.set(i.id, i);
586
+ }
587
+ this.issuesByID = iByID;
588
+ this.totalTicketCount = input.tickets.length;
589
+ this.openTicketCount = input.tickets.filter(
590
+ (t) => t.status !== "complete"
591
+ ).length;
592
+ this.completeTicketCount = input.tickets.filter(
593
+ (t) => t.status === "complete"
594
+ ).length;
595
+ this.openIssueCount = input.issues.filter(
596
+ (i) => i.status === "open"
597
+ ).length;
598
+ const bySev = /* @__PURE__ */ new Map();
599
+ for (const i of input.issues) {
600
+ if (i.status === "open") {
601
+ bySev.set(i.severity, (bySev.get(i.severity) ?? 0) + 1);
602
+ }
603
+ }
604
+ this.issuesBySeverity = bySev;
605
+ }
606
+ // --- Query Methods ---
607
+ isUmbrella(ticket) {
608
+ return this.umbrellaIDs.has(ticket.id);
609
+ }
610
+ phaseTickets(phaseId) {
611
+ return this.leafTicketsByPhase.get(phaseId) ?? [];
612
+ }
613
+ /** Phase status derived from leaf tickets only. Umbrella stored status is ignored. */
614
+ phaseStatus(phaseId) {
615
+ const leaves = this.phaseTickets(phaseId);
616
+ return _ProjectState.aggregateStatus(leaves);
617
+ }
618
+ umbrellaChildren(ticketId) {
619
+ return this.childrenByParent.get(ticketId) ?? [];
620
+ }
621
+ /** Umbrella status derived from descendant leaf tickets (recursive traversal). */
622
+ umbrellaStatus(ticketId) {
623
+ const visited = /* @__PURE__ */ new Set();
624
+ const leaves = this.descendantLeaves(ticketId, visited);
625
+ return _ProjectState.aggregateStatus(leaves);
626
+ }
627
+ reverseBlocks(ticketId) {
628
+ return this.reverseBlocksMap.get(ticketId) ?? [];
629
+ }
630
+ /**
631
+ * A ticket is blocked if any blockedBy reference points to a non-complete ticket.
632
+ * Unknown blocker IDs treated as blocked (conservative — unknown dependency = assume not cleared).
633
+ */
634
+ isBlocked(ticket) {
635
+ if (ticket.blockedBy.length === 0) return false;
636
+ return ticket.blockedBy.some((blockerID) => {
637
+ const blocker = this.ticketsByID.get(blockerID);
638
+ if (!blocker) return true;
639
+ return blocker.status !== "complete";
640
+ });
641
+ }
642
+ get blockedCount() {
643
+ return this.tickets.filter((t) => this.isBlocked(t)).length;
644
+ }
645
+ ticketByID(id) {
646
+ return this.ticketsByID.get(id);
647
+ }
648
+ issueByID(id) {
649
+ return this.issuesByID.get(id);
650
+ }
651
+ // --- Deletion Safety ---
652
+ /** IDs of tickets that list `ticketId` in their blockedBy. */
653
+ ticketsBlocking(ticketId) {
654
+ return (this.reverseBlocksMap.get(ticketId) ?? []).map((t) => t.id);
655
+ }
656
+ /** IDs of tickets that have `ticketId` as their parentTicket. */
657
+ childrenOf(ticketId) {
658
+ return (this.childrenByParent.get(ticketId) ?? []).map((t) => t.id);
659
+ }
660
+ /** IDs of issues that reference `ticketId` in relatedTickets. */
661
+ issuesReferencing(ticketId) {
662
+ return this.issues.filter((i) => i.relatedTickets.includes(ticketId)).map((i) => i.id);
663
+ }
664
+ // --- Private ---
665
+ /**
666
+ * Recursively collects all descendant leaf tickets of an umbrella.
667
+ * Uses a visited set to guard against cycles in malformed data.
668
+ */
669
+ descendantLeaves(ticketId, visited) {
670
+ if (visited.has(ticketId)) return [];
671
+ visited.add(ticketId);
672
+ const directChildren = this.childrenByParent.get(ticketId) ?? [];
673
+ const leaves = [];
674
+ for (const child of directChildren) {
675
+ if (this.umbrellaIDs.has(child.id)) {
676
+ leaves.push(...this.descendantLeaves(child.id, visited));
677
+ } else {
678
+ leaves.push(child);
679
+ }
680
+ }
681
+ return leaves;
682
+ }
683
+ /**
684
+ * Shared aggregation logic for phase and umbrella status.
685
+ * - all complete → complete
686
+ * - any inprogress OR any complete (but not all) → inprogress
687
+ * - else → notstarted (nothing started)
688
+ */
689
+ static aggregateStatus(tickets) {
690
+ if (tickets.length === 0) return "notstarted";
691
+ const allComplete = tickets.every((t) => t.status === "complete");
692
+ if (allComplete) return "complete";
693
+ const anyProgress = tickets.some((t) => t.status === "inprogress");
694
+ const anyComplete = tickets.some((t) => t.status === "complete");
695
+ if (anyProgress || anyComplete) return "inprogress";
696
+ return "notstarted";
697
+ }
698
+ };
699
+
700
+ // src/core/project-loader.ts
701
+ init_esm_shims();
702
+ import {
703
+ readdir as readdir2,
704
+ readFile as readFile2,
705
+ writeFile,
706
+ rename,
707
+ unlink,
708
+ stat,
709
+ realpath,
710
+ lstat,
711
+ open
712
+ } from "fs/promises";
713
+ import { existsSync as existsSync2 } from "fs";
714
+ import { join as join2, resolve, relative as relative2, extname as extname2, dirname, basename } from "path";
715
+ import lockfile from "proper-lockfile";
716
+
717
+ // src/models/ticket.ts
718
+ init_esm_shims();
719
+ import { z as z2 } from "zod";
720
+
721
+ // src/models/types.ts
722
+ init_esm_shims();
723
+ import { z } from "zod";
724
+ var TICKET_ID_REGEX = /^T-\d+[a-z]?$/;
725
+ var ISSUE_ID_REGEX = /^ISS-\d+$/;
726
+ var TICKET_STATUSES = ["open", "inprogress", "complete"];
727
+ var TICKET_TYPES = ["task", "feature", "chore"];
728
+ var ISSUE_STATUSES = ["open", "inprogress", "resolved"];
729
+ var ISSUE_SEVERITIES = ["critical", "high", "medium", "low"];
730
+ var OUTPUT_FORMATS = ["json", "md"];
731
+ var DATE_REGEX = /^\d{4}-\d{2}-\d{2}$/;
732
+ var DateSchema = z.string().regex(DATE_REGEX, "Date must be YYYY-MM-DD").refine(
733
+ (val) => {
734
+ const d = /* @__PURE__ */ new Date(val + "T00:00:00Z");
735
+ return !isNaN(d.getTime()) && d.toISOString().startsWith(val);
736
+ },
737
+ { message: "Invalid calendar date" }
738
+ );
739
+ var TicketIdSchema = z.string().regex(TICKET_ID_REGEX, "Ticket ID must match T-NNN or T-NNNx");
740
+ var IssueIdSchema = z.string().regex(ISSUE_ID_REGEX, "Issue ID must match ISS-NNN");
741
+
742
+ // src/models/ticket.ts
743
+ var TicketSchema = z2.object({
744
+ id: TicketIdSchema,
745
+ title: z2.string().min(1),
746
+ description: z2.string(),
747
+ type: z2.enum(TICKET_TYPES),
748
+ status: z2.enum(TICKET_STATUSES),
749
+ phase: z2.string().nullable(),
750
+ order: z2.number().int(),
751
+ createdDate: DateSchema,
752
+ completedDate: DateSchema.nullable(),
753
+ blockedBy: z2.array(TicketIdSchema),
754
+ parentTicket: TicketIdSchema.nullable().optional(),
755
+ // Attribution fields — unused in v1, baked in to avoid future migration
756
+ createdBy: z2.string().nullable().optional(),
757
+ assignedTo: z2.string().nullable().optional(),
758
+ lastModifiedBy: z2.string().nullable().optional()
759
+ }).passthrough();
760
+
761
+ // src/models/issue.ts
762
+ init_esm_shims();
763
+ import { z as z3 } from "zod";
764
+ var IssueSchema = z3.object({
765
+ id: IssueIdSchema,
766
+ title: z3.string().min(1),
767
+ status: z3.enum(ISSUE_STATUSES),
768
+ severity: z3.enum(ISSUE_SEVERITIES),
769
+ components: z3.array(z3.string()),
770
+ impact: z3.string(),
771
+ resolution: z3.string().nullable(),
772
+ location: z3.array(z3.string()),
773
+ discoveredDate: DateSchema,
774
+ resolvedDate: DateSchema.nullable(),
775
+ relatedTickets: z3.array(TicketIdSchema),
776
+ // Optional fields — older issues may omit these
777
+ order: z3.number().int().optional(),
778
+ phase: z3.string().nullable().optional(),
779
+ // Attribution fields — unused in v1
780
+ createdBy: z3.string().nullable().optional(),
781
+ assignedTo: z3.string().nullable().optional(),
782
+ lastModifiedBy: z3.string().nullable().optional()
783
+ }).passthrough();
784
+
785
+ // src/models/roadmap.ts
786
+ init_esm_shims();
787
+ import { z as z4 } from "zod";
788
+ var BlockerSchema = z4.object({
789
+ name: z4.string().min(1),
790
+ // Legacy format (pre-T-082)
791
+ cleared: z4.boolean().optional(),
792
+ // New date-based format (T-082 migration)
793
+ createdDate: DateSchema.optional(),
794
+ clearedDate: DateSchema.nullable().optional(),
795
+ // Present in all current data but optional for future minimal blockers
796
+ note: z4.string().nullable().optional()
797
+ }).passthrough();
798
+ var PhaseSchema = z4.object({
799
+ id: z4.string().min(1),
800
+ label: z4.string(),
801
+ name: z4.string(),
802
+ description: z4.string(),
803
+ summary: z4.string().optional()
804
+ }).passthrough();
805
+ var RoadmapSchema = z4.object({
806
+ title: z4.string(),
807
+ date: DateSchema,
808
+ phases: z4.array(PhaseSchema),
809
+ blockers: z4.array(BlockerSchema)
810
+ }).passthrough();
811
+
812
+ // src/models/config.ts
813
+ init_esm_shims();
814
+ import { z as z5 } from "zod";
815
+ var FeaturesSchema = z5.object({
816
+ tickets: z5.boolean(),
817
+ issues: z5.boolean(),
818
+ handovers: z5.boolean(),
819
+ roadmap: z5.boolean(),
820
+ reviews: z5.boolean()
821
+ }).passthrough();
822
+ var ConfigSchema = z5.object({
823
+ version: z5.number().int().min(1),
824
+ schemaVersion: z5.number().int().optional(),
825
+ project: z5.string().min(1),
826
+ type: z5.string(),
827
+ language: z5.string(),
828
+ features: FeaturesSchema
829
+ }).passthrough();
830
+
831
+ // src/core/project-loader.ts
832
+ init_errors();
833
+
834
+ // src/core/handover-parser.ts
835
+ init_esm_shims();
836
+ import { readdir, readFile } from "fs/promises";
837
+ import { existsSync } from "fs";
838
+ import { join, relative, extname } from "path";
839
+ var HANDOVER_DATE_REGEX = /^\d{4}-\d{2}-\d{2}/;
840
+ async function listHandovers(handoversDir, root, warnings) {
841
+ if (!existsSync(handoversDir)) return [];
842
+ let entries;
843
+ try {
844
+ entries = await readdir(handoversDir);
845
+ } catch (err) {
846
+ warnings.push({
847
+ file: relative(root, handoversDir),
848
+ message: `Cannot enumerate handovers: ${err instanceof Error ? err.message : String(err)}`,
849
+ type: "parse_error"
850
+ });
851
+ return [];
852
+ }
853
+ const conforming = [];
854
+ const nonConforming = [];
855
+ for (const entry of entries.sort()) {
856
+ if (entry.startsWith(".")) continue;
857
+ if (extname(entry) !== ".md") continue;
858
+ if (HANDOVER_DATE_REGEX.test(entry)) {
859
+ conforming.push(entry);
860
+ } else {
861
+ nonConforming.push(entry);
862
+ warnings.push({
863
+ file: relative(root, join(handoversDir, entry)),
864
+ message: "Handover filename does not start with YYYY-MM-DD date prefix.",
865
+ type: "naming_convention"
866
+ });
867
+ }
868
+ }
869
+ conforming.sort((a, b) => b.localeCompare(a));
870
+ return [...conforming, ...nonConforming];
871
+ }
872
+ async function readHandover(handoversDir, filename) {
873
+ return readFile(join(handoversDir, filename), "utf-8");
874
+ }
875
+
876
+ // src/core/project-loader.ts
877
+ async function loadProject(root, options) {
878
+ const absRoot = resolve(root);
879
+ const wrapDir = join2(absRoot, ".story");
880
+ try {
881
+ const wrapStat = await stat(wrapDir);
882
+ if (!wrapStat.isDirectory()) {
883
+ throw new ProjectLoaderError(
884
+ "not_found",
885
+ "Missing .story/ directory."
886
+ );
887
+ }
888
+ } catch (err) {
889
+ if (err instanceof ProjectLoaderError) throw err;
890
+ throw new ProjectLoaderError(
891
+ "not_found",
892
+ "Missing .story/ directory."
893
+ );
894
+ }
895
+ if (existsSync2(join2(wrapDir, ".txn.json"))) {
896
+ await withLock(wrapDir, () => doRecoverTransaction(wrapDir));
897
+ }
898
+ const config = await loadSingletonFile(
899
+ "config.json",
900
+ wrapDir,
901
+ absRoot,
902
+ ConfigSchema
903
+ );
904
+ const maxVersion = options?.maxSchemaVersion ?? CURRENT_SCHEMA_VERSION;
905
+ if (config.schemaVersion !== void 0 && config.schemaVersion > maxVersion) {
906
+ throw new ProjectLoaderError(
907
+ "version_mismatch",
908
+ `Config schemaVersion ${config.schemaVersion} exceeds max supported ${maxVersion}.`
909
+ );
910
+ }
911
+ const roadmap = await loadSingletonFile(
912
+ "roadmap.json",
913
+ wrapDir,
914
+ absRoot,
915
+ RoadmapSchema
916
+ );
917
+ const warnings = [];
918
+ const tickets = await loadDirectory(
919
+ join2(wrapDir, "tickets"),
920
+ absRoot,
921
+ TicketSchema,
922
+ warnings
923
+ );
924
+ const issues = await loadDirectory(
925
+ join2(wrapDir, "issues"),
926
+ absRoot,
927
+ IssueSchema,
928
+ warnings
929
+ );
930
+ const handoversDir = join2(wrapDir, "handovers");
931
+ const handoverFilenames = await listHandovers(
932
+ handoversDir,
933
+ absRoot,
934
+ warnings
935
+ );
936
+ if (options?.strict) {
937
+ const integrityWarning = warnings.find(
938
+ (w) => INTEGRITY_WARNING_TYPES.includes(w.type)
939
+ );
940
+ if (integrityWarning) {
941
+ throw new ProjectLoaderError(
942
+ "project_corrupt",
943
+ `Strict mode: ${integrityWarning.file}: ${integrityWarning.message}`
944
+ );
945
+ }
946
+ }
947
+ const state = new ProjectState({
948
+ tickets,
949
+ issues,
950
+ roadmap,
951
+ config,
952
+ handoverFilenames
953
+ });
954
+ return { state, warnings };
955
+ }
956
+ async function writeTicketUnlocked(ticket, root) {
957
+ const parsed = TicketSchema.parse(ticket);
958
+ if (!TICKET_ID_REGEX.test(parsed.id)) {
959
+ throw new ProjectLoaderError(
960
+ "invalid_input",
961
+ `Invalid ticket ID: ${parsed.id}`
962
+ );
963
+ }
964
+ const wrapDir = resolve(root, ".story");
965
+ const targetPath = join2(wrapDir, "tickets", `${parsed.id}.json`);
966
+ await guardPath(targetPath, wrapDir);
967
+ const json = serializeJSON(parsed);
968
+ await atomicWrite(targetPath, json);
969
+ }
970
+ async function writeIssueUnlocked(issue, root) {
971
+ const parsed = IssueSchema.parse(issue);
972
+ if (!ISSUE_ID_REGEX.test(parsed.id)) {
973
+ throw new ProjectLoaderError(
974
+ "invalid_input",
975
+ `Invalid issue ID: ${parsed.id}`
976
+ );
977
+ }
978
+ const wrapDir = resolve(root, ".story");
979
+ const targetPath = join2(wrapDir, "issues", `${parsed.id}.json`);
980
+ await guardPath(targetPath, wrapDir);
981
+ const json = serializeJSON(parsed);
982
+ await atomicWrite(targetPath, json);
983
+ }
984
+ async function writeRoadmapUnlocked(roadmap, root) {
985
+ const parsed = RoadmapSchema.parse(roadmap);
986
+ const wrapDir = resolve(root, ".story");
987
+ const targetPath = join2(wrapDir, "roadmap.json");
988
+ await guardPath(targetPath, wrapDir);
989
+ const json = serializeJSON(parsed);
990
+ await atomicWrite(targetPath, json);
991
+ }
992
+ async function writeRoadmap(roadmap, root) {
993
+ const wrapDir = resolve(root, ".story");
994
+ await withLock(wrapDir, async () => {
995
+ await writeRoadmapUnlocked(roadmap, root);
996
+ });
997
+ }
998
+ async function writeConfig(config, root) {
999
+ const parsed = ConfigSchema.parse(config);
1000
+ const wrapDir = resolve(root, ".story");
1001
+ const targetPath = join2(wrapDir, "config.json");
1002
+ await guardPath(targetPath, wrapDir);
1003
+ const json = serializeJSON(parsed);
1004
+ await withLock(wrapDir, async () => {
1005
+ await atomicWrite(targetPath, json);
1006
+ });
1007
+ }
1008
+ async function deleteTicket(id, root, options) {
1009
+ if (!TICKET_ID_REGEX.test(id)) {
1010
+ throw new ProjectLoaderError(
1011
+ "invalid_input",
1012
+ `Invalid ticket ID: ${id}`
1013
+ );
1014
+ }
1015
+ const wrapDir = resolve(root, ".story");
1016
+ const targetPath = join2(wrapDir, "tickets", `${id}.json`);
1017
+ await guardPath(targetPath, wrapDir);
1018
+ await withLock(wrapDir, async () => {
1019
+ if (!options?.force) {
1020
+ const { state } = await loadProjectUnlocked(resolve(root));
1021
+ const blocking = state.ticketsBlocking(id);
1022
+ if (blocking.length > 0) {
1023
+ throw new ProjectLoaderError(
1024
+ "conflict",
1025
+ `Cannot delete ${id}: referenced in blockedBy by ${blocking.join(", ")}`
1026
+ );
1027
+ }
1028
+ const children = state.childrenOf(id);
1029
+ if (children.length > 0) {
1030
+ throw new ProjectLoaderError(
1031
+ "conflict",
1032
+ `Cannot delete ${id}: has child tickets ${children.join(", ")}`
1033
+ );
1034
+ }
1035
+ const refs = state.issuesReferencing(id);
1036
+ if (refs.length > 0) {
1037
+ throw new ProjectLoaderError(
1038
+ "conflict",
1039
+ `Cannot delete ${id}: referenced by issues ${refs.join(", ")}`
1040
+ );
1041
+ }
1042
+ }
1043
+ try {
1044
+ await stat(targetPath);
1045
+ } catch {
1046
+ throw new ProjectLoaderError(
1047
+ "not_found",
1048
+ `Ticket file not found: tickets/${id}.json`
1049
+ );
1050
+ }
1051
+ await unlink(targetPath);
1052
+ });
1053
+ }
1054
+ async function deleteIssue(id, root) {
1055
+ if (!ISSUE_ID_REGEX.test(id)) {
1056
+ throw new ProjectLoaderError(
1057
+ "invalid_input",
1058
+ `Invalid issue ID: ${id}`
1059
+ );
1060
+ }
1061
+ const wrapDir = resolve(root, ".story");
1062
+ const targetPath = join2(wrapDir, "issues", `${id}.json`);
1063
+ await guardPath(targetPath, wrapDir);
1064
+ await withLock(wrapDir, async () => {
1065
+ try {
1066
+ await stat(targetPath);
1067
+ } catch {
1068
+ throw new ProjectLoaderError(
1069
+ "not_found",
1070
+ `Issue file not found: issues/${id}.json`
1071
+ );
1072
+ }
1073
+ await unlink(targetPath);
1074
+ });
1075
+ }
1076
+ async function withProjectLock(root, options, handler) {
1077
+ const absRoot = resolve(root);
1078
+ const wrapDir = join2(absRoot, ".story");
1079
+ await withLock(wrapDir, async () => {
1080
+ await doRecoverTransaction(wrapDir);
1081
+ const result = await loadProjectUnlocked(absRoot);
1082
+ const config = result.state.config;
1083
+ if (config.schemaVersion !== void 0 && config.schemaVersion > CURRENT_SCHEMA_VERSION) {
1084
+ throw new ProjectLoaderError(
1085
+ "version_mismatch",
1086
+ `Config schemaVersion ${config.schemaVersion} exceeds max supported ${CURRENT_SCHEMA_VERSION}.`
1087
+ );
1088
+ }
1089
+ if (options.strict) {
1090
+ const integrityWarning = result.warnings.find(
1091
+ (w) => INTEGRITY_WARNING_TYPES.includes(w.type)
1092
+ );
1093
+ if (integrityWarning) {
1094
+ throw new ProjectLoaderError(
1095
+ "project_corrupt",
1096
+ `Strict mode: ${integrityWarning.file}: ${integrityWarning.message}`
1097
+ );
1098
+ }
1099
+ }
1100
+ await handler(result);
1101
+ });
1102
+ }
1103
+ async function runTransactionUnlocked(root, operations) {
1104
+ const wrapDir = resolve(root, ".story");
1105
+ const journalPath = join2(wrapDir, ".txn.json");
1106
+ const entries = [];
1107
+ let commitStarted = false;
1108
+ try {
1109
+ for (const op of operations) {
1110
+ if (op.op === "write") {
1111
+ const tempPath = `${op.target}.${process.pid}.tmp`;
1112
+ entries.push({ op: "write", target: op.target, tempPath });
1113
+ } else {
1114
+ entries.push({ op: "delete", target: op.target });
1115
+ }
1116
+ }
1117
+ const journal = { entries, commitStarted: false };
1118
+ await fsyncWrite(journalPath, JSON.stringify(journal, null, 2));
1119
+ for (const op of operations) {
1120
+ if (op.op === "write") {
1121
+ const tempPath = `${op.target}.${process.pid}.tmp`;
1122
+ await fsyncWrite(tempPath, op.content);
1123
+ }
1124
+ }
1125
+ journal.commitStarted = true;
1126
+ await fsyncWrite(journalPath, JSON.stringify(journal, null, 2));
1127
+ commitStarted = true;
1128
+ for (const entry of entries) {
1129
+ if (entry.op === "write" && entry.tempPath) {
1130
+ await rename(entry.tempPath, entry.target);
1131
+ } else if (entry.op === "delete") {
1132
+ try {
1133
+ await unlink(entry.target);
1134
+ } catch {
1135
+ }
1136
+ }
1137
+ }
1138
+ await unlink(journalPath);
1139
+ } catch (err) {
1140
+ if (!commitStarted) {
1141
+ for (const entry of entries) {
1142
+ if (entry.tempPath) {
1143
+ try {
1144
+ await unlink(entry.tempPath);
1145
+ } catch {
1146
+ }
1147
+ }
1148
+ }
1149
+ try {
1150
+ await unlink(journalPath);
1151
+ } catch {
1152
+ }
1153
+ }
1154
+ if (err instanceof ProjectLoaderError) throw err;
1155
+ throw new ProjectLoaderError("io_error", "Transaction failed", err);
1156
+ }
1157
+ }
1158
+ async function doRecoverTransaction(wrapDir) {
1159
+ const journalPath = join2(wrapDir, ".txn.json");
1160
+ let entries;
1161
+ let commitStarted = false;
1162
+ try {
1163
+ const raw = await readFile2(journalPath, "utf-8");
1164
+ const parsed = JSON.parse(raw);
1165
+ if (Array.isArray(parsed)) {
1166
+ entries = parsed;
1167
+ commitStarted = true;
1168
+ } else if (parsed != null && typeof parsed === "object" && Array.isArray(parsed.entries) && typeof parsed.commitStarted === "boolean") {
1169
+ const journal = parsed;
1170
+ entries = journal.entries;
1171
+ commitStarted = journal.commitStarted;
1172
+ } else {
1173
+ try {
1174
+ await unlink(journalPath);
1175
+ } catch {
1176
+ }
1177
+ return;
1178
+ }
1179
+ } catch {
1180
+ try {
1181
+ await unlink(journalPath);
1182
+ } catch {
1183
+ }
1184
+ return;
1185
+ }
1186
+ if (!commitStarted) {
1187
+ for (const entry of entries) {
1188
+ if (entry.op === "write" && entry.tempPath && existsSync2(entry.tempPath)) {
1189
+ try {
1190
+ await unlink(entry.tempPath);
1191
+ } catch {
1192
+ }
1193
+ }
1194
+ }
1195
+ try {
1196
+ await unlink(journalPath);
1197
+ } catch {
1198
+ }
1199
+ return;
1200
+ }
1201
+ for (const entry of entries) {
1202
+ if (entry.op === "write" && entry.tempPath) {
1203
+ const tempExists = existsSync2(entry.tempPath);
1204
+ if (tempExists) {
1205
+ try {
1206
+ await rename(entry.tempPath, entry.target);
1207
+ } catch {
1208
+ }
1209
+ try {
1210
+ await unlink(entry.tempPath);
1211
+ } catch {
1212
+ }
1213
+ }
1214
+ } else if (entry.op === "delete") {
1215
+ try {
1216
+ await unlink(entry.target);
1217
+ } catch {
1218
+ }
1219
+ }
1220
+ }
1221
+ try {
1222
+ await unlink(journalPath);
1223
+ } catch {
1224
+ }
1225
+ }
1226
+ async function loadProjectUnlocked(absRoot) {
1227
+ const wrapDir = join2(absRoot, ".story");
1228
+ const config = await loadSingletonFile("config.json", wrapDir, absRoot, ConfigSchema);
1229
+ const roadmap = await loadSingletonFile("roadmap.json", wrapDir, absRoot, RoadmapSchema);
1230
+ const warnings = [];
1231
+ const tickets = await loadDirectory(join2(wrapDir, "tickets"), absRoot, TicketSchema, warnings);
1232
+ const issues = await loadDirectory(join2(wrapDir, "issues"), absRoot, IssueSchema, warnings);
1233
+ const handoverFilenames = await listHandovers(join2(wrapDir, "handovers"), absRoot, warnings);
1234
+ const state = new ProjectState({ tickets, issues, roadmap, config, handoverFilenames });
1235
+ return { state, warnings };
1236
+ }
1237
+ async function loadSingletonFile(filename, wrapDir, root, schema) {
1238
+ const filePath = join2(wrapDir, filename);
1239
+ const relPath = relative2(root, filePath);
1240
+ let raw;
1241
+ try {
1242
+ raw = await readFile2(filePath, "utf-8");
1243
+ } catch (err) {
1244
+ if (err.code === "ENOENT") {
1245
+ throw new ProjectLoaderError("not_found", `File not found: ${relPath}`);
1246
+ }
1247
+ throw new ProjectLoaderError(
1248
+ "io_error",
1249
+ `Cannot read file: ${relPath}`,
1250
+ err
1251
+ );
1252
+ }
1253
+ let parsed;
1254
+ try {
1255
+ parsed = JSON.parse(raw);
1256
+ } catch (err) {
1257
+ throw new ProjectLoaderError(
1258
+ "validation_failed",
1259
+ `Invalid JSON in ${relPath}`,
1260
+ err
1261
+ );
1262
+ }
1263
+ const result = schema.safeParse(parsed);
1264
+ if (!result.success) {
1265
+ throw new ProjectLoaderError(
1266
+ "validation_failed",
1267
+ `Validation failed for ${relPath}: ${result.error.issues.map((i) => i.message).join("; ")}`,
1268
+ result.error
1269
+ );
1270
+ }
1271
+ return result.data;
1272
+ }
1273
+ async function loadDirectory(dirPath, root, schema, warnings) {
1274
+ if (!existsSync2(dirPath)) return [];
1275
+ let entries;
1276
+ try {
1277
+ entries = await readdir2(dirPath);
1278
+ } catch (err) {
1279
+ throw new ProjectLoaderError(
1280
+ "io_error",
1281
+ `Cannot enumerate ${relative2(root, dirPath)}`,
1282
+ err
1283
+ );
1284
+ }
1285
+ entries.sort();
1286
+ const results = [];
1287
+ for (const entry of entries) {
1288
+ if (entry.startsWith(".")) continue;
1289
+ if (extname2(entry) !== ".json") continue;
1290
+ const filePath = join2(dirPath, entry);
1291
+ const relPath = relative2(root, filePath);
1292
+ try {
1293
+ const raw = await readFile2(filePath, "utf-8");
1294
+ const parsed = JSON.parse(raw);
1295
+ const result = schema.safeParse(parsed);
1296
+ if (!result.success) {
1297
+ warnings.push({
1298
+ file: relPath,
1299
+ message: result.error.issues.map((i) => i.message).join("; "),
1300
+ type: "schema_error"
1301
+ });
1302
+ continue;
1303
+ }
1304
+ results.push(result.data);
1305
+ } catch (err) {
1306
+ warnings.push({
1307
+ file: relPath,
1308
+ message: err instanceof Error ? err.message : String(err),
1309
+ type: "parse_error"
1310
+ });
1311
+ }
1312
+ }
1313
+ return results;
1314
+ }
1315
+ function sortKeysDeep(value) {
1316
+ if (value === null || value === void 0) return value;
1317
+ if (typeof value !== "object") return value;
1318
+ if (Array.isArray(value)) return value.map(sortKeysDeep);
1319
+ const obj = value;
1320
+ const sorted = {};
1321
+ for (const key of Object.keys(obj).sort()) {
1322
+ sorted[key] = sortKeysDeep(obj[key]);
1323
+ }
1324
+ return sorted;
1325
+ }
1326
+ function serializeJSON(obj) {
1327
+ return JSON.stringify(sortKeysDeep(obj), null, 2) + "\n";
1328
+ }
1329
+ async function atomicWrite(targetPath, content) {
1330
+ const tempPath = `${targetPath}.${process.pid}.tmp`;
1331
+ try {
1332
+ await writeFile(tempPath, content, "utf-8");
1333
+ await rename(tempPath, targetPath);
1334
+ } catch (err) {
1335
+ try {
1336
+ await unlink(tempPath);
1337
+ } catch {
1338
+ }
1339
+ throw new ProjectLoaderError(
1340
+ "io_error",
1341
+ `Failed to write ${basename(targetPath)}`,
1342
+ err
1343
+ );
1344
+ }
1345
+ }
1346
+ async function fsyncWrite(filePath, content) {
1347
+ const fh = await open(filePath, "w");
1348
+ try {
1349
+ await fh.writeFile(content, "utf-8");
1350
+ await fh.sync();
1351
+ } finally {
1352
+ await fh.close();
1353
+ }
1354
+ }
1355
+ async function guardPath(target, root) {
1356
+ let resolvedRoot;
1357
+ try {
1358
+ resolvedRoot = await realpath(root);
1359
+ } catch {
1360
+ throw new ProjectLoaderError(
1361
+ "invalid_input",
1362
+ `Cannot resolve project root: ${root}`
1363
+ );
1364
+ }
1365
+ const targetDir = dirname(target);
1366
+ let resolvedDir;
1367
+ try {
1368
+ resolvedDir = await realpath(targetDir);
1369
+ } catch {
1370
+ resolvedDir = targetDir;
1371
+ }
1372
+ if (!resolvedDir.startsWith(resolvedRoot)) {
1373
+ throw new ProjectLoaderError(
1374
+ "invalid_input",
1375
+ `Path ${target} resolves outside project root`
1376
+ );
1377
+ }
1378
+ if (existsSync2(target)) {
1379
+ try {
1380
+ const stats = await lstat(target);
1381
+ if (stats.isSymbolicLink()) {
1382
+ throw new ProjectLoaderError(
1383
+ "invalid_input",
1384
+ `Symlink target rejected: ${target}`
1385
+ );
1386
+ }
1387
+ } catch (err) {
1388
+ if (err instanceof ProjectLoaderError) throw err;
1389
+ }
1390
+ }
1391
+ }
1392
+ async function withLock(wrapDir, fn) {
1393
+ let release;
1394
+ try {
1395
+ release = await lockfile.lock(wrapDir, {
1396
+ retries: { retries: 3, minTimeout: 100, maxTimeout: 1e3 },
1397
+ stale: 1e4,
1398
+ lockfilePath: join2(wrapDir, ".lock")
1399
+ });
1400
+ } catch (err) {
1401
+ if (err instanceof ProjectLoaderError) throw err;
1402
+ throw new ProjectLoaderError(
1403
+ "io_error",
1404
+ `Lock acquisition failed for ${wrapDir}`,
1405
+ err
1406
+ );
1407
+ }
1408
+ try {
1409
+ return await fn();
1410
+ } finally {
1411
+ if (release) {
1412
+ try {
1413
+ await release();
1414
+ } catch {
1415
+ }
1416
+ }
1417
+ }
1418
+ }
1419
+
1420
+ // src/core/index.ts
1421
+ init_project_root_discovery();
1422
+ init_errors();
1423
+
1424
+ // src/core/id-allocation.ts
1425
+ init_esm_shims();
1426
+ var TICKET_NUMERIC_REGEX = /^T-(\d+)[a-z]?$/;
1427
+ var ISSUE_NUMERIC_REGEX = /^ISS-(\d+)$/;
1428
+ function nextTicketID(tickets) {
1429
+ let max = 0;
1430
+ for (const t of tickets) {
1431
+ if (!TICKET_ID_REGEX.test(t.id)) continue;
1432
+ const match = t.id.match(TICKET_NUMERIC_REGEX);
1433
+ if (match?.[1]) {
1434
+ const num = parseInt(match[1], 10);
1435
+ if (num > max) max = num;
1436
+ }
1437
+ }
1438
+ return `T-${String(max + 1).padStart(3, "0")}`;
1439
+ }
1440
+ function nextIssueID(issues) {
1441
+ let max = 0;
1442
+ for (const i of issues) {
1443
+ if (!ISSUE_ID_REGEX.test(i.id)) continue;
1444
+ const match = i.id.match(ISSUE_NUMERIC_REGEX);
1445
+ if (match?.[1]) {
1446
+ const num = parseInt(match[1], 10);
1447
+ if (num > max) max = num;
1448
+ }
1449
+ }
1450
+ return `ISS-${String(max + 1).padStart(3, "0")}`;
1451
+ }
1452
+ function nextOrder(phaseId, state) {
1453
+ const tickets = state.phaseTickets(phaseId);
1454
+ if (tickets.length === 0) return 10;
1455
+ return tickets[tickets.length - 1].order + 10;
1456
+ }
1457
+
1458
+ // src/core/validation.ts
1459
+ init_esm_shims();
1460
+ function validateProject(state) {
1461
+ const findings = [];
1462
+ const phaseIDs = new Set(state.roadmap.phases.map((p) => p.id));
1463
+ const ticketIDs = /* @__PURE__ */ new Set();
1464
+ const issueIDs = /* @__PURE__ */ new Set();
1465
+ const ticketIDCounts = /* @__PURE__ */ new Map();
1466
+ for (const t of state.tickets) {
1467
+ ticketIDCounts.set(t.id, (ticketIDCounts.get(t.id) ?? 0) + 1);
1468
+ ticketIDs.add(t.id);
1469
+ }
1470
+ for (const [id, count] of ticketIDCounts) {
1471
+ if (count > 1) {
1472
+ findings.push({
1473
+ level: "error",
1474
+ code: "duplicate_ticket_id",
1475
+ message: `Duplicate ticket ID: ${id} appears ${count} times.`,
1476
+ entity: id
1477
+ });
1478
+ }
1479
+ }
1480
+ const issueIDCounts = /* @__PURE__ */ new Map();
1481
+ for (const i of state.issues) {
1482
+ issueIDCounts.set(i.id, (issueIDCounts.get(i.id) ?? 0) + 1);
1483
+ issueIDs.add(i.id);
1484
+ }
1485
+ for (const [id, count] of issueIDCounts) {
1486
+ if (count > 1) {
1487
+ findings.push({
1488
+ level: "error",
1489
+ code: "duplicate_issue_id",
1490
+ message: `Duplicate issue ID: ${id} appears ${count} times.`,
1491
+ entity: id
1492
+ });
1493
+ }
1494
+ }
1495
+ const phaseIDCounts = /* @__PURE__ */ new Map();
1496
+ for (const p of state.roadmap.phases) {
1497
+ phaseIDCounts.set(p.id, (phaseIDCounts.get(p.id) ?? 0) + 1);
1498
+ }
1499
+ for (const [id, count] of phaseIDCounts) {
1500
+ if (count > 1) {
1501
+ findings.push({
1502
+ level: "error",
1503
+ code: "duplicate_phase_id",
1504
+ message: `Duplicate phase ID: ${id} appears ${count} times.`,
1505
+ entity: id
1506
+ });
1507
+ }
1508
+ }
1509
+ for (const t of state.tickets) {
1510
+ if (t.phase !== null && !phaseIDs.has(t.phase)) {
1511
+ findings.push({
1512
+ level: "error",
1513
+ code: "invalid_phase_ref",
1514
+ message: `Ticket ${t.id} references unknown phase "${t.phase}".`,
1515
+ entity: t.id
1516
+ });
1517
+ }
1518
+ for (const bid of t.blockedBy) {
1519
+ if (bid === t.id) {
1520
+ findings.push({
1521
+ level: "error",
1522
+ code: "self_ref_blocked_by",
1523
+ message: `Ticket ${t.id} references itself in blockedBy.`,
1524
+ entity: t.id
1525
+ });
1526
+ } else if (!ticketIDs.has(bid)) {
1527
+ findings.push({
1528
+ level: "error",
1529
+ code: "invalid_blocked_by_ref",
1530
+ message: `Ticket ${t.id} blockedBy references nonexistent ticket ${bid}.`,
1531
+ entity: t.id
1532
+ });
1533
+ } else if (state.umbrellaIDs.has(bid)) {
1534
+ findings.push({
1535
+ level: "error",
1536
+ code: "blocked_by_umbrella",
1537
+ message: `Ticket ${t.id} blockedBy references umbrella ticket ${bid}. Use leaf tickets instead.`,
1538
+ entity: t.id
1539
+ });
1540
+ }
1541
+ }
1542
+ if (t.parentTicket != null) {
1543
+ if (t.parentTicket === t.id) {
1544
+ findings.push({
1545
+ level: "error",
1546
+ code: "self_ref_parent",
1547
+ message: `Ticket ${t.id} references itself as parentTicket.`,
1548
+ entity: t.id
1549
+ });
1550
+ } else if (!ticketIDs.has(t.parentTicket)) {
1551
+ findings.push({
1552
+ level: "error",
1553
+ code: "invalid_parent_ref",
1554
+ message: `Ticket ${t.id} parentTicket references nonexistent ticket ${t.parentTicket}.`,
1555
+ entity: t.id
1556
+ });
1557
+ }
1558
+ }
1559
+ }
1560
+ detectParentCycles(state, findings);
1561
+ detectBlockedByCycles(state, findings);
1562
+ for (const i of state.issues) {
1563
+ for (const tref of i.relatedTickets) {
1564
+ if (!ticketIDs.has(tref)) {
1565
+ findings.push({
1566
+ level: "error",
1567
+ code: "invalid_related_ticket_ref",
1568
+ message: `Issue ${i.id} relatedTickets references nonexistent ticket ${tref}.`,
1569
+ entity: i.id
1570
+ });
1571
+ }
1572
+ }
1573
+ if (i.phase != null && !phaseIDs.has(i.phase)) {
1574
+ findings.push({
1575
+ level: "error",
1576
+ code: "invalid_phase_ref",
1577
+ message: `Issue ${i.id} references unknown phase "${i.phase}".`,
1578
+ entity: i.id
1579
+ });
1580
+ }
1581
+ if (i.relatedTickets.length === 0 && i.status === "open") {
1582
+ findings.push({
1583
+ level: "warning",
1584
+ code: "orphan_issue",
1585
+ message: `Issue ${i.id} is open with no related tickets.`,
1586
+ entity: i.id
1587
+ });
1588
+ }
1589
+ }
1590
+ const orderByPhase = /* @__PURE__ */ new Map();
1591
+ for (const t of state.leafTickets) {
1592
+ const phase = t.phase;
1593
+ if (!orderByPhase.has(phase)) orderByPhase.set(phase, /* @__PURE__ */ new Map());
1594
+ const orders = orderByPhase.get(phase);
1595
+ if (!orders.has(t.order)) orders.set(t.order, []);
1596
+ orders.get(t.order).push(t.id);
1597
+ }
1598
+ for (const [phase, orders] of orderByPhase) {
1599
+ for (const [order, ids] of orders) {
1600
+ if (ids.length > 1) {
1601
+ findings.push({
1602
+ level: "info",
1603
+ code: "duplicate_order",
1604
+ message: `Phase "${phase ?? "null"}": tickets ${ids.join(", ")} share order ${order}.`,
1605
+ entity: null
1606
+ });
1607
+ }
1608
+ }
1609
+ }
1610
+ const errorCount = findings.filter((f) => f.level === "error").length;
1611
+ const warningCount = findings.filter((f) => f.level === "warning").length;
1612
+ const infoCount = findings.filter((f) => f.level === "info").length;
1613
+ return {
1614
+ valid: errorCount === 0,
1615
+ errorCount,
1616
+ warningCount,
1617
+ infoCount,
1618
+ findings
1619
+ };
1620
+ }
1621
+ function mergeValidation(result, loaderWarnings) {
1622
+ if (loaderWarnings.length === 0) return result;
1623
+ const extra = loaderWarnings.map((w) => ({
1624
+ level: w.type === "naming_convention" ? "info" : "error",
1625
+ code: `loader_${w.type}`,
1626
+ message: `${w.file}: ${w.message}`,
1627
+ entity: null
1628
+ }));
1629
+ const allFindings = [...result.findings, ...extra];
1630
+ const errorCount = allFindings.filter((f) => f.level === "error").length;
1631
+ const warningCount = allFindings.filter((f) => f.level === "warning").length;
1632
+ const infoCount = allFindings.filter((f) => f.level === "info").length;
1633
+ return {
1634
+ valid: errorCount === 0,
1635
+ errorCount,
1636
+ warningCount,
1637
+ infoCount,
1638
+ findings: allFindings
1639
+ };
1640
+ }
1641
+ function detectParentCycles(state, findings) {
1642
+ const visited = /* @__PURE__ */ new Set();
1643
+ const inStack = /* @__PURE__ */ new Set();
1644
+ for (const t of state.tickets) {
1645
+ if (t.parentTicket == null || visited.has(t.id)) continue;
1646
+ dfsParent(t.id, state, visited, inStack, findings);
1647
+ }
1648
+ }
1649
+ function dfsParent(id, state, visited, inStack, findings) {
1650
+ if (inStack.has(id)) {
1651
+ findings.push({
1652
+ level: "error",
1653
+ code: "parent_cycle",
1654
+ message: `Cycle detected in parentTicket chain involving ${id}.`,
1655
+ entity: id
1656
+ });
1657
+ return;
1658
+ }
1659
+ if (visited.has(id)) return;
1660
+ inStack.add(id);
1661
+ const ticket = state.ticketByID(id);
1662
+ if (ticket?.parentTicket && ticket.parentTicket !== id) {
1663
+ dfsParent(ticket.parentTicket, state, visited, inStack, findings);
1664
+ }
1665
+ inStack.delete(id);
1666
+ visited.add(id);
1667
+ }
1668
+ function detectBlockedByCycles(state, findings) {
1669
+ const visited = /* @__PURE__ */ new Set();
1670
+ const inStack = /* @__PURE__ */ new Set();
1671
+ for (const t of state.tickets) {
1672
+ if (t.blockedBy.length === 0 || visited.has(t.id)) continue;
1673
+ dfsBlocked(t.id, state, visited, inStack, findings);
1674
+ }
1675
+ }
1676
+ function dfsBlocked(id, state, visited, inStack, findings) {
1677
+ if (inStack.has(id)) {
1678
+ findings.push({
1679
+ level: "error",
1680
+ code: "blocked_by_cycle",
1681
+ message: `Cycle detected in blockedBy chain involving ${id}.`,
1682
+ entity: id
1683
+ });
1684
+ return;
1685
+ }
1686
+ if (visited.has(id)) return;
1687
+ inStack.add(id);
1688
+ const ticket = state.ticketByID(id);
1689
+ if (ticket) {
1690
+ for (const bid of ticket.blockedBy) {
1691
+ if (bid !== id) {
1692
+ dfsBlocked(bid, state, visited, inStack, findings);
1693
+ }
1694
+ }
1695
+ }
1696
+ inStack.delete(id);
1697
+ visited.add(id);
1698
+ }
1699
+
1700
+ // src/core/init.ts
1701
+ init_esm_shims();
1702
+ import { mkdir, stat as stat2 } from "fs/promises";
1703
+ import { join as join4, resolve as resolve3 } from "path";
1704
+ init_errors();
1705
+ async function initProject(root, options) {
1706
+ const absRoot = resolve3(root);
1707
+ const wrapDir = join4(absRoot, ".story");
1708
+ let exists = false;
1709
+ try {
1710
+ const s = await stat2(wrapDir);
1711
+ if (s.isDirectory()) exists = true;
1712
+ } catch (err) {
1713
+ if (err.code !== "ENOENT") {
1714
+ throw new ProjectLoaderError(
1715
+ "io_error",
1716
+ `Cannot check .story/ directory: ${err.message}`,
1717
+ err
1718
+ );
1719
+ }
1720
+ }
1721
+ if (exists && !options.force) {
1722
+ throw new ProjectLoaderError(
1723
+ "conflict",
1724
+ ".story/ already exists. Use --force to overwrite config and roadmap."
1725
+ );
1726
+ }
1727
+ await mkdir(join4(wrapDir, "tickets"), { recursive: true });
1728
+ await mkdir(join4(wrapDir, "issues"), { recursive: true });
1729
+ await mkdir(join4(wrapDir, "handovers"), { recursive: true });
1730
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1731
+ const config = {
1732
+ version: 2,
1733
+ schemaVersion: CURRENT_SCHEMA_VERSION,
1734
+ project: options.name,
1735
+ type: options.type ?? "generic",
1736
+ language: options.language ?? "unknown",
1737
+ features: {
1738
+ tickets: true,
1739
+ issues: true,
1740
+ handovers: true,
1741
+ roadmap: true,
1742
+ reviews: true
1743
+ }
1744
+ };
1745
+ const roadmap = {
1746
+ title: options.name,
1747
+ date: today,
1748
+ phases: [
1749
+ {
1750
+ id: "p0",
1751
+ label: "PHASE 0",
1752
+ name: "Setup",
1753
+ description: "Initial project setup."
1754
+ }
1755
+ ],
1756
+ blockers: []
1757
+ };
1758
+ await writeConfig(config, absRoot);
1759
+ await writeRoadmap(roadmap, absRoot);
1760
+ return {
1761
+ root: absRoot,
1762
+ created: [
1763
+ ".story/config.json",
1764
+ ".story/roadmap.json",
1765
+ ".story/tickets/",
1766
+ ".story/issues/",
1767
+ ".story/handovers/"
1768
+ ]
1769
+ };
1770
+ }
1771
+
1772
+ // src/cli/run.ts
1773
+ init_errors();
1774
+
1775
+ // src/cli/helpers.ts
1776
+ init_esm_shims();
1777
+ import { resolve as resolve4, relative as relative3, extname as extname3 } from "path";
1778
+ import { lstat as lstat2 } from "fs/promises";
1779
+ var CliValidationError = class extends Error {
1780
+ constructor(code, message) {
1781
+ super(message);
1782
+ this.code = code;
1783
+ this.name = "CliValidationError";
1784
+ }
1785
+ };
1786
+ function formatZodError(err) {
1787
+ return err.issues.map((i) => i.message).join("; ");
1788
+ }
1789
+ function parseTicketId(raw) {
1790
+ const result = TicketIdSchema.safeParse(raw);
1791
+ if (!result.success) {
1792
+ throw new CliValidationError(
1793
+ "invalid_input",
1794
+ `Invalid ticket ID "${raw}": ${formatZodError(result.error)}`
1795
+ );
1796
+ }
1797
+ return result.data;
1798
+ }
1799
+ function parseIssueId(raw) {
1800
+ const result = IssueIdSchema.safeParse(raw);
1801
+ if (!result.success) {
1802
+ throw new CliValidationError(
1803
+ "invalid_input",
1804
+ `Invalid issue ID "${raw}": ${formatZodError(result.error)}`
1805
+ );
1806
+ }
1807
+ return result.data;
1808
+ }
1809
+ function parseOutputFormat(raw) {
1810
+ if (!OUTPUT_FORMATS.includes(raw)) {
1811
+ throw new CliValidationError(
1812
+ "invalid_input",
1813
+ `Invalid output format "${raw}": must be one of ${OUTPUT_FORMATS.join(", ")}`
1814
+ );
1815
+ }
1816
+ return raw;
1817
+ }
1818
+ function todayISO() {
1819
+ const d = /* @__PURE__ */ new Date();
1820
+ const y = d.getFullYear();
1821
+ const m = String(d.getMonth() + 1).padStart(2, "0");
1822
+ const day = String(d.getDate()).padStart(2, "0");
1823
+ return `${y}-${m}-${day}`;
1824
+ }
1825
+ function normalizeArrayOption(arr) {
1826
+ if (!arr) return [];
1827
+ return arr.filter((s) => s.trim() !== "");
1828
+ }
1829
+ function addFormatOption(y) {
1830
+ return y.option("format", {
1831
+ type: "string",
1832
+ default: "md",
1833
+ choices: ["json", "md"],
1834
+ describe: "Output format: json or md"
1835
+ });
1836
+ }
1837
+ async function parseHandoverFilename(raw, handoversDir) {
1838
+ if (raw.includes("/") || raw.includes("\\") || raw.includes("..") || raw.includes("\0")) {
1839
+ throw new CliValidationError(
1840
+ "invalid_input",
1841
+ `Invalid handover filename "${raw}": contains path traversal characters`
1842
+ );
1843
+ }
1844
+ if (extname3(raw) !== ".md") {
1845
+ throw new CliValidationError(
1846
+ "invalid_input",
1847
+ `Invalid handover filename "${raw}": must have .md extension`
1848
+ );
1849
+ }
1850
+ const resolvedDir = resolve4(handoversDir);
1851
+ const resolvedCandidate = resolve4(handoversDir, raw);
1852
+ const rel = relative3(resolvedDir, resolvedCandidate);
1853
+ if (!rel || rel.startsWith("..") || resolve4(resolvedDir, rel) !== resolvedCandidate) {
1854
+ throw new CliValidationError(
1855
+ "invalid_input",
1856
+ `Invalid handover filename "${raw}": resolves outside handovers directory`
1857
+ );
1858
+ }
1859
+ try {
1860
+ const stats = await lstat2(resolvedCandidate);
1861
+ if (stats.isSymbolicLink()) {
1862
+ throw new CliValidationError(
1863
+ "invalid_input",
1864
+ `Invalid handover filename "${raw}": symlinks not allowed`
1865
+ );
1866
+ }
1867
+ } catch (err) {
1868
+ if (err instanceof CliValidationError) throw err;
1869
+ if (err.code !== "ENOENT") {
1870
+ throw new CliValidationError(
1871
+ "io_error",
1872
+ `Cannot check handover file "${raw}": ${err.message}`
1873
+ );
1874
+ }
1875
+ }
1876
+ return raw;
1877
+ }
1878
+
1879
+ // src/cli/run.ts
1880
+ process.stdout.on("error", (err) => {
1881
+ if (err.code === "EPIPE") {
1882
+ process.exitCode = ExitCode.OK;
1883
+ return;
1884
+ }
1885
+ process.exitCode = ExitCode.USER_ERROR;
1886
+ });
1887
+ function writeOutput(text) {
1888
+ try {
1889
+ process.stdout.write(text + "\n");
1890
+ } catch (err) {
1891
+ if (err.code === "EPIPE") {
1892
+ process.exitCode = ExitCode.OK;
1893
+ return;
1894
+ }
1895
+ throw err;
1896
+ }
1897
+ }
1898
+ function hasIntegrityWarnings(warnings) {
1899
+ return warnings.some(
1900
+ (w) => INTEGRITY_WARNING_TYPES.includes(w.type)
1901
+ );
1902
+ }
1903
+ async function runReadCommand(format, handler) {
1904
+ try {
1905
+ const root = discoverProjectRoot();
1906
+ if (!root) {
1907
+ writeOutput(
1908
+ formatError("not_found", "No .story/ project found. Run `claudestory init` first.", format)
1909
+ );
1910
+ process.exitCode = ExitCode.USER_ERROR;
1911
+ return;
1912
+ }
1913
+ const { state, warnings } = await loadProject(root);
1914
+ const handoversDir = join5(root, ".story", "handovers");
1915
+ const result = await handler({ state, warnings, root, handoversDir, format });
1916
+ writeOutput(result.output);
1917
+ let exitCode = result.exitCode ?? ExitCode.OK;
1918
+ if (exitCode === ExitCode.OK && hasIntegrityWarnings(warnings)) {
1919
+ exitCode = ExitCode.PARTIAL;
1920
+ }
1921
+ process.exitCode = exitCode;
1922
+ } catch (err) {
1923
+ if (err instanceof ProjectLoaderError) {
1924
+ writeOutput(formatError(err.code, err.message, format));
1925
+ process.exitCode = ExitCode.USER_ERROR;
1926
+ return;
1927
+ }
1928
+ if (err instanceof CliValidationError) {
1929
+ writeOutput(formatError(err.code, err.message, format));
1930
+ process.exitCode = ExitCode.USER_ERROR;
1931
+ return;
1932
+ }
1933
+ const message = err instanceof Error ? err.message : String(err);
1934
+ writeOutput(formatError("io_error", message, format));
1935
+ process.exitCode = ExitCode.USER_ERROR;
1936
+ }
1937
+ }
1938
+ async function runDeleteCommand(format, force, handler) {
1939
+ try {
1940
+ const root = discoverProjectRoot();
1941
+ if (!root) {
1942
+ writeOutput(
1943
+ formatError("not_found", "No .story/ project found. Run `claudestory init` first.", format)
1944
+ );
1945
+ process.exitCode = ExitCode.USER_ERROR;
1946
+ return;
1947
+ }
1948
+ const { state, warnings } = await loadProject(root);
1949
+ const handoversDir = join5(root, ".story", "handovers");
1950
+ if (!force && hasIntegrityWarnings(warnings)) {
1951
+ writeOutput(
1952
+ formatError(
1953
+ "project_corrupt",
1954
+ "Project has integrity issues. Use --force to delete anyway.",
1955
+ format
1956
+ )
1957
+ );
1958
+ process.exitCode = ExitCode.USER_ERROR;
1959
+ return;
1960
+ }
1961
+ const result = await handler({ state, warnings, root, handoversDir, format, force });
1962
+ writeOutput(result.output);
1963
+ process.exitCode = result.exitCode ?? ExitCode.OK;
1964
+ } catch (err) {
1965
+ if (err instanceof ProjectLoaderError) {
1966
+ writeOutput(formatError(err.code, err.message, format));
1967
+ process.exitCode = ExitCode.USER_ERROR;
1968
+ return;
1969
+ }
1970
+ if (err instanceof CliValidationError) {
1971
+ writeOutput(formatError(err.code, err.message, format));
1972
+ process.exitCode = ExitCode.USER_ERROR;
1973
+ return;
1974
+ }
1975
+ const message = err instanceof Error ? err.message : String(err);
1976
+ writeOutput(formatError("io_error", message, format));
1977
+ process.exitCode = ExitCode.USER_ERROR;
1978
+ }
1979
+ }
1980
+
1981
+ // src/cli/register.ts
1982
+ init_esm_shims();
1983
+
1984
+ // src/cli/commands/status.ts
1985
+ init_esm_shims();
1986
+ function handleStatus(ctx) {
1987
+ return { output: formatStatus(ctx.state, ctx.format) };
1988
+ }
1989
+
1990
+ // src/cli/commands/validate.ts
1991
+ init_esm_shims();
1992
+ function handleValidate(ctx) {
1993
+ const baseResult = validateProject(ctx.state);
1994
+ const merged = mergeValidation(baseResult, ctx.warnings);
1995
+ return {
1996
+ output: formatValidation(merged, ctx.format),
1997
+ exitCode: merged.valid ? ExitCode.OK : ExitCode.VALIDATION_ERROR
1998
+ };
1999
+ }
2000
+
2001
+ // src/cli/commands/handover.ts
2002
+ init_esm_shims();
2003
+ function handleHandoverList(ctx) {
2004
+ return { output: formatHandoverList(ctx.state.handoverFilenames, ctx.format) };
2005
+ }
2006
+ async function handleHandoverLatest(ctx) {
2007
+ if (ctx.state.handoverFilenames.length === 0) {
2008
+ return {
2009
+ output: formatError("not_found", "No handovers found", ctx.format),
2010
+ exitCode: ExitCode.USER_ERROR,
2011
+ errorCode: "not_found"
2012
+ };
2013
+ }
2014
+ const filename = ctx.state.handoverFilenames[0];
2015
+ await parseHandoverFilename(filename, ctx.handoversDir);
2016
+ try {
2017
+ const content = await readHandover(ctx.handoversDir, filename);
2018
+ return { output: formatHandoverContent(filename, content, ctx.format) };
2019
+ } catch (err) {
2020
+ if (err.code === "ENOENT") {
2021
+ return {
2022
+ output: formatError("not_found", `Handover file not found: ${filename}`, ctx.format),
2023
+ exitCode: ExitCode.USER_ERROR,
2024
+ errorCode: "not_found"
2025
+ };
2026
+ }
2027
+ return {
2028
+ output: formatError("io_error", `Cannot read handover: ${err.message}`, ctx.format),
2029
+ exitCode: ExitCode.USER_ERROR,
2030
+ errorCode: "io_error"
2031
+ };
2032
+ }
2033
+ }
2034
+ async function handleHandoverGet(filename, ctx) {
2035
+ await parseHandoverFilename(filename, ctx.handoversDir);
2036
+ try {
2037
+ const content = await readHandover(ctx.handoversDir, filename);
2038
+ return { output: formatHandoverContent(filename, content, ctx.format) };
2039
+ } catch (err) {
2040
+ if (err.code === "ENOENT") {
2041
+ return {
2042
+ output: formatError("not_found", `Handover not found: ${filename}`, ctx.format),
2043
+ exitCode: ExitCode.USER_ERROR,
2044
+ errorCode: "not_found"
2045
+ };
2046
+ }
2047
+ return {
2048
+ output: formatError("io_error", `Cannot read handover: ${err.message}`, ctx.format),
2049
+ exitCode: ExitCode.USER_ERROR,
2050
+ errorCode: "io_error"
2051
+ };
2052
+ }
2053
+ }
2054
+
2055
+ // src/cli/commands/blocker.ts
2056
+ init_esm_shims();
2057
+ function handleBlockerList(ctx) {
2058
+ return { output: formatBlockerList(ctx.state.roadmap, ctx.format) };
2059
+ }
2060
+ async function handleBlockerAdd(args, format, root) {
2061
+ let createdBlocker;
2062
+ await withProjectLock(root, { strict: true }, async ({ state }) => {
2063
+ const activeConflict = state.roadmap.blockers.find(
2064
+ (b) => b.name === args.name && !isBlockerCleared(b)
2065
+ );
2066
+ if (activeConflict) {
2067
+ throw new CliValidationError("conflict", `Active blocker "${args.name}" already exists`);
2068
+ }
2069
+ const blocker = {
2070
+ name: args.name,
2071
+ cleared: false,
2072
+ createdDate: todayISO(),
2073
+ clearedDate: null,
2074
+ note: args.note ?? null
2075
+ };
2076
+ const newBlockers = [...state.roadmap.blockers, blocker];
2077
+ const newRoadmap = { ...state.roadmap, blockers: newBlockers };
2078
+ await writeRoadmapUnlocked(newRoadmap, root);
2079
+ createdBlocker = blocker;
2080
+ });
2081
+ if (!createdBlocker) throw new Error("Blocker not created");
2082
+ if (format === "json") {
2083
+ return { output: JSON.stringify(successEnvelope(createdBlocker), null, 2) };
2084
+ }
2085
+ return { output: `Added blocker: ${createdBlocker.name}` };
2086
+ }
2087
+ async function handleBlockerClear(name, note, format, root) {
2088
+ let clearedBlocker;
2089
+ await withProjectLock(root, { strict: true }, async ({ state }) => {
2090
+ const idx = state.roadmap.blockers.findIndex(
2091
+ (b) => b.name === name && !isBlockerCleared(b)
2092
+ );
2093
+ if (idx < 0) {
2094
+ throw new CliValidationError("not_found", `No active blocker named "${name}"`);
2095
+ }
2096
+ const existing = state.roadmap.blockers[idx];
2097
+ const updated = {
2098
+ ...existing,
2099
+ cleared: true,
2100
+ clearedDate: todayISO()
2101
+ };
2102
+ if (note !== void 0) {
2103
+ updated.note = note;
2104
+ }
2105
+ const newBlockers = [...state.roadmap.blockers];
2106
+ newBlockers[idx] = updated;
2107
+ const newRoadmap = { ...state.roadmap, blockers: newBlockers };
2108
+ await writeRoadmapUnlocked(newRoadmap, root);
2109
+ clearedBlocker = updated;
2110
+ });
2111
+ if (!clearedBlocker) throw new Error("Blocker not cleared");
2112
+ if (format === "json") {
2113
+ return { output: JSON.stringify(successEnvelope(clearedBlocker), null, 2) };
2114
+ }
2115
+ return { output: `Cleared blocker: ${clearedBlocker.name}` };
2116
+ }
2117
+
2118
+ // src/cli/commands/ticket.ts
2119
+ init_esm_shims();
2120
+ function handleTicketList(filters, ctx) {
2121
+ let tickets = [...ctx.state.leafTickets];
2122
+ if (filters.status) {
2123
+ if (!TICKET_STATUSES.includes(filters.status)) {
2124
+ throw new CliValidationError(
2125
+ "invalid_input",
2126
+ `Unknown ticket status "${filters.status}": must be one of ${TICKET_STATUSES.join(", ")}`
2127
+ );
2128
+ }
2129
+ tickets = tickets.filter((t) => t.status === filters.status);
2130
+ }
2131
+ if (filters.phase) {
2132
+ tickets = tickets.filter((t) => t.phase === filters.phase);
2133
+ }
2134
+ if (filters.type) {
2135
+ if (!TICKET_TYPES.includes(filters.type)) {
2136
+ throw new CliValidationError(
2137
+ "invalid_input",
2138
+ `Unknown ticket type "${filters.type}": must be one of ${TICKET_TYPES.join(", ")}`
2139
+ );
2140
+ }
2141
+ tickets = tickets.filter((t) => t.type === filters.type);
2142
+ }
2143
+ return { output: formatTicketList(tickets, ctx.format) };
2144
+ }
2145
+ function handleTicketGet(id, ctx) {
2146
+ const ticket = ctx.state.ticketByID(id);
2147
+ if (!ticket) {
2148
+ return {
2149
+ output: formatError("not_found", `Ticket ${id} not found`, ctx.format),
2150
+ exitCode: ExitCode.USER_ERROR,
2151
+ errorCode: "not_found"
2152
+ };
2153
+ }
2154
+ return { output: formatTicket(ticket, ctx.state, ctx.format) };
2155
+ }
2156
+ function handleTicketNext(ctx) {
2157
+ const outcome = nextTicket(ctx.state);
2158
+ const exitCode = outcome.kind === "found" ? ExitCode.OK : ExitCode.USER_ERROR;
2159
+ return {
2160
+ output: formatNextTicketOutcome(outcome, ctx.state, ctx.format),
2161
+ exitCode
2162
+ };
2163
+ }
2164
+ function handleTicketBlocked(ctx) {
2165
+ const blocked = blockedTickets(ctx.state);
2166
+ return { output: formatBlockedTickets(blocked, ctx.state, ctx.format) };
2167
+ }
2168
+ function validatePhase(phase, ctx) {
2169
+ if (phase !== null && !ctx.state.roadmap.phases.some((p) => p.id === phase)) {
2170
+ throw new CliValidationError("invalid_input", `Phase "${phase}" not found in roadmap`);
2171
+ }
2172
+ }
2173
+ function validateBlockedBy(ids, ticketId, state) {
2174
+ for (const bid of ids) {
2175
+ if (bid === ticketId) {
2176
+ throw new CliValidationError("invalid_input", `Ticket cannot block itself: ${bid}`);
2177
+ }
2178
+ const blocker = state.ticketByID(bid);
2179
+ if (!blocker) {
2180
+ throw new CliValidationError("invalid_input", `Blocked-by ticket ${bid} not found`);
2181
+ }
2182
+ if (state.umbrellaIDs.has(bid)) {
2183
+ throw new CliValidationError("invalid_input", `Cannot block on umbrella ticket ${bid}. Use leaf tickets instead.`);
2184
+ }
2185
+ }
2186
+ }
2187
+ function validateParentTicket(parentId, ticketId, state) {
2188
+ if (parentId === ticketId) {
2189
+ throw new CliValidationError("invalid_input", `Ticket cannot be its own parent`);
2190
+ }
2191
+ if (!state.ticketByID(parentId)) {
2192
+ throw new CliValidationError("invalid_input", `Parent ticket ${parentId} not found`);
2193
+ }
2194
+ }
2195
+ function validatePostWriteState(candidate, state, isCreate) {
2196
+ const existingTickets = [...state.tickets];
2197
+ if (isCreate) {
2198
+ existingTickets.push(candidate);
2199
+ } else {
2200
+ const idx = existingTickets.findIndex((t) => t.id === candidate.id);
2201
+ if (idx >= 0) existingTickets[idx] = candidate;
2202
+ else existingTickets.push(candidate);
2203
+ }
2204
+ const postState = new ProjectState({
2205
+ tickets: existingTickets,
2206
+ issues: [...state.issues],
2207
+ roadmap: state.roadmap,
2208
+ config: state.config,
2209
+ handoverFilenames: [...state.handoverFilenames]
2210
+ });
2211
+ const result = validateProject(postState);
2212
+ if (!result.valid) {
2213
+ const errors = result.findings.filter((f) => f.level === "error");
2214
+ const msg = errors.map((f) => f.message).join("; ");
2215
+ throw new CliValidationError("validation_failed", `Write would create invalid state: ${msg}`);
2216
+ }
2217
+ }
2218
+ async function handleTicketCreate(args, format, root) {
2219
+ if (!TICKET_TYPES.includes(args.type)) {
2220
+ throw new CliValidationError(
2221
+ "invalid_input",
2222
+ `Unknown ticket type "${args.type}": must be one of ${TICKET_TYPES.join(", ")}`
2223
+ );
2224
+ }
2225
+ let createdTicket;
2226
+ await withProjectLock(root, { strict: true }, async ({ state }) => {
2227
+ validatePhase(args.phase, { state });
2228
+ if (args.blockedBy.length > 0) {
2229
+ validateBlockedBy(args.blockedBy, "", state);
2230
+ }
2231
+ if (args.parentTicket) {
2232
+ validateParentTicket(args.parentTicket, "", state);
2233
+ }
2234
+ const id = nextTicketID(state.tickets);
2235
+ const order = nextOrder(args.phase, state);
2236
+ const ticket = {
2237
+ id,
2238
+ title: args.title,
2239
+ description: args.description,
2240
+ type: args.type,
2241
+ status: "open",
2242
+ phase: args.phase,
2243
+ order,
2244
+ createdDate: todayISO(),
2245
+ completedDate: null,
2246
+ blockedBy: args.blockedBy,
2247
+ parentTicket: args.parentTicket ?? void 0
2248
+ };
2249
+ validatePostWriteState(ticket, state, true);
2250
+ await writeTicketUnlocked(ticket, root);
2251
+ createdTicket = ticket;
2252
+ });
2253
+ if (!createdTicket) throw new Error("Ticket not created");
2254
+ if (format === "json") {
2255
+ return { output: JSON.stringify(successEnvelope(createdTicket), null, 2) };
2256
+ }
2257
+ return { output: `Created ticket ${createdTicket.id}: ${createdTicket.title}` };
2258
+ }
2259
+ async function handleTicketUpdate(id, updates, format, root) {
2260
+ if (updates.status && !TICKET_STATUSES.includes(updates.status)) {
2261
+ throw new CliValidationError(
2262
+ "invalid_input",
2263
+ `Unknown ticket status "${updates.status}": must be one of ${TICKET_STATUSES.join(", ")}`
2264
+ );
2265
+ }
2266
+ let updatedTicket;
2267
+ await withProjectLock(root, { strict: true }, async ({ state }) => {
2268
+ const existing = state.ticketByID(id);
2269
+ if (!existing) {
2270
+ throw new CliValidationError("not_found", `Ticket ${id} not found`);
2271
+ }
2272
+ if (updates.phase !== void 0) {
2273
+ validatePhase(updates.phase, { state });
2274
+ }
2275
+ if (updates.blockedBy) {
2276
+ validateBlockedBy(updates.blockedBy, id, state);
2277
+ }
2278
+ if (updates.parentTicket) {
2279
+ validateParentTicket(updates.parentTicket, id, state);
2280
+ }
2281
+ const ticket = { ...existing };
2282
+ if (updates.title !== void 0) ticket.title = updates.title;
2283
+ if (updates.description !== void 0) ticket.description = updates.description;
2284
+ if (updates.phase !== void 0) ticket.phase = updates.phase;
2285
+ if (updates.order !== void 0) ticket.order = updates.order;
2286
+ if (updates.blockedBy !== void 0) ticket.blockedBy = updates.blockedBy;
2287
+ if (updates.parentTicket !== void 0) ticket.parentTicket = updates.parentTicket === null ? void 0 : updates.parentTicket;
2288
+ if (updates.status !== void 0 && updates.status !== existing.status) {
2289
+ ticket.status = updates.status;
2290
+ if (updates.status === "complete" && existing.status !== "complete") {
2291
+ ticket.completedDate = todayISO();
2292
+ } else if (updates.status !== "complete" && existing.status === "complete") {
2293
+ ticket.completedDate = null;
2294
+ }
2295
+ }
2296
+ validatePostWriteState(ticket, state, false);
2297
+ await writeTicketUnlocked(ticket, root);
2298
+ updatedTicket = ticket;
2299
+ });
2300
+ if (!updatedTicket) throw new Error("Ticket not updated");
2301
+ if (format === "json") {
2302
+ return { output: JSON.stringify(successEnvelope(updatedTicket), null, 2) };
2303
+ }
2304
+ return { output: `Updated ticket ${updatedTicket.id}: ${updatedTicket.title}` };
2305
+ }
2306
+ async function handleTicketDelete(id, force, format, root) {
2307
+ if (force) {
2308
+ process.stderr.write(
2309
+ `Warning: force-deleting ${id} may leave dangling references. Run \`claudestory validate\` to check.
2310
+ `
2311
+ );
2312
+ }
2313
+ await deleteTicket(id, root, { force });
2314
+ if (format === "json") {
2315
+ return { output: JSON.stringify(successEnvelope({ id, deleted: true }), null, 2) };
2316
+ }
2317
+ return { output: `Deleted ticket ${id}.` };
2318
+ }
2319
+
2320
+ // src/cli/commands/issue.ts
2321
+ init_esm_shims();
2322
+ function handleIssueList(filters, ctx) {
2323
+ let issues = [...ctx.state.issues];
2324
+ if (filters.status) {
2325
+ if (!ISSUE_STATUSES.includes(filters.status)) {
2326
+ throw new CliValidationError(
2327
+ "invalid_input",
2328
+ `Unknown issue status "${filters.status}": must be one of ${ISSUE_STATUSES.join(", ")}`
2329
+ );
2330
+ }
2331
+ issues = issues.filter((i) => i.status === filters.status);
2332
+ }
2333
+ if (filters.severity) {
2334
+ if (!ISSUE_SEVERITIES.includes(filters.severity)) {
2335
+ throw new CliValidationError(
2336
+ "invalid_input",
2337
+ `Unknown issue severity "${filters.severity}": must be one of ${ISSUE_SEVERITIES.join(", ")}`
2338
+ );
2339
+ }
2340
+ issues = issues.filter((i) => i.severity === filters.severity);
2341
+ }
2342
+ return { output: formatIssueList(issues, ctx.format) };
2343
+ }
2344
+ function handleIssueGet(id, ctx) {
2345
+ const issue = ctx.state.issueByID(id);
2346
+ if (!issue) {
2347
+ return {
2348
+ output: formatError("not_found", `Issue ${id} not found`, ctx.format),
2349
+ exitCode: ExitCode.USER_ERROR,
2350
+ errorCode: "not_found"
2351
+ };
2352
+ }
2353
+ return { output: formatIssue(issue, ctx.format) };
2354
+ }
2355
+ function validateRelatedTickets(ids, state) {
2356
+ for (const tid of ids) {
2357
+ if (!state.ticketByID(tid)) {
2358
+ throw new CliValidationError("invalid_input", `Related ticket ${tid} not found`);
2359
+ }
2360
+ }
2361
+ }
2362
+ function validatePostWriteIssueState(candidate, state, isCreate) {
2363
+ const existingIssues = [...state.issues];
2364
+ if (isCreate) {
2365
+ existingIssues.push(candidate);
2366
+ } else {
2367
+ const idx = existingIssues.findIndex((i) => i.id === candidate.id);
2368
+ if (idx >= 0) existingIssues[idx] = candidate;
2369
+ else existingIssues.push(candidate);
2370
+ }
2371
+ const postState = new ProjectState({
2372
+ tickets: [...state.tickets],
2373
+ issues: existingIssues,
2374
+ roadmap: state.roadmap,
2375
+ config: state.config,
2376
+ handoverFilenames: [...state.handoverFilenames]
2377
+ });
2378
+ const result = validateProject(postState);
2379
+ if (!result.valid) {
2380
+ const errors = result.findings.filter((f) => f.level === "error");
2381
+ const msg = errors.map((f) => f.message).join("; ");
2382
+ throw new CliValidationError("validation_failed", `Write would create invalid state: ${msg}`);
2383
+ }
2384
+ }
2385
+ async function handleIssueCreate(args, format, root) {
2386
+ if (!ISSUE_SEVERITIES.includes(args.severity)) {
2387
+ throw new CliValidationError(
2388
+ "invalid_input",
2389
+ `Unknown issue severity "${args.severity}": must be one of ${ISSUE_SEVERITIES.join(", ")}`
2390
+ );
2391
+ }
2392
+ let createdIssue;
2393
+ await withProjectLock(root, { strict: true }, async ({ state }) => {
2394
+ if (args.relatedTickets.length > 0) {
2395
+ validateRelatedTickets(args.relatedTickets, state);
2396
+ }
2397
+ const id = nextIssueID(state.issues);
2398
+ const issue = {
2399
+ id,
2400
+ title: args.title,
2401
+ status: "open",
2402
+ severity: args.severity,
2403
+ components: args.components,
2404
+ impact: args.impact,
2405
+ resolution: null,
2406
+ location: args.location,
2407
+ discoveredDate: todayISO(),
2408
+ resolvedDate: null,
2409
+ relatedTickets: args.relatedTickets
2410
+ };
2411
+ validatePostWriteIssueState(issue, state, true);
2412
+ await writeIssueUnlocked(issue, root);
2413
+ createdIssue = issue;
2414
+ });
2415
+ if (!createdIssue) throw new Error("Issue not created");
2416
+ if (format === "json") {
2417
+ return { output: JSON.stringify(successEnvelope(createdIssue), null, 2) };
2418
+ }
2419
+ return { output: `Created issue ${createdIssue.id}: ${createdIssue.title}` };
2420
+ }
2421
+ async function handleIssueUpdate(id, updates, format, root) {
2422
+ if (updates.status && !ISSUE_STATUSES.includes(updates.status)) {
2423
+ throw new CliValidationError(
2424
+ "invalid_input",
2425
+ `Unknown issue status "${updates.status}": must be one of ${ISSUE_STATUSES.join(", ")}`
2426
+ );
2427
+ }
2428
+ if (updates.severity && !ISSUE_SEVERITIES.includes(updates.severity)) {
2429
+ throw new CliValidationError(
2430
+ "invalid_input",
2431
+ `Unknown issue severity "${updates.severity}": must be one of ${ISSUE_SEVERITIES.join(", ")}`
2432
+ );
2433
+ }
2434
+ let updatedIssue;
2435
+ await withProjectLock(root, { strict: true }, async ({ state }) => {
2436
+ const existing = state.issueByID(id);
2437
+ if (!existing) {
2438
+ throw new CliValidationError("not_found", `Issue ${id} not found`);
2439
+ }
2440
+ if (updates.relatedTickets) {
2441
+ validateRelatedTickets(updates.relatedTickets, state);
2442
+ }
2443
+ const issue = { ...existing };
2444
+ if (updates.title !== void 0) issue.title = updates.title;
2445
+ if (updates.severity !== void 0) issue.severity = updates.severity;
2446
+ if (updates.impact !== void 0) issue.impact = updates.impact;
2447
+ if (updates.resolution !== void 0) issue.resolution = updates.resolution;
2448
+ if (updates.components !== void 0) issue.components = updates.components;
2449
+ if (updates.relatedTickets !== void 0) issue.relatedTickets = updates.relatedTickets;
2450
+ if (updates.location !== void 0) issue.location = updates.location;
2451
+ if (updates.status !== void 0 && updates.status !== existing.status) {
2452
+ issue.status = updates.status;
2453
+ if (updates.status === "resolved" && existing.status !== "resolved") {
2454
+ issue.resolvedDate = todayISO();
2455
+ } else if (updates.status !== "resolved" && existing.status === "resolved") {
2456
+ issue.resolvedDate = null;
2457
+ }
2458
+ }
2459
+ validatePostWriteIssueState(issue, state, false);
2460
+ await writeIssueUnlocked(issue, root);
2461
+ updatedIssue = issue;
2462
+ });
2463
+ if (!updatedIssue) throw new Error("Issue not updated");
2464
+ if (format === "json") {
2465
+ return { output: JSON.stringify(successEnvelope(updatedIssue), null, 2) };
2466
+ }
2467
+ return { output: `Updated issue ${updatedIssue.id}: ${updatedIssue.title}` };
2468
+ }
2469
+ async function handleIssueDelete(id, format, root) {
2470
+ await deleteIssue(id, root);
2471
+ if (format === "json") {
2472
+ return { output: JSON.stringify(successEnvelope({ id, deleted: true }), null, 2) };
2473
+ }
2474
+ return { output: `Deleted issue ${id}.` };
2475
+ }
2476
+
2477
+ // src/cli/commands/phase.ts
2478
+ init_esm_shims();
2479
+ import { join as join6, resolve as resolve5 } from "path";
2480
+ var PHASE_ID_REGEX = /^[a-z0-9]+(-[a-z0-9]+)*$/;
2481
+ var PHASE_ID_MAX_LENGTH = 40;
2482
+ function validatePhaseId(id) {
2483
+ if (id.length > PHASE_ID_MAX_LENGTH) {
2484
+ throw new CliValidationError("invalid_input", `Phase ID "${id}" exceeds ${PHASE_ID_MAX_LENGTH} characters`);
2485
+ }
2486
+ if (!PHASE_ID_REGEX.test(id)) {
2487
+ throw new CliValidationError("invalid_input", `Phase ID "${id}" must be lowercase alphanumeric with hyphens (e.g. "my-phase")`);
2488
+ }
2489
+ }
2490
+ function handlePhaseList(ctx) {
2491
+ return { output: formatPhaseList(ctx.state, ctx.format) };
2492
+ }
2493
+ function handlePhaseCurrent(ctx) {
2494
+ const phase = currentPhase(ctx.state);
2495
+ if (phase) {
2496
+ if (ctx.format === "json") {
2497
+ return { output: JSON.stringify(successEnvelope(phase), null, 2) };
2498
+ }
2499
+ const summary = phase.summary ?? phase.description;
2500
+ return { output: `${phase.name} (${phase.id}) \u2014 ${summary}` };
2501
+ }
2502
+ const hasLeavesInAnyPhase = ctx.state.roadmap.phases.some(
2503
+ (p) => ctx.state.phaseTickets(p.id).length > 0
2504
+ );
2505
+ if (!hasLeavesInAnyPhase) {
2506
+ if (ctx.format === "json") {
2507
+ return {
2508
+ output: JSON.stringify(successEnvelope({ current: null, reason: "no_phases" }), null, 2),
2509
+ exitCode: ExitCode.USER_ERROR
2510
+ };
2511
+ }
2512
+ return {
2513
+ output: "No phases with tickets defined.",
2514
+ exitCode: ExitCode.USER_ERROR
2515
+ };
2516
+ }
2517
+ if (ctx.format === "json") {
2518
+ return {
2519
+ output: JSON.stringify(successEnvelope({ current: null, reason: "all_complete" }), null, 2)
2520
+ };
2521
+ }
2522
+ return { output: "All phases complete." };
2523
+ }
2524
+ function handlePhaseTickets(phaseId, ctx) {
2525
+ const phaseExists = ctx.state.roadmap.phases.some((p) => p.id === phaseId);
2526
+ if (!phaseExists) {
2527
+ return {
2528
+ output: formatError("not_found", `Phase "${phaseId}" not found`, ctx.format),
2529
+ exitCode: ExitCode.USER_ERROR,
2530
+ errorCode: "not_found"
2531
+ };
2532
+ }
2533
+ return { output: formatPhaseTickets(phaseId, ctx.state, ctx.format) };
2534
+ }
2535
+ async function handlePhaseCreate(args, format, root) {
2536
+ validatePhaseId(args.id);
2537
+ if (args.atStart && args.after) {
2538
+ throw new CliValidationError("invalid_input", "Cannot use both --after and --at-start");
2539
+ }
2540
+ if (!args.atStart && !args.after) {
2541
+ throw new CliValidationError("invalid_input", "Must specify either --after <phase-id> or --at-start");
2542
+ }
2543
+ let createdPhase;
2544
+ await withProjectLock(root, { strict: true }, async ({ state }) => {
2545
+ if (state.roadmap.phases.some((p) => p.id === args.id)) {
2546
+ throw new CliValidationError("conflict", `Phase "${args.id}" already exists`);
2547
+ }
2548
+ const phase = {
2549
+ id: args.id,
2550
+ label: args.label,
2551
+ name: args.name,
2552
+ description: args.description
2553
+ };
2554
+ if (args.summary !== void 0) {
2555
+ phase.summary = args.summary;
2556
+ }
2557
+ const newPhases = [...state.roadmap.phases];
2558
+ if (args.atStart) {
2559
+ newPhases.unshift(phase);
2560
+ } else {
2561
+ const afterIdx = newPhases.findIndex((p) => p.id === args.after);
2562
+ if (afterIdx < 0) {
2563
+ throw new CliValidationError("not_found", `Phase "${args.after}" not found`);
2564
+ }
2565
+ newPhases.splice(afterIdx + 1, 0, phase);
2566
+ }
2567
+ const newRoadmap = { ...state.roadmap, phases: newPhases };
2568
+ await writeRoadmapUnlocked(newRoadmap, root);
2569
+ createdPhase = phase;
2570
+ });
2571
+ if (!createdPhase) throw new Error("Phase not created");
2572
+ if (format === "json") {
2573
+ return { output: JSON.stringify(successEnvelope(createdPhase), null, 2) };
2574
+ }
2575
+ return { output: `Created phase ${createdPhase.id}: ${createdPhase.name}` };
2576
+ }
2577
+ async function handlePhaseRename(id, updates, format, root) {
2578
+ let updatedPhase;
2579
+ await withProjectLock(root, { strict: true }, async ({ state }) => {
2580
+ const idx = state.roadmap.phases.findIndex((p) => p.id === id);
2581
+ if (idx < 0) {
2582
+ throw new CliValidationError("not_found", `Phase "${id}" not found`);
2583
+ }
2584
+ const existing = state.roadmap.phases[idx];
2585
+ const phase = { ...existing };
2586
+ if (updates.name !== void 0) phase.name = updates.name;
2587
+ if (updates.label !== void 0) phase.label = updates.label;
2588
+ if (updates.description !== void 0) phase.description = updates.description;
2589
+ if (updates.summary !== void 0) phase.summary = updates.summary;
2590
+ const newPhases = [...state.roadmap.phases];
2591
+ newPhases[idx] = phase;
2592
+ const newRoadmap = { ...state.roadmap, phases: newPhases };
2593
+ await writeRoadmapUnlocked(newRoadmap, root);
2594
+ updatedPhase = phase;
2595
+ });
2596
+ if (!updatedPhase) throw new Error("Phase not updated");
2597
+ if (format === "json") {
2598
+ return { output: JSON.stringify(successEnvelope(updatedPhase), null, 2) };
2599
+ }
2600
+ return { output: `Updated phase ${updatedPhase.id}: ${updatedPhase.name}` };
2601
+ }
2602
+ async function handlePhaseMove(id, args, format, root) {
2603
+ if (args.atStart && args.after) {
2604
+ throw new CliValidationError("invalid_input", "Cannot use both --after and --at-start");
2605
+ }
2606
+ if (!args.atStart && !args.after) {
2607
+ throw new CliValidationError("invalid_input", "Must specify either --after <phase-id> or --at-start");
2608
+ }
2609
+ let movedPhase;
2610
+ await withProjectLock(root, { strict: true }, async ({ state }) => {
2611
+ const idx = state.roadmap.phases.findIndex((p) => p.id === id);
2612
+ if (idx < 0) {
2613
+ throw new CliValidationError("not_found", `Phase "${id}" not found`);
2614
+ }
2615
+ const phase = state.roadmap.phases[idx];
2616
+ const newPhases = state.roadmap.phases.filter((p) => p.id !== id);
2617
+ if (args.atStart) {
2618
+ newPhases.unshift(phase);
2619
+ } else {
2620
+ const afterIdx = newPhases.findIndex((p) => p.id === args.after);
2621
+ if (afterIdx < 0) {
2622
+ throw new CliValidationError("not_found", `Phase "${args.after}" not found`);
2623
+ }
2624
+ newPhases.splice(afterIdx + 1, 0, phase);
2625
+ }
2626
+ const newRoadmap = { ...state.roadmap, phases: newPhases };
2627
+ await writeRoadmapUnlocked(newRoadmap, root);
2628
+ movedPhase = phase;
2629
+ });
2630
+ if (!movedPhase) throw new Error("Phase not moved");
2631
+ if (format === "json") {
2632
+ return { output: JSON.stringify(successEnvelope(movedPhase), null, 2) };
2633
+ }
2634
+ return { output: `Moved phase ${movedPhase.id}: ${movedPhase.name}` };
2635
+ }
2636
+ async function handlePhaseDelete(id, reassign, format, root) {
2637
+ if (reassign === id) {
2638
+ throw new CliValidationError("invalid_input", `Cannot reassign to the phase being deleted: ${id}`);
2639
+ }
2640
+ await withProjectLock(root, { strict: true }, async ({ state }) => {
2641
+ const idx = state.roadmap.phases.findIndex((p) => p.id === id);
2642
+ if (idx < 0) {
2643
+ throw new CliValidationError("not_found", `Phase "${id}" not found`);
2644
+ }
2645
+ const affectedTickets = state.tickets.filter((t) => t.phase === id);
2646
+ const affectedIssues = state.issues.filter((i) => i.phase === id);
2647
+ if ((affectedTickets.length > 0 || affectedIssues.length > 0) && !reassign) {
2648
+ const parts = [];
2649
+ if (affectedTickets.length > 0) parts.push(`${affectedTickets.length} ticket(s)`);
2650
+ if (affectedIssues.length > 0) parts.push(`${affectedIssues.length} issue(s)`);
2651
+ throw new CliValidationError(
2652
+ "conflict",
2653
+ `Cannot delete phase "${id}": ${parts.join(" and ")} reference it. Use --reassign <target-phase> to move them.`
2654
+ );
2655
+ }
2656
+ if (reassign) {
2657
+ if (!state.roadmap.phases.some((p) => p.id === reassign)) {
2658
+ throw new CliValidationError("not_found", `Reassignment target phase "${reassign}" not found`);
2659
+ }
2660
+ const targetLeaves = state.phaseTickets(reassign);
2661
+ let maxOrder = targetLeaves.length > 0 ? targetLeaves[targetLeaves.length - 1].order : 0;
2662
+ const wrapDir = resolve5(root, ".story");
2663
+ const operations = [];
2664
+ const sortedTickets = [...affectedTickets].sort((a, b) => a.order - b.order);
2665
+ for (const ticket of sortedTickets) {
2666
+ maxOrder += 10;
2667
+ const updated = { ...ticket, phase: reassign, order: maxOrder };
2668
+ const parsed = TicketSchema.parse(updated);
2669
+ const content = serializeJSON(parsed);
2670
+ const target = join6(wrapDir, "tickets", `${parsed.id}.json`);
2671
+ operations.push({ op: "write", target, content });
2672
+ }
2673
+ for (const issue of affectedIssues) {
2674
+ const updated = { ...issue, phase: reassign };
2675
+ const parsed = IssueSchema.parse(updated);
2676
+ const content = serializeJSON(parsed);
2677
+ const target = join6(wrapDir, "issues", `${parsed.id}.json`);
2678
+ operations.push({ op: "write", target, content });
2679
+ }
2680
+ const newPhases = state.roadmap.phases.filter((p) => p.id !== id);
2681
+ const newRoadmap = { ...state.roadmap, phases: newPhases };
2682
+ const parsedRoadmap = RoadmapSchema.parse(newRoadmap);
2683
+ const roadmapContent = serializeJSON(parsedRoadmap);
2684
+ const roadmapTarget = join6(wrapDir, "roadmap.json");
2685
+ operations.push({ op: "write", target: roadmapTarget, content: roadmapContent });
2686
+ await runTransactionUnlocked(root, operations);
2687
+ } else {
2688
+ const newPhases = state.roadmap.phases.filter((p) => p.id !== id);
2689
+ const newRoadmap = { ...state.roadmap, phases: newPhases };
2690
+ await writeRoadmapUnlocked(newRoadmap, root);
2691
+ }
2692
+ });
2693
+ if (format === "json") {
2694
+ return { output: JSON.stringify(successEnvelope({ id, deleted: true }), null, 2) };
2695
+ }
2696
+ return { output: `Deleted phase ${id}.` };
2697
+ }
2698
+
2699
+ // src/cli/commands/init.ts
2700
+ init_esm_shims();
2701
+ import { basename as basename2 } from "path";
2702
+ init_errors();
2703
+ init_project_root_discovery();
2704
+ function registerInitCommand(yargs2) {
2705
+ return yargs2.command(
2706
+ "init",
2707
+ "Scaffold a new .story/ project",
2708
+ (y) => addFormatOption(
2709
+ y.option("name", {
2710
+ type: "string",
2711
+ describe: "Project name (defaults to current directory name)"
2712
+ }).option("force", {
2713
+ type: "boolean",
2714
+ default: false,
2715
+ describe: "Overwrite existing config and roadmap"
2716
+ }).option("type", {
2717
+ type: "string",
2718
+ describe: "Project type (e.g. npm, macapp)"
2719
+ }).option("language", {
2720
+ type: "string",
2721
+ describe: "Primary language"
2722
+ })
2723
+ ),
2724
+ async (argv) => {
2725
+ const format = parseOutputFormat(argv.format);
2726
+ try {
2727
+ const name = argv.name ?? basename2(process.cwd());
2728
+ if (!name) {
2729
+ throw new CliValidationError(
2730
+ "invalid_input",
2731
+ "Could not derive project name from current directory. Use --name to specify."
2732
+ );
2733
+ }
2734
+ const parentRoot = discoverProjectRoot();
2735
+ if (parentRoot && parentRoot !== process.cwd()) {
2736
+ process.stderr.write(
2737
+ `Warning: existing .story/ project found at ${parentRoot}. Creating nested project.
2738
+ `
2739
+ );
2740
+ }
2741
+ const result = await initProject(process.cwd(), {
2742
+ name,
2743
+ force: argv.force,
2744
+ type: argv.type,
2745
+ language: argv.language
2746
+ });
2747
+ writeOutput(formatInitResult(result, format));
2748
+ process.exitCode = ExitCode.OK;
2749
+ } catch (err) {
2750
+ if (err instanceof ProjectLoaderError) {
2751
+ writeOutput(formatError(err.code, err.message, format));
2752
+ process.exitCode = ExitCode.USER_ERROR;
2753
+ return;
2754
+ }
2755
+ if (err instanceof CliValidationError) {
2756
+ writeOutput(formatError(err.code, err.message, format));
2757
+ process.exitCode = ExitCode.USER_ERROR;
2758
+ return;
2759
+ }
2760
+ const message = err instanceof Error ? err.message : String(err);
2761
+ writeOutput(formatError("io_error", message, format));
2762
+ process.exitCode = ExitCode.USER_ERROR;
2763
+ }
2764
+ }
2765
+ );
2766
+ }
2767
+
2768
+ // src/cli/register.ts
2769
+ function registerStatusCommand(yargs2) {
2770
+ return yargs2.command(
2771
+ "status",
2772
+ "Project summary",
2773
+ (y) => addFormatOption(y),
2774
+ async (argv) => {
2775
+ const format = parseOutputFormat(argv.format);
2776
+ await runReadCommand(format, handleStatus);
2777
+ }
2778
+ );
2779
+ }
2780
+ function registerValidateCommand(yargs2) {
2781
+ return yargs2.command(
2782
+ "validate",
2783
+ "Reference integrity + schema checks",
2784
+ (y) => addFormatOption(y),
2785
+ async (argv) => {
2786
+ const format = parseOutputFormat(argv.format);
2787
+ await runReadCommand(format, handleValidate);
2788
+ }
2789
+ );
2790
+ }
2791
+ function registerHandoverCommand(yargs2) {
2792
+ return yargs2.command(
2793
+ "handover",
2794
+ "Handover operations",
2795
+ (y) => y.command(
2796
+ "list",
2797
+ "List handover filenames (newest first)",
2798
+ (y2) => addFormatOption(y2),
2799
+ async (argv) => {
2800
+ const format = parseOutputFormat(argv.format);
2801
+ await runReadCommand(format, handleHandoverList);
2802
+ }
2803
+ ).command(
2804
+ "latest",
2805
+ "Content of most recent handover",
2806
+ (y2) => addFormatOption(y2),
2807
+ async (argv) => {
2808
+ const format = parseOutputFormat(argv.format);
2809
+ await runReadCommand(format, handleHandoverLatest);
2810
+ }
2811
+ ).command(
2812
+ "get <filename>",
2813
+ "Content of a specific handover",
2814
+ (y2) => addFormatOption(
2815
+ y2.positional("filename", {
2816
+ type: "string",
2817
+ demandOption: true,
2818
+ describe: "Handover filename (e.g. 2026-03-19-session.md)"
2819
+ })
2820
+ ),
2821
+ async (argv) => {
2822
+ const format = parseOutputFormat(argv.format);
2823
+ const filename = argv.filename;
2824
+ await runReadCommand(
2825
+ format,
2826
+ (ctx) => handleHandoverGet(filename, ctx)
2827
+ );
2828
+ }
2829
+ ).demandCommand(1, "Specify a handover subcommand: list, latest, get").strict(),
2830
+ () => {
2831
+ }
2832
+ );
2833
+ }
2834
+ function registerBlockerCommand(yargs2) {
2835
+ return yargs2.command(
2836
+ "blocker",
2837
+ "Blocker operations",
2838
+ (y) => y.command(
2839
+ "list",
2840
+ "List all blockers",
2841
+ (y2) => addFormatOption(y2),
2842
+ async (argv) => {
2843
+ const format = parseOutputFormat(argv.format);
2844
+ await runReadCommand(format, handleBlockerList);
2845
+ }
2846
+ ).command(
2847
+ "add",
2848
+ "Add a new blocker",
2849
+ (y2) => addFormatOption(
2850
+ y2.option("name", {
2851
+ type: "string",
2852
+ demandOption: true,
2853
+ describe: "Blocker name"
2854
+ }).option("note", {
2855
+ type: "string",
2856
+ describe: "Optional note"
2857
+ })
2858
+ ),
2859
+ async (argv) => {
2860
+ const format = parseOutputFormat(argv.format);
2861
+ const root = (await Promise.resolve().then(() => (init_project_root_discovery(), project_root_discovery_exports))).discoverProjectRoot();
2862
+ if (!root) {
2863
+ writeOutput(
2864
+ formatError(
2865
+ "not_found",
2866
+ "No .story/ project found.",
2867
+ format
2868
+ )
2869
+ );
2870
+ process.exitCode = ExitCode.USER_ERROR;
2871
+ return;
2872
+ }
2873
+ try {
2874
+ const result = await handleBlockerAdd(
2875
+ {
2876
+ name: argv.name,
2877
+ note: argv.note
2878
+ },
2879
+ format,
2880
+ root
2881
+ );
2882
+ writeOutput(result.output);
2883
+ process.exitCode = result.exitCode ?? ExitCode.OK;
2884
+ } catch (err) {
2885
+ if (err instanceof CliValidationError) {
2886
+ writeOutput(formatError(err.code, err.message, format));
2887
+ process.exitCode = ExitCode.USER_ERROR;
2888
+ return;
2889
+ }
2890
+ const { ProjectLoaderError: ProjectLoaderError2 } = await Promise.resolve().then(() => (init_errors(), errors_exports));
2891
+ if (err instanceof ProjectLoaderError2) {
2892
+ writeOutput(formatError(err.code, err.message, format));
2893
+ process.exitCode = ExitCode.USER_ERROR;
2894
+ return;
2895
+ }
2896
+ const message = err instanceof Error ? err.message : String(err);
2897
+ writeOutput(formatError("io_error", message, format));
2898
+ process.exitCode = ExitCode.USER_ERROR;
2899
+ }
2900
+ }
2901
+ ).command(
2902
+ "clear",
2903
+ "Clear (resolve) a blocker",
2904
+ (y2) => addFormatOption(
2905
+ y2.option("name", {
2906
+ type: "string",
2907
+ demandOption: true,
2908
+ describe: "Blocker name to clear"
2909
+ }).option("note", {
2910
+ type: "string",
2911
+ describe: "Optional note"
2912
+ })
2913
+ ),
2914
+ async (argv) => {
2915
+ const format = parseOutputFormat(argv.format);
2916
+ const root = (await Promise.resolve().then(() => (init_project_root_discovery(), project_root_discovery_exports))).discoverProjectRoot();
2917
+ if (!root) {
2918
+ writeOutput(
2919
+ formatError(
2920
+ "not_found",
2921
+ "No .story/ project found.",
2922
+ format
2923
+ )
2924
+ );
2925
+ process.exitCode = ExitCode.USER_ERROR;
2926
+ return;
2927
+ }
2928
+ try {
2929
+ const result = await handleBlockerClear(
2930
+ argv.name,
2931
+ argv.note,
2932
+ format,
2933
+ root
2934
+ );
2935
+ writeOutput(result.output);
2936
+ process.exitCode = result.exitCode ?? ExitCode.OK;
2937
+ } catch (err) {
2938
+ if (err instanceof CliValidationError) {
2939
+ writeOutput(formatError(err.code, err.message, format));
2940
+ process.exitCode = ExitCode.USER_ERROR;
2941
+ return;
2942
+ }
2943
+ const { ProjectLoaderError: ProjectLoaderError2 } = await Promise.resolve().then(() => (init_errors(), errors_exports));
2944
+ if (err instanceof ProjectLoaderError2) {
2945
+ writeOutput(formatError(err.code, err.message, format));
2946
+ process.exitCode = ExitCode.USER_ERROR;
2947
+ return;
2948
+ }
2949
+ const message = err instanceof Error ? err.message : String(err);
2950
+ writeOutput(formatError("io_error", message, format));
2951
+ process.exitCode = ExitCode.USER_ERROR;
2952
+ }
2953
+ }
2954
+ ).demandCommand(1, "Specify a blocker subcommand: list, add, clear").strict(),
2955
+ () => {
2956
+ }
2957
+ );
2958
+ }
2959
+ function registerTicketCommand(yargs2) {
2960
+ return yargs2.command(
2961
+ "ticket",
2962
+ "Ticket operations",
2963
+ (y) => y.command(
2964
+ "list",
2965
+ "List tickets",
2966
+ (y2) => addFormatOption(
2967
+ y2.option("status", {
2968
+ type: "string",
2969
+ describe: "Filter by status"
2970
+ }).option("phase", {
2971
+ type: "string",
2972
+ describe: "Filter by phase"
2973
+ }).option("type", {
2974
+ type: "string",
2975
+ describe: "Filter by type"
2976
+ })
2977
+ ),
2978
+ async (argv) => {
2979
+ const format = parseOutputFormat(argv.format);
2980
+ await runReadCommand(
2981
+ format,
2982
+ (ctx) => handleTicketList(
2983
+ {
2984
+ status: argv.status,
2985
+ phase: argv.phase,
2986
+ type: argv.type
2987
+ },
2988
+ ctx
2989
+ )
2990
+ );
2991
+ }
2992
+ ).command(
2993
+ "get <id>",
2994
+ "Get ticket details",
2995
+ (y2) => addFormatOption(
2996
+ y2.positional("id", {
2997
+ type: "string",
2998
+ demandOption: true,
2999
+ describe: "Ticket ID (e.g. T-001)"
3000
+ })
3001
+ ),
3002
+ async (argv) => {
3003
+ const format = parseOutputFormat(argv.format);
3004
+ const id = parseTicketId(argv.id);
3005
+ await runReadCommand(format, (ctx) => handleTicketGet(id, ctx));
3006
+ }
3007
+ ).command(
3008
+ "next",
3009
+ "Suggest next ticket to work on",
3010
+ (y2) => addFormatOption(y2),
3011
+ async (argv) => {
3012
+ const format = parseOutputFormat(argv.format);
3013
+ await runReadCommand(format, handleTicketNext);
3014
+ }
3015
+ ).command(
3016
+ "blocked",
3017
+ "List blocked tickets",
3018
+ (y2) => addFormatOption(y2),
3019
+ async (argv) => {
3020
+ const format = parseOutputFormat(argv.format);
3021
+ await runReadCommand(format, handleTicketBlocked);
3022
+ }
3023
+ ).command(
3024
+ "create",
3025
+ "Create a new ticket",
3026
+ (y2) => addFormatOption(
3027
+ y2.option("title", {
3028
+ type: "string",
3029
+ demandOption: true,
3030
+ describe: "Ticket title"
3031
+ }).option("type", {
3032
+ type: "string",
3033
+ demandOption: true,
3034
+ describe: "Ticket type"
3035
+ }).option("phase", {
3036
+ type: "string",
3037
+ describe: "Phase ID"
3038
+ }).option("description", {
3039
+ type: "string",
3040
+ default: "",
3041
+ describe: "Ticket description"
3042
+ }).option("blocked-by", {
3043
+ type: "string",
3044
+ array: true,
3045
+ describe: "IDs of blocking tickets"
3046
+ }).option("parent-ticket", {
3047
+ type: "string",
3048
+ describe: "Parent ticket ID (makes this a sub-ticket)"
3049
+ })
3050
+ ),
3051
+ async (argv) => {
3052
+ const format = parseOutputFormat(argv.format);
3053
+ const root = (await Promise.resolve().then(() => (init_project_root_discovery(), project_root_discovery_exports))).discoverProjectRoot();
3054
+ if (!root) {
3055
+ writeOutput(
3056
+ formatError(
3057
+ "not_found",
3058
+ "No .story/ project found.",
3059
+ format
3060
+ )
3061
+ );
3062
+ process.exitCode = ExitCode.USER_ERROR;
3063
+ return;
3064
+ }
3065
+ try {
3066
+ const result = await handleTicketCreate(
3067
+ {
3068
+ title: argv.title,
3069
+ type: argv.type,
3070
+ phase: argv.phase === "" ? null : argv.phase ?? null,
3071
+ description: argv.description,
3072
+ blockedBy: normalizeArrayOption(
3073
+ argv["blocked-by"]
3074
+ ),
3075
+ parentTicket: argv["parent-ticket"] === "" ? null : argv["parent-ticket"] ?? null
3076
+ },
3077
+ format,
3078
+ root
3079
+ );
3080
+ writeOutput(result.output);
3081
+ process.exitCode = result.exitCode ?? ExitCode.OK;
3082
+ } catch (err) {
3083
+ if (err instanceof CliValidationError) {
3084
+ writeOutput(formatError(err.code, err.message, format));
3085
+ process.exitCode = ExitCode.USER_ERROR;
3086
+ return;
3087
+ }
3088
+ const { ProjectLoaderError: ProjectLoaderError2 } = await Promise.resolve().then(() => (init_errors(), errors_exports));
3089
+ if (err instanceof ProjectLoaderError2) {
3090
+ writeOutput(formatError(err.code, err.message, format));
3091
+ process.exitCode = ExitCode.USER_ERROR;
3092
+ return;
3093
+ }
3094
+ const message = err instanceof Error ? err.message : String(err);
3095
+ writeOutput(formatError("io_error", message, format));
3096
+ process.exitCode = ExitCode.USER_ERROR;
3097
+ }
3098
+ }
3099
+ ).command(
3100
+ "update <id>",
3101
+ "Update a ticket",
3102
+ (y2) => addFormatOption(
3103
+ y2.positional("id", {
3104
+ type: "string",
3105
+ demandOption: true,
3106
+ describe: "Ticket ID (e.g. T-001)"
3107
+ }).option("status", {
3108
+ type: "string",
3109
+ describe: "New status"
3110
+ }).option("title", {
3111
+ type: "string",
3112
+ describe: "New title"
3113
+ }).option("phase", {
3114
+ type: "string",
3115
+ describe: "New phase ID"
3116
+ }).option("order", {
3117
+ type: "number",
3118
+ describe: "New sort order"
3119
+ }).option("description", {
3120
+ type: "string",
3121
+ describe: "New description"
3122
+ }).option("blocked-by", {
3123
+ type: "string",
3124
+ array: true,
3125
+ describe: "IDs of blocking tickets"
3126
+ }).option("parent-ticket", {
3127
+ type: "string",
3128
+ describe: "Parent ticket ID"
3129
+ })
3130
+ ),
3131
+ async (argv) => {
3132
+ const format = parseOutputFormat(argv.format);
3133
+ const id = parseTicketId(argv.id);
3134
+ const root = (await Promise.resolve().then(() => (init_project_root_discovery(), project_root_discovery_exports))).discoverProjectRoot();
3135
+ if (!root) {
3136
+ writeOutput(
3137
+ formatError(
3138
+ "not_found",
3139
+ "No .story/ project found.",
3140
+ format
3141
+ )
3142
+ );
3143
+ process.exitCode = ExitCode.USER_ERROR;
3144
+ return;
3145
+ }
3146
+ try {
3147
+ const result = await handleTicketUpdate(
3148
+ id,
3149
+ {
3150
+ status: argv.status,
3151
+ title: argv.title,
3152
+ phase: argv.phase === "" ? null : argv.phase,
3153
+ order: argv.order,
3154
+ description: argv.description,
3155
+ blockedBy: argv["blocked-by"] ? normalizeArrayOption(argv["blocked-by"]) : void 0,
3156
+ parentTicket: argv["parent-ticket"] === "" ? null : argv["parent-ticket"]
3157
+ },
3158
+ format,
3159
+ root
3160
+ );
3161
+ writeOutput(result.output);
3162
+ process.exitCode = result.exitCode ?? ExitCode.OK;
3163
+ } catch (err) {
3164
+ if (err instanceof CliValidationError) {
3165
+ writeOutput(formatError(err.code, err.message, format));
3166
+ process.exitCode = ExitCode.USER_ERROR;
3167
+ return;
3168
+ }
3169
+ const { ProjectLoaderError: ProjectLoaderError2 } = await Promise.resolve().then(() => (init_errors(), errors_exports));
3170
+ if (err instanceof ProjectLoaderError2) {
3171
+ writeOutput(formatError(err.code, err.message, format));
3172
+ process.exitCode = ExitCode.USER_ERROR;
3173
+ return;
3174
+ }
3175
+ const message = err instanceof Error ? err.message : String(err);
3176
+ writeOutput(formatError("io_error", message, format));
3177
+ process.exitCode = ExitCode.USER_ERROR;
3178
+ }
3179
+ }
3180
+ ).command(
3181
+ "delete <id>",
3182
+ "Delete a ticket",
3183
+ (y2) => addFormatOption(
3184
+ y2.positional("id", {
3185
+ type: "string",
3186
+ demandOption: true,
3187
+ describe: "Ticket ID (e.g. T-001)"
3188
+ }).option("force", {
3189
+ type: "boolean",
3190
+ default: false,
3191
+ describe: "Force delete even with integrity issues"
3192
+ })
3193
+ ),
3194
+ async (argv) => {
3195
+ const format = parseOutputFormat(argv.format);
3196
+ const id = parseTicketId(argv.id);
3197
+ const force = argv.force;
3198
+ await runDeleteCommand(
3199
+ format,
3200
+ force,
3201
+ async (ctx) => handleTicketDelete(id, force, format, ctx.root)
3202
+ );
3203
+ }
3204
+ ).demandCommand(
3205
+ 1,
3206
+ "Specify a ticket subcommand: list, get, next, blocked, create, update, delete"
3207
+ ).strict(),
3208
+ () => {
3209
+ }
3210
+ );
3211
+ }
3212
+ function registerIssueCommand(yargs2) {
3213
+ return yargs2.command(
3214
+ "issue",
3215
+ "Issue operations",
3216
+ (y) => y.command(
3217
+ "list",
3218
+ "List issues",
3219
+ (y2) => addFormatOption(
3220
+ y2.option("status", {
3221
+ type: "string",
3222
+ describe: "Filter by status"
3223
+ }).option("severity", {
3224
+ type: "string",
3225
+ describe: "Filter by severity"
3226
+ })
3227
+ ),
3228
+ async (argv) => {
3229
+ const format = parseOutputFormat(argv.format);
3230
+ await runReadCommand(
3231
+ format,
3232
+ (ctx) => handleIssueList(
3233
+ {
3234
+ status: argv.status,
3235
+ severity: argv.severity
3236
+ },
3237
+ ctx
3238
+ )
3239
+ );
3240
+ }
3241
+ ).command(
3242
+ "get <id>",
3243
+ "Get issue details",
3244
+ (y2) => addFormatOption(
3245
+ y2.positional("id", {
3246
+ type: "string",
3247
+ demandOption: true,
3248
+ describe: "Issue ID (e.g. ISS-001)"
3249
+ })
3250
+ ),
3251
+ async (argv) => {
3252
+ const format = parseOutputFormat(argv.format);
3253
+ const id = parseIssueId(argv.id);
3254
+ await runReadCommand(format, (ctx) => handleIssueGet(id, ctx));
3255
+ }
3256
+ ).command(
3257
+ "create",
3258
+ "Create a new issue",
3259
+ (y2) => addFormatOption(
3260
+ y2.option("title", {
3261
+ type: "string",
3262
+ demandOption: true,
3263
+ describe: "Issue title"
3264
+ }).option("severity", {
3265
+ type: "string",
3266
+ demandOption: true,
3267
+ describe: "Issue severity"
3268
+ }).option("impact", {
3269
+ type: "string",
3270
+ demandOption: true,
3271
+ describe: "Impact description"
3272
+ }).option("components", {
3273
+ type: "string",
3274
+ array: true,
3275
+ describe: "Affected components"
3276
+ }).option("related-tickets", {
3277
+ type: "string",
3278
+ array: true,
3279
+ describe: "Related ticket IDs"
3280
+ }).option("location", {
3281
+ type: "string",
3282
+ array: true,
3283
+ describe: "File locations"
3284
+ })
3285
+ ),
3286
+ async (argv) => {
3287
+ const format = parseOutputFormat(argv.format);
3288
+ const root = (await Promise.resolve().then(() => (init_project_root_discovery(), project_root_discovery_exports))).discoverProjectRoot();
3289
+ if (!root) {
3290
+ writeOutput(
3291
+ formatError(
3292
+ "not_found",
3293
+ "No .story/ project found.",
3294
+ format
3295
+ )
3296
+ );
3297
+ process.exitCode = ExitCode.USER_ERROR;
3298
+ return;
3299
+ }
3300
+ try {
3301
+ const result = await handleIssueCreate(
3302
+ {
3303
+ title: argv.title,
3304
+ severity: argv.severity,
3305
+ impact: argv.impact,
3306
+ components: normalizeArrayOption(
3307
+ argv.components
3308
+ ),
3309
+ relatedTickets: normalizeArrayOption(
3310
+ argv["related-tickets"]
3311
+ ),
3312
+ location: normalizeArrayOption(
3313
+ argv.location
3314
+ )
3315
+ },
3316
+ format,
3317
+ root
3318
+ );
3319
+ writeOutput(result.output);
3320
+ process.exitCode = result.exitCode ?? ExitCode.OK;
3321
+ } catch (err) {
3322
+ if (err instanceof CliValidationError) {
3323
+ writeOutput(formatError(err.code, err.message, format));
3324
+ process.exitCode = ExitCode.USER_ERROR;
3325
+ return;
3326
+ }
3327
+ const { ProjectLoaderError: ProjectLoaderError2 } = await Promise.resolve().then(() => (init_errors(), errors_exports));
3328
+ if (err instanceof ProjectLoaderError2) {
3329
+ writeOutput(formatError(err.code, err.message, format));
3330
+ process.exitCode = ExitCode.USER_ERROR;
3331
+ return;
3332
+ }
3333
+ const message = err instanceof Error ? err.message : String(err);
3334
+ writeOutput(formatError("io_error", message, format));
3335
+ process.exitCode = ExitCode.USER_ERROR;
3336
+ }
3337
+ }
3338
+ ).command(
3339
+ "update <id>",
3340
+ "Update an issue",
3341
+ (y2) => addFormatOption(
3342
+ y2.positional("id", {
3343
+ type: "string",
3344
+ demandOption: true,
3345
+ describe: "Issue ID (e.g. ISS-001)"
3346
+ }).option("status", {
3347
+ type: "string",
3348
+ describe: "New status"
3349
+ }).option("title", {
3350
+ type: "string",
3351
+ describe: "New title"
3352
+ }).option("severity", {
3353
+ type: "string",
3354
+ describe: "New severity"
3355
+ }).option("impact", {
3356
+ type: "string",
3357
+ describe: "New impact description"
3358
+ }).option("resolution", {
3359
+ type: "string",
3360
+ describe: "Resolution description"
3361
+ }).option("components", {
3362
+ type: "string",
3363
+ array: true,
3364
+ describe: "Affected components"
3365
+ }).option("related-tickets", {
3366
+ type: "string",
3367
+ array: true,
3368
+ describe: "Related ticket IDs"
3369
+ }).option("location", {
3370
+ type: "string",
3371
+ array: true,
3372
+ describe: "File locations"
3373
+ })
3374
+ ),
3375
+ async (argv) => {
3376
+ const format = parseOutputFormat(argv.format);
3377
+ const id = parseIssueId(argv.id);
3378
+ const root = (await Promise.resolve().then(() => (init_project_root_discovery(), project_root_discovery_exports))).discoverProjectRoot();
3379
+ if (!root) {
3380
+ writeOutput(
3381
+ formatError(
3382
+ "not_found",
3383
+ "No .story/ project found.",
3384
+ format
3385
+ )
3386
+ );
3387
+ process.exitCode = ExitCode.USER_ERROR;
3388
+ return;
3389
+ }
3390
+ try {
3391
+ const result = await handleIssueUpdate(
3392
+ id,
3393
+ {
3394
+ status: argv.status,
3395
+ title: argv.title,
3396
+ severity: argv.severity,
3397
+ impact: argv.impact,
3398
+ resolution: argv.resolution === "" ? null : argv.resolution,
3399
+ components: argv.components ? normalizeArrayOption(argv.components) : void 0,
3400
+ relatedTickets: argv["related-tickets"] ? normalizeArrayOption(argv["related-tickets"]) : void 0,
3401
+ location: argv.location ? normalizeArrayOption(argv.location) : void 0
3402
+ },
3403
+ format,
3404
+ root
3405
+ );
3406
+ writeOutput(result.output);
3407
+ process.exitCode = result.exitCode ?? ExitCode.OK;
3408
+ } catch (err) {
3409
+ if (err instanceof CliValidationError) {
3410
+ writeOutput(formatError(err.code, err.message, format));
3411
+ process.exitCode = ExitCode.USER_ERROR;
3412
+ return;
3413
+ }
3414
+ const { ProjectLoaderError: ProjectLoaderError2 } = await Promise.resolve().then(() => (init_errors(), errors_exports));
3415
+ if (err instanceof ProjectLoaderError2) {
3416
+ writeOutput(formatError(err.code, err.message, format));
3417
+ process.exitCode = ExitCode.USER_ERROR;
3418
+ return;
3419
+ }
3420
+ const message = err instanceof Error ? err.message : String(err);
3421
+ writeOutput(formatError("io_error", message, format));
3422
+ process.exitCode = ExitCode.USER_ERROR;
3423
+ }
3424
+ }
3425
+ ).command(
3426
+ "delete <id>",
3427
+ "Delete an issue",
3428
+ (y2) => addFormatOption(
3429
+ y2.positional("id", {
3430
+ type: "string",
3431
+ demandOption: true,
3432
+ describe: "Issue ID (e.g. ISS-001)"
3433
+ })
3434
+ ),
3435
+ async (argv) => {
3436
+ const format = parseOutputFormat(argv.format);
3437
+ const id = parseIssueId(argv.id);
3438
+ await runDeleteCommand(
3439
+ format,
3440
+ false,
3441
+ async (ctx) => handleIssueDelete(id, format, ctx.root)
3442
+ );
3443
+ }
3444
+ ).demandCommand(
3445
+ 1,
3446
+ "Specify an issue subcommand: list, get, create, update, delete"
3447
+ ).strict(),
3448
+ () => {
3449
+ }
3450
+ );
3451
+ }
3452
+ function registerPhaseCommand(yargs2) {
3453
+ return yargs2.command(
3454
+ "phase",
3455
+ "Phase operations",
3456
+ (y) => y.command(
3457
+ "list",
3458
+ "List all phases",
3459
+ (y2) => addFormatOption(y2),
3460
+ async (argv) => {
3461
+ const format = parseOutputFormat(argv.format);
3462
+ await runReadCommand(format, handlePhaseList);
3463
+ }
3464
+ ).command(
3465
+ "current",
3466
+ "Show current phase",
3467
+ (y2) => addFormatOption(y2),
3468
+ async (argv) => {
3469
+ const format = parseOutputFormat(argv.format);
3470
+ await runReadCommand(format, handlePhaseCurrent);
3471
+ }
3472
+ ).command(
3473
+ "tickets",
3474
+ "List tickets in a phase",
3475
+ (y2) => addFormatOption(
3476
+ y2.option("phase", {
3477
+ type: "string",
3478
+ demandOption: true,
3479
+ describe: "Phase ID"
3480
+ })
3481
+ ),
3482
+ async (argv) => {
3483
+ const format = parseOutputFormat(argv.format);
3484
+ const phaseId = argv.phase;
3485
+ await runReadCommand(
3486
+ format,
3487
+ (ctx) => handlePhaseTickets(phaseId, ctx)
3488
+ );
3489
+ }
3490
+ ).command(
3491
+ "create",
3492
+ "Create a new phase",
3493
+ (y2) => addFormatOption(
3494
+ y2.option("id", {
3495
+ type: "string",
3496
+ demandOption: true,
3497
+ describe: "Phase ID (lowercase alphanumeric with hyphens)"
3498
+ }).option("name", {
3499
+ type: "string",
3500
+ demandOption: true,
3501
+ describe: "Phase name"
3502
+ }).option("label", {
3503
+ type: "string",
3504
+ demandOption: true,
3505
+ describe: "Phase label (e.g. PHASE 5)"
3506
+ }).option("description", {
3507
+ type: "string",
3508
+ demandOption: true,
3509
+ describe: "Phase description"
3510
+ }).option("summary", {
3511
+ type: "string",
3512
+ describe: "Short summary"
3513
+ }).option("after", {
3514
+ type: "string",
3515
+ describe: "Insert after this phase ID"
3516
+ }).option("at-start", {
3517
+ type: "boolean",
3518
+ default: false,
3519
+ describe: "Insert at the beginning"
3520
+ })
3521
+ ),
3522
+ async (argv) => {
3523
+ const format = parseOutputFormat(argv.format);
3524
+ const root = (await Promise.resolve().then(() => (init_project_root_discovery(), project_root_discovery_exports))).discoverProjectRoot();
3525
+ if (!root) {
3526
+ writeOutput(
3527
+ formatError(
3528
+ "not_found",
3529
+ "No .story/ project found.",
3530
+ format
3531
+ )
3532
+ );
3533
+ process.exitCode = ExitCode.USER_ERROR;
3534
+ return;
3535
+ }
3536
+ try {
3537
+ const result = await handlePhaseCreate(
3538
+ {
3539
+ id: argv.id,
3540
+ name: argv.name,
3541
+ label: argv.label,
3542
+ description: argv.description,
3543
+ summary: argv.summary,
3544
+ after: argv.after,
3545
+ atStart: argv.atStart
3546
+ },
3547
+ format,
3548
+ root
3549
+ );
3550
+ writeOutput(result.output);
3551
+ process.exitCode = result.exitCode ?? ExitCode.OK;
3552
+ } catch (err) {
3553
+ if (err instanceof CliValidationError) {
3554
+ writeOutput(formatError(err.code, err.message, format));
3555
+ process.exitCode = ExitCode.USER_ERROR;
3556
+ return;
3557
+ }
3558
+ const { ProjectLoaderError: ProjectLoaderError2 } = await Promise.resolve().then(() => (init_errors(), errors_exports));
3559
+ if (err instanceof ProjectLoaderError2) {
3560
+ writeOutput(formatError(err.code, err.message, format));
3561
+ process.exitCode = ExitCode.USER_ERROR;
3562
+ return;
3563
+ }
3564
+ const message = err instanceof Error ? err.message : String(err);
3565
+ writeOutput(formatError("io_error", message, format));
3566
+ process.exitCode = ExitCode.USER_ERROR;
3567
+ }
3568
+ }
3569
+ ).command(
3570
+ "rename <id>",
3571
+ "Rename/update phase metadata",
3572
+ (y2) => addFormatOption(
3573
+ y2.positional("id", {
3574
+ type: "string",
3575
+ demandOption: true,
3576
+ describe: "Phase ID"
3577
+ }).option("name", {
3578
+ type: "string",
3579
+ describe: "New name"
3580
+ }).option("label", {
3581
+ type: "string",
3582
+ describe: "New label"
3583
+ }).option("description", {
3584
+ type: "string",
3585
+ describe: "New description"
3586
+ }).option("summary", {
3587
+ type: "string",
3588
+ describe: "New summary"
3589
+ })
3590
+ ),
3591
+ async (argv) => {
3592
+ const format = parseOutputFormat(argv.format);
3593
+ const id = argv.id;
3594
+ const root = (await Promise.resolve().then(() => (init_project_root_discovery(), project_root_discovery_exports))).discoverProjectRoot();
3595
+ if (!root) {
3596
+ writeOutput(
3597
+ formatError(
3598
+ "not_found",
3599
+ "No .story/ project found.",
3600
+ format
3601
+ )
3602
+ );
3603
+ process.exitCode = ExitCode.USER_ERROR;
3604
+ return;
3605
+ }
3606
+ try {
3607
+ const result = await handlePhaseRename(
3608
+ id,
3609
+ {
3610
+ name: argv.name,
3611
+ label: argv.label,
3612
+ description: argv.description,
3613
+ summary: argv.summary
3614
+ },
3615
+ format,
3616
+ root
3617
+ );
3618
+ writeOutput(result.output);
3619
+ process.exitCode = result.exitCode ?? ExitCode.OK;
3620
+ } catch (err) {
3621
+ if (err instanceof CliValidationError) {
3622
+ writeOutput(formatError(err.code, err.message, format));
3623
+ process.exitCode = ExitCode.USER_ERROR;
3624
+ return;
3625
+ }
3626
+ const { ProjectLoaderError: ProjectLoaderError2 } = await Promise.resolve().then(() => (init_errors(), errors_exports));
3627
+ if (err instanceof ProjectLoaderError2) {
3628
+ writeOutput(formatError(err.code, err.message, format));
3629
+ process.exitCode = ExitCode.USER_ERROR;
3630
+ return;
3631
+ }
3632
+ const message = err instanceof Error ? err.message : String(err);
3633
+ writeOutput(formatError("io_error", message, format));
3634
+ process.exitCode = ExitCode.USER_ERROR;
3635
+ }
3636
+ }
3637
+ ).command(
3638
+ "move <id>",
3639
+ "Move a phase to a new position",
3640
+ (y2) => addFormatOption(
3641
+ y2.positional("id", {
3642
+ type: "string",
3643
+ demandOption: true,
3644
+ describe: "Phase ID to move"
3645
+ }).option("after", {
3646
+ type: "string",
3647
+ describe: "Place after this phase ID"
3648
+ }).option("at-start", {
3649
+ type: "boolean",
3650
+ default: false,
3651
+ describe: "Move to the beginning"
3652
+ })
3653
+ ),
3654
+ async (argv) => {
3655
+ const format = parseOutputFormat(argv.format);
3656
+ const id = argv.id;
3657
+ const root = (await Promise.resolve().then(() => (init_project_root_discovery(), project_root_discovery_exports))).discoverProjectRoot();
3658
+ if (!root) {
3659
+ writeOutput(
3660
+ formatError(
3661
+ "not_found",
3662
+ "No .story/ project found.",
3663
+ format
3664
+ )
3665
+ );
3666
+ process.exitCode = ExitCode.USER_ERROR;
3667
+ return;
3668
+ }
3669
+ try {
3670
+ const result = await handlePhaseMove(
3671
+ id,
3672
+ {
3673
+ after: argv.after,
3674
+ atStart: argv.atStart
3675
+ },
3676
+ format,
3677
+ root
3678
+ );
3679
+ writeOutput(result.output);
3680
+ process.exitCode = result.exitCode ?? ExitCode.OK;
3681
+ } catch (err) {
3682
+ if (err instanceof CliValidationError) {
3683
+ writeOutput(formatError(err.code, err.message, format));
3684
+ process.exitCode = ExitCode.USER_ERROR;
3685
+ return;
3686
+ }
3687
+ const { ProjectLoaderError: ProjectLoaderError2 } = await Promise.resolve().then(() => (init_errors(), errors_exports));
3688
+ if (err instanceof ProjectLoaderError2) {
3689
+ writeOutput(formatError(err.code, err.message, format));
3690
+ process.exitCode = ExitCode.USER_ERROR;
3691
+ return;
3692
+ }
3693
+ const message = err instanceof Error ? err.message : String(err);
3694
+ writeOutput(formatError("io_error", message, format));
3695
+ process.exitCode = ExitCode.USER_ERROR;
3696
+ }
3697
+ }
3698
+ ).command(
3699
+ "delete <id>",
3700
+ "Delete a phase",
3701
+ (y2) => addFormatOption(
3702
+ y2.positional("id", {
3703
+ type: "string",
3704
+ demandOption: true,
3705
+ describe: "Phase ID to delete"
3706
+ }).option("reassign", {
3707
+ type: "string",
3708
+ describe: "Move tickets/issues to this phase"
3709
+ })
3710
+ ),
3711
+ async (argv) => {
3712
+ const format = parseOutputFormat(argv.format);
3713
+ const id = argv.id;
3714
+ const root = (await Promise.resolve().then(() => (init_project_root_discovery(), project_root_discovery_exports))).discoverProjectRoot();
3715
+ if (!root) {
3716
+ writeOutput(
3717
+ formatError(
3718
+ "not_found",
3719
+ "No .story/ project found.",
3720
+ format
3721
+ )
3722
+ );
3723
+ process.exitCode = ExitCode.USER_ERROR;
3724
+ return;
3725
+ }
3726
+ try {
3727
+ const result = await handlePhaseDelete(
3728
+ id,
3729
+ argv.reassign,
3730
+ format,
3731
+ root
3732
+ );
3733
+ writeOutput(result.output);
3734
+ process.exitCode = result.exitCode ?? ExitCode.OK;
3735
+ } catch (err) {
3736
+ if (err instanceof CliValidationError) {
3737
+ writeOutput(formatError(err.code, err.message, format));
3738
+ process.exitCode = ExitCode.USER_ERROR;
3739
+ return;
3740
+ }
3741
+ const { ProjectLoaderError: ProjectLoaderError2 } = await Promise.resolve().then(() => (init_errors(), errors_exports));
3742
+ if (err instanceof ProjectLoaderError2) {
3743
+ writeOutput(formatError(err.code, err.message, format));
3744
+ process.exitCode = ExitCode.USER_ERROR;
3745
+ return;
3746
+ }
3747
+ const message = err instanceof Error ? err.message : String(err);
3748
+ writeOutput(formatError("io_error", message, format));
3749
+ process.exitCode = ExitCode.USER_ERROR;
3750
+ }
3751
+ }
3752
+ ).demandCommand(
3753
+ 1,
3754
+ "Specify a phase subcommand: list, current, tickets, create, rename, move, delete"
3755
+ ).strict(),
3756
+ () => {
3757
+ }
3758
+ );
3759
+ }
3760
+
3761
+ // src/cli/index.ts
3762
+ var version = "0.1.0";
3763
+ var HANDLED_ERROR = /* @__PURE__ */ Symbol("HANDLED_ERROR");
3764
+ var rawArgs = hideBin(process.argv);
3765
+ function sniffFormat(args) {
3766
+ for (let i = 0; i < args.length; i++) {
3767
+ if (args[i] === "--format" && args[i + 1] === "json") return "json";
3768
+ if (args[i]?.startsWith("--format=") && args[i].slice("--format=".length) === "json") return "json";
3769
+ }
3770
+ return "md";
3771
+ }
3772
+ var errorFormat = sniffFormat(rawArgs);
3773
+ var cli = yargs(rawArgs).scriptName("claudestory").version(version).strict().demandCommand(1, "Specify a command. Run with --help for available commands.").help().fail((msg, err) => {
3774
+ if (err) throw err;
3775
+ writeOutput(formatError("invalid_input", msg ?? "Unknown error", errorFormat));
3776
+ process.exitCode = ExitCode.USER_ERROR;
3777
+ throw HANDLED_ERROR;
3778
+ });
3779
+ cli = registerInitCommand(cli);
3780
+ cli = registerStatusCommand(cli);
3781
+ cli = registerPhaseCommand(cli);
3782
+ cli = registerTicketCommand(cli);
3783
+ cli = registerIssueCommand(cli);
3784
+ cli = registerHandoverCommand(cli);
3785
+ cli = registerBlockerCommand(cli);
3786
+ cli = registerValidateCommand(cli);
3787
+ await cli.parseAsync().catch((err) => {
3788
+ if (err === HANDLED_ERROR) return;
3789
+ const message = err instanceof Error ? err.message : String(err);
3790
+ writeOutput(formatError("io_error", message, errorFormat));
3791
+ process.exitCode = ExitCode.USER_ERROR;
3792
+ });