@anthropologies/claudestory 0.1.2 → 0.1.4
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/README.md +25 -4
- package/dist/cli.js +637 -27
- package/dist/index.d.ts +604 -1
- package/dist/index.js +517 -14
- package/dist/mcp.js +690 -18
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -484,6 +484,278 @@ function formatHandoverContent(filename, content, format) {
|
|
|
484
484
|
}
|
|
485
485
|
return content;
|
|
486
486
|
}
|
|
487
|
+
function formatSnapshotResult(result, format) {
|
|
488
|
+
if (format === "json") {
|
|
489
|
+
return JSON.stringify(successEnvelope(result), null, 2);
|
|
490
|
+
}
|
|
491
|
+
let line = `Snapshot saved: ${result.filename} (${result.retained} retained`;
|
|
492
|
+
if (result.pruned > 0) line += `, ${result.pruned} pruned`;
|
|
493
|
+
line += ")";
|
|
494
|
+
return line;
|
|
495
|
+
}
|
|
496
|
+
function formatRecap(recap, state, format) {
|
|
497
|
+
if (format === "json") {
|
|
498
|
+
return JSON.stringify(successEnvelope(recap), null, 2);
|
|
499
|
+
}
|
|
500
|
+
const lines = [];
|
|
501
|
+
if (!recap.snapshot) {
|
|
502
|
+
lines.push(`# ${escapeMarkdownInline(state.config.project)} \u2014 Recap`);
|
|
503
|
+
lines.push("");
|
|
504
|
+
lines.push("No snapshot found. Run `claudestory snapshot` to enable session diffs.");
|
|
505
|
+
lines.push("");
|
|
506
|
+
lines.push(`Tickets: ${state.completeLeafTicketCount}/${state.leafTicketCount} complete, ${state.blockedCount} blocked`);
|
|
507
|
+
lines.push(`Issues: ${state.openIssueCount} open`);
|
|
508
|
+
} else {
|
|
509
|
+
lines.push(`# ${escapeMarkdownInline(state.config.project)} \u2014 Recap`);
|
|
510
|
+
lines.push("");
|
|
511
|
+
lines.push(`Since snapshot: ${recap.snapshot.createdAt}`);
|
|
512
|
+
if (recap.partial) {
|
|
513
|
+
lines.push("**Note:** Snapshot was taken from a project with integrity warnings. Diff may be incomplete.");
|
|
514
|
+
}
|
|
515
|
+
const changes = recap.changes;
|
|
516
|
+
const hasChanges = hasAnyChanges(changes);
|
|
517
|
+
if (!hasChanges) {
|
|
518
|
+
lines.push("");
|
|
519
|
+
lines.push("No changes since last snapshot.");
|
|
520
|
+
} else {
|
|
521
|
+
if (changes.phases.statusChanged.length > 0) {
|
|
522
|
+
lines.push("");
|
|
523
|
+
lines.push("## Phase Transitions");
|
|
524
|
+
for (const p of changes.phases.statusChanged) {
|
|
525
|
+
lines.push(`- **${escapeMarkdownInline(p.name)}** (${p.id}): ${p.from} \u2192 ${p.to}`);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
const ticketChanges = changes.tickets;
|
|
529
|
+
if (ticketChanges.added.length > 0 || ticketChanges.removed.length > 0 || ticketChanges.statusChanged.length > 0) {
|
|
530
|
+
lines.push("");
|
|
531
|
+
lines.push("## Tickets");
|
|
532
|
+
for (const t of ticketChanges.statusChanged) {
|
|
533
|
+
lines.push(`- ${t.id}: ${escapeMarkdownInline(t.title)} \u2014 ${t.from} \u2192 ${t.to}`);
|
|
534
|
+
}
|
|
535
|
+
for (const t of ticketChanges.added) {
|
|
536
|
+
lines.push(`- ${t.id}: ${escapeMarkdownInline(t.title)} \u2014 **new**`);
|
|
537
|
+
}
|
|
538
|
+
for (const t of ticketChanges.removed) {
|
|
539
|
+
lines.push(`- ${t.id}: ${escapeMarkdownInline(t.title)} \u2014 **removed**`);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
const issueChanges = changes.issues;
|
|
543
|
+
if (issueChanges.added.length > 0 || issueChanges.resolved.length > 0 || issueChanges.statusChanged.length > 0) {
|
|
544
|
+
lines.push("");
|
|
545
|
+
lines.push("## Issues");
|
|
546
|
+
for (const i of issueChanges.resolved) {
|
|
547
|
+
lines.push(`- ${i.id}: ${escapeMarkdownInline(i.title)} \u2014 **resolved**`);
|
|
548
|
+
}
|
|
549
|
+
for (const i of issueChanges.statusChanged) {
|
|
550
|
+
lines.push(`- ${i.id}: ${escapeMarkdownInline(i.title)} \u2014 ${i.from} \u2192 ${i.to}`);
|
|
551
|
+
}
|
|
552
|
+
for (const i of issueChanges.added) {
|
|
553
|
+
lines.push(`- ${i.id}: ${escapeMarkdownInline(i.title)} \u2014 **new**`);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
if (changes.blockers.added.length > 0 || changes.blockers.cleared.length > 0) {
|
|
557
|
+
lines.push("");
|
|
558
|
+
lines.push("## Blockers");
|
|
559
|
+
for (const name of changes.blockers.cleared) {
|
|
560
|
+
lines.push(`- ${escapeMarkdownInline(name)} \u2014 **cleared**`);
|
|
561
|
+
}
|
|
562
|
+
for (const name of changes.blockers.added) {
|
|
563
|
+
lines.push(`- ${escapeMarkdownInline(name)} \u2014 **new**`);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
const actions = recap.suggestedActions;
|
|
569
|
+
lines.push("");
|
|
570
|
+
lines.push("## Suggested Actions");
|
|
571
|
+
if (actions.nextTicket) {
|
|
572
|
+
lines.push(`- **Next:** ${actions.nextTicket.id} \u2014 ${escapeMarkdownInline(actions.nextTicket.title)}${actions.nextTicket.phase ? ` (${actions.nextTicket.phase})` : ""}`);
|
|
573
|
+
}
|
|
574
|
+
if (actions.highSeverityIssues.length > 0) {
|
|
575
|
+
for (const i of actions.highSeverityIssues) {
|
|
576
|
+
lines.push(`- **${i.severity} issue:** ${i.id} \u2014 ${escapeMarkdownInline(i.title)}`);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
if (actions.recentlyClearedBlockers.length > 0) {
|
|
580
|
+
lines.push(`- **Recently cleared:** ${actions.recentlyClearedBlockers.map(escapeMarkdownInline).join(", ")}`);
|
|
581
|
+
}
|
|
582
|
+
if (!actions.nextTicket && actions.highSeverityIssues.length === 0 && actions.recentlyClearedBlockers.length === 0) {
|
|
583
|
+
lines.push("- No urgent actions.");
|
|
584
|
+
}
|
|
585
|
+
return lines.join("\n");
|
|
586
|
+
}
|
|
587
|
+
function formatExport(state, mode, phaseId, format) {
|
|
588
|
+
if (mode === "phase" && phaseId) {
|
|
589
|
+
return formatPhaseExport(state, phaseId, format);
|
|
590
|
+
}
|
|
591
|
+
return formatFullExport(state, format);
|
|
592
|
+
}
|
|
593
|
+
function formatPhaseExport(state, phaseId, format) {
|
|
594
|
+
const phase = state.roadmap.phases.find((p) => p.id === phaseId);
|
|
595
|
+
if (!phase) {
|
|
596
|
+
return formatError("not_found", `Phase "${phaseId}" not found`, format);
|
|
597
|
+
}
|
|
598
|
+
const phaseStatus = state.phaseStatus(phaseId);
|
|
599
|
+
const leaves = state.phaseTickets(phaseId);
|
|
600
|
+
const umbrellaAncestors = /* @__PURE__ */ new Map();
|
|
601
|
+
for (const leaf of leaves) {
|
|
602
|
+
if (leaf.parentTicket) {
|
|
603
|
+
const parent = state.ticketByID(leaf.parentTicket);
|
|
604
|
+
if (parent && !umbrellaAncestors.has(parent.id)) {
|
|
605
|
+
umbrellaAncestors.set(parent.id, parent);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
const crossPhaseDeps = /* @__PURE__ */ new Map();
|
|
610
|
+
for (const leaf of leaves) {
|
|
611
|
+
for (const blockerId of leaf.blockedBy) {
|
|
612
|
+
const blocker = state.ticketByID(blockerId);
|
|
613
|
+
if (blocker && blocker.phase !== phaseId && !crossPhaseDeps.has(blocker.id)) {
|
|
614
|
+
crossPhaseDeps.set(blocker.id, blocker);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
const relatedIssues = state.issues.filter(
|
|
619
|
+
(i) => i.status !== "resolved" && (i.phase === phaseId || i.relatedTickets.some((tid) => {
|
|
620
|
+
const t = state.ticketByID(tid);
|
|
621
|
+
return t && t.phase === phaseId;
|
|
622
|
+
}))
|
|
623
|
+
);
|
|
624
|
+
const activeBlockers = state.roadmap.blockers.filter(
|
|
625
|
+
(b) => !isBlockerCleared(b)
|
|
626
|
+
);
|
|
627
|
+
if (format === "json") {
|
|
628
|
+
return JSON.stringify(
|
|
629
|
+
successEnvelope({
|
|
630
|
+
phase: { id: phase.id, name: phase.name, description: phase.description, status: phaseStatus },
|
|
631
|
+
tickets: leaves.map((t) => ({ id: t.id, title: t.title, status: t.status, type: t.type, order: t.order })),
|
|
632
|
+
umbrellaAncestors: [...umbrellaAncestors.values()].map((t) => ({ id: t.id, title: t.title })),
|
|
633
|
+
crossPhaseDependencies: [...crossPhaseDeps.values()].map((t) => ({ id: t.id, title: t.title, status: t.status, phase: t.phase })),
|
|
634
|
+
issues: relatedIssues.map((i) => ({ id: i.id, title: i.title, severity: i.severity, status: i.status })),
|
|
635
|
+
blockers: activeBlockers.map((b) => ({ name: b.name, note: b.note ?? null }))
|
|
636
|
+
}),
|
|
637
|
+
null,
|
|
638
|
+
2
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
const lines = [];
|
|
642
|
+
lines.push(`# ${escapeMarkdownInline(phase.name)} (${phase.id})`);
|
|
643
|
+
lines.push("");
|
|
644
|
+
lines.push(`Status: ${phaseStatus}`);
|
|
645
|
+
if (phase.description) {
|
|
646
|
+
lines.push(`Description: ${escapeMarkdownInline(phase.description)}`);
|
|
647
|
+
}
|
|
648
|
+
if (leaves.length > 0) {
|
|
649
|
+
lines.push("");
|
|
650
|
+
lines.push("## Tickets");
|
|
651
|
+
for (const t of leaves) {
|
|
652
|
+
const indicator = t.status === "complete" ? "[x]" : t.status === "inprogress" ? "[~]" : "[ ]";
|
|
653
|
+
const parentNote = t.parentTicket && umbrellaAncestors.has(t.parentTicket) ? ` (under ${t.parentTicket})` : "";
|
|
654
|
+
lines.push(`${indicator} ${t.id}: ${escapeMarkdownInline(t.title)}${parentNote}`);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
if (crossPhaseDeps.size > 0) {
|
|
658
|
+
lines.push("");
|
|
659
|
+
lines.push("## Cross-Phase Dependencies");
|
|
660
|
+
for (const [, dep] of crossPhaseDeps) {
|
|
661
|
+
lines.push(`- ${dep.id}: ${escapeMarkdownInline(dep.title)} [${dep.status}] (${dep.phase ?? "unphased"})`);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
if (relatedIssues.length > 0) {
|
|
665
|
+
lines.push("");
|
|
666
|
+
lines.push("## Open Issues");
|
|
667
|
+
for (const i of relatedIssues) {
|
|
668
|
+
lines.push(`- ${i.id} [${i.severity}]: ${escapeMarkdownInline(i.title)}`);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
if (activeBlockers.length > 0) {
|
|
672
|
+
lines.push("");
|
|
673
|
+
lines.push("## Active Blockers");
|
|
674
|
+
for (const b of activeBlockers) {
|
|
675
|
+
lines.push(`- ${escapeMarkdownInline(b.name)}${b.note ? ` \u2014 ${escapeMarkdownInline(b.note)}` : ""}`);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
return lines.join("\n");
|
|
679
|
+
}
|
|
680
|
+
function formatFullExport(state, format) {
|
|
681
|
+
const phases = phasesWithStatus(state);
|
|
682
|
+
if (format === "json") {
|
|
683
|
+
return JSON.stringify(
|
|
684
|
+
successEnvelope({
|
|
685
|
+
project: state.config.project,
|
|
686
|
+
phases: phases.map((p) => ({
|
|
687
|
+
id: p.phase.id,
|
|
688
|
+
name: p.phase.name,
|
|
689
|
+
description: p.phase.description,
|
|
690
|
+
status: p.status,
|
|
691
|
+
tickets: state.phaseTickets(p.phase.id).map((t) => ({
|
|
692
|
+
id: t.id,
|
|
693
|
+
title: t.title,
|
|
694
|
+
status: t.status,
|
|
695
|
+
type: t.type
|
|
696
|
+
}))
|
|
697
|
+
})),
|
|
698
|
+
issues: state.issues.map((i) => ({
|
|
699
|
+
id: i.id,
|
|
700
|
+
title: i.title,
|
|
701
|
+
severity: i.severity,
|
|
702
|
+
status: i.status
|
|
703
|
+
})),
|
|
704
|
+
blockers: state.roadmap.blockers.map((b) => ({
|
|
705
|
+
name: b.name,
|
|
706
|
+
cleared: isBlockerCleared(b),
|
|
707
|
+
note: b.note ?? null
|
|
708
|
+
}))
|
|
709
|
+
}),
|
|
710
|
+
null,
|
|
711
|
+
2
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
const lines = [];
|
|
715
|
+
lines.push(`# ${escapeMarkdownInline(state.config.project)} \u2014 Full Export`);
|
|
716
|
+
lines.push("");
|
|
717
|
+
lines.push(`Tickets: ${state.completeLeafTicketCount}/${state.leafTicketCount} complete`);
|
|
718
|
+
lines.push(`Issues: ${state.openIssueCount} open`);
|
|
719
|
+
lines.push("");
|
|
720
|
+
lines.push("## Phases");
|
|
721
|
+
for (const p of phases) {
|
|
722
|
+
const indicator = p.status === "complete" ? "[x]" : p.status === "inprogress" ? "[~]" : "[ ]";
|
|
723
|
+
lines.push("");
|
|
724
|
+
lines.push(`### ${indicator} ${escapeMarkdownInline(p.phase.name)} (${p.phase.id})`);
|
|
725
|
+
if (p.phase.description) {
|
|
726
|
+
lines.push(escapeMarkdownInline(p.phase.description));
|
|
727
|
+
}
|
|
728
|
+
const tickets = state.phaseTickets(p.phase.id);
|
|
729
|
+
if (tickets.length > 0) {
|
|
730
|
+
lines.push("");
|
|
731
|
+
for (const t of tickets) {
|
|
732
|
+
const ti = t.status === "complete" ? "[x]" : t.status === "inprogress" ? "[~]" : "[ ]";
|
|
733
|
+
lines.push(`${ti} ${t.id}: ${escapeMarkdownInline(t.title)}`);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
if (state.issues.length > 0) {
|
|
738
|
+
lines.push("");
|
|
739
|
+
lines.push("## Issues");
|
|
740
|
+
for (const i of state.issues) {
|
|
741
|
+
const resolved = i.status === "resolved" ? " \u2713" : "";
|
|
742
|
+
lines.push(`- ${i.id} [${i.severity}]: ${escapeMarkdownInline(i.title)}${resolved}`);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
const blockers = state.roadmap.blockers;
|
|
746
|
+
if (blockers.length > 0) {
|
|
747
|
+
lines.push("");
|
|
748
|
+
lines.push("## Blockers");
|
|
749
|
+
for (const b of blockers) {
|
|
750
|
+
const cleared = isBlockerCleared(b) ? "[x]" : "[ ]";
|
|
751
|
+
lines.push(`${cleared} ${escapeMarkdownInline(b.name)}${b.note ? ` \u2014 ${escapeMarkdownInline(b.note)}` : ""}`);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
return lines.join("\n");
|
|
755
|
+
}
|
|
756
|
+
function hasAnyChanges(diff) {
|
|
757
|
+
return diff.tickets.added.length > 0 || diff.tickets.removed.length > 0 || diff.tickets.statusChanged.length > 0 || diff.issues.added.length > 0 || diff.issues.resolved.length > 0 || diff.issues.statusChanged.length > 0 || diff.blockers.added.length > 0 || diff.blockers.cleared.length > 0 || diff.phases.added.length > 0 || diff.phases.removed.length > 0 || diff.phases.statusChanged.length > 0;
|
|
758
|
+
}
|
|
487
759
|
function truncate(text, maxLen) {
|
|
488
760
|
if (text.length <= maxLen) return text;
|
|
489
761
|
return text.slice(0, maxLen - 3) + "...";
|
|
@@ -496,7 +768,7 @@ function formatTicketOneLiner(t, state) {
|
|
|
496
768
|
|
|
497
769
|
// src/cli/run.ts
|
|
498
770
|
init_esm_shims();
|
|
499
|
-
import { join as
|
|
771
|
+
import { join as join6 } from "path";
|
|
500
772
|
|
|
501
773
|
// src/core/index.ts
|
|
502
774
|
init_esm_shims();
|
|
@@ -1709,7 +1981,7 @@ function dfsBlocked(id, state, visited, inStack, findings) {
|
|
|
1709
1981
|
|
|
1710
1982
|
// src/core/init.ts
|
|
1711
1983
|
init_esm_shims();
|
|
1712
|
-
import { mkdir, stat as stat2
|
|
1984
|
+
import { mkdir, stat as stat2 } from "fs/promises";
|
|
1713
1985
|
import { join as join4, resolve as resolve3 } from "path";
|
|
1714
1986
|
init_errors();
|
|
1715
1987
|
async function initProject(root, options) {
|
|
@@ -1769,21 +2041,14 @@ async function initProject(root, options) {
|
|
|
1769
2041
|
await writeRoadmap(roadmap, absRoot);
|
|
1770
2042
|
const warnings = [];
|
|
1771
2043
|
if (options.force && exists) {
|
|
1772
|
-
|
|
1773
|
-
const
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
if (!f.endsWith(".json")) continue;
|
|
1778
|
-
try {
|
|
1779
|
-
const content = await readFile3(join4(dir, f), "utf-8");
|
|
1780
|
-
JSON.parse(content);
|
|
1781
|
-
} catch {
|
|
1782
|
-
warnings.push(`.story/${subdir}/${f}`);
|
|
1783
|
-
}
|
|
2044
|
+
try {
|
|
2045
|
+
const { warnings: loadWarnings } = await loadProject(absRoot);
|
|
2046
|
+
for (const w of loadWarnings) {
|
|
2047
|
+
if (INTEGRITY_WARNING_TYPES.includes(w.type)) {
|
|
2048
|
+
warnings.push(`${w.file}: ${w.message}`);
|
|
1784
2049
|
}
|
|
1785
|
-
} catch {
|
|
1786
2050
|
}
|
|
2051
|
+
} catch {
|
|
1787
2052
|
}
|
|
1788
2053
|
}
|
|
1789
2054
|
return {
|
|
@@ -1799,12 +2064,241 @@ async function initProject(root, options) {
|
|
|
1799
2064
|
};
|
|
1800
2065
|
}
|
|
1801
2066
|
|
|
2067
|
+
// src/core/snapshot.ts
|
|
2068
|
+
init_esm_shims();
|
|
2069
|
+
import { readdir as readdir3, readFile as readFile3, mkdir as mkdir2, unlink as unlink2 } from "fs/promises";
|
|
2070
|
+
import { existsSync as existsSync4 } from "fs";
|
|
2071
|
+
import { join as join5, resolve as resolve4 } from "path";
|
|
2072
|
+
import { z as z6 } from "zod";
|
|
2073
|
+
var LoadWarningSchema = z6.object({
|
|
2074
|
+
type: z6.string(),
|
|
2075
|
+
file: z6.string(),
|
|
2076
|
+
message: z6.string()
|
|
2077
|
+
});
|
|
2078
|
+
var SnapshotV1Schema = z6.object({
|
|
2079
|
+
version: z6.literal(1),
|
|
2080
|
+
createdAt: z6.string().datetime({ offset: true }),
|
|
2081
|
+
project: z6.string(),
|
|
2082
|
+
config: ConfigSchema,
|
|
2083
|
+
roadmap: RoadmapSchema,
|
|
2084
|
+
tickets: z6.array(TicketSchema),
|
|
2085
|
+
issues: z6.array(IssueSchema),
|
|
2086
|
+
warnings: z6.array(LoadWarningSchema).optional()
|
|
2087
|
+
});
|
|
2088
|
+
var MAX_SNAPSHOTS = 20;
|
|
2089
|
+
async function saveSnapshot(root, loadResult) {
|
|
2090
|
+
const absRoot = resolve4(root);
|
|
2091
|
+
const snapshotsDir = join5(absRoot, ".story", "snapshots");
|
|
2092
|
+
await mkdir2(snapshotsDir, { recursive: true });
|
|
2093
|
+
const { state, warnings } = loadResult;
|
|
2094
|
+
const now = /* @__PURE__ */ new Date();
|
|
2095
|
+
const filename = formatSnapshotFilename(now);
|
|
2096
|
+
const snapshot = {
|
|
2097
|
+
version: 1,
|
|
2098
|
+
createdAt: now.toISOString(),
|
|
2099
|
+
project: state.config.project,
|
|
2100
|
+
config: state.config,
|
|
2101
|
+
roadmap: state.roadmap,
|
|
2102
|
+
tickets: [...state.tickets],
|
|
2103
|
+
issues: [...state.issues],
|
|
2104
|
+
...warnings.length > 0 ? {
|
|
2105
|
+
warnings: warnings.map((w) => ({
|
|
2106
|
+
type: w.type,
|
|
2107
|
+
file: w.file,
|
|
2108
|
+
message: w.message
|
|
2109
|
+
}))
|
|
2110
|
+
} : {}
|
|
2111
|
+
};
|
|
2112
|
+
const json = JSON.stringify(snapshot, null, 2) + "\n";
|
|
2113
|
+
const targetPath = join5(snapshotsDir, filename);
|
|
2114
|
+
const wrapDir = join5(absRoot, ".story");
|
|
2115
|
+
await guardPath(targetPath, wrapDir);
|
|
2116
|
+
await atomicWrite(targetPath, json);
|
|
2117
|
+
const pruned = await pruneSnapshots(snapshotsDir);
|
|
2118
|
+
const entries = await listSnapshotFiles(snapshotsDir);
|
|
2119
|
+
return { filename, retained: entries.length, pruned };
|
|
2120
|
+
}
|
|
2121
|
+
async function loadLatestSnapshot(root) {
|
|
2122
|
+
const snapshotsDir = join5(resolve4(root), ".story", "snapshots");
|
|
2123
|
+
if (!existsSync4(snapshotsDir)) return null;
|
|
2124
|
+
const files = await listSnapshotFiles(snapshotsDir);
|
|
2125
|
+
if (files.length === 0) return null;
|
|
2126
|
+
for (const filename of files) {
|
|
2127
|
+
try {
|
|
2128
|
+
const content = await readFile3(join5(snapshotsDir, filename), "utf-8");
|
|
2129
|
+
const parsed = JSON.parse(content);
|
|
2130
|
+
const snapshot = SnapshotV1Schema.parse(parsed);
|
|
2131
|
+
return { snapshot, filename };
|
|
2132
|
+
} catch {
|
|
2133
|
+
continue;
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
return null;
|
|
2137
|
+
}
|
|
2138
|
+
function diffStates(snapshotState, currentState) {
|
|
2139
|
+
const snapTickets = new Map(snapshotState.tickets.map((t) => [t.id, t]));
|
|
2140
|
+
const curTickets = new Map(currentState.tickets.map((t) => [t.id, t]));
|
|
2141
|
+
const ticketsAdded = [];
|
|
2142
|
+
const ticketsRemoved = [];
|
|
2143
|
+
const ticketsStatusChanged = [];
|
|
2144
|
+
for (const [id, cur] of curTickets) {
|
|
2145
|
+
const snap = snapTickets.get(id);
|
|
2146
|
+
if (!snap) {
|
|
2147
|
+
ticketsAdded.push({ id, title: cur.title });
|
|
2148
|
+
} else if (snap.status !== cur.status) {
|
|
2149
|
+
ticketsStatusChanged.push({ id, title: cur.title, from: snap.status, to: cur.status });
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
for (const [id, snap] of snapTickets) {
|
|
2153
|
+
if (!curTickets.has(id)) {
|
|
2154
|
+
ticketsRemoved.push({ id, title: snap.title });
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
const snapIssues = new Map(snapshotState.issues.map((i) => [i.id, i]));
|
|
2158
|
+
const curIssues = new Map(currentState.issues.map((i) => [i.id, i]));
|
|
2159
|
+
const issuesAdded = [];
|
|
2160
|
+
const issuesResolved = [];
|
|
2161
|
+
const issuesStatusChanged = [];
|
|
2162
|
+
for (const [id, cur] of curIssues) {
|
|
2163
|
+
const snap = snapIssues.get(id);
|
|
2164
|
+
if (!snap) {
|
|
2165
|
+
issuesAdded.push({ id, title: cur.title });
|
|
2166
|
+
} else if (snap.status !== cur.status) {
|
|
2167
|
+
if (cur.status === "resolved") {
|
|
2168
|
+
issuesResolved.push({ id, title: cur.title });
|
|
2169
|
+
} else {
|
|
2170
|
+
issuesStatusChanged.push({ id, title: cur.title, from: snap.status, to: cur.status });
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
const snapBlockers = new Map(
|
|
2175
|
+
snapshotState.roadmap.blockers.map((b) => [b.name, b])
|
|
2176
|
+
);
|
|
2177
|
+
const curBlockers = new Map(
|
|
2178
|
+
currentState.roadmap.blockers.map((b) => [b.name, b])
|
|
2179
|
+
);
|
|
2180
|
+
const blockersAdded = [];
|
|
2181
|
+
const blockersCleared = [];
|
|
2182
|
+
for (const [name, cur] of curBlockers) {
|
|
2183
|
+
const snap = snapBlockers.get(name);
|
|
2184
|
+
if (!snap) {
|
|
2185
|
+
blockersAdded.push(name);
|
|
2186
|
+
} else if (!isBlockerCleared(snap) && isBlockerCleared(cur)) {
|
|
2187
|
+
blockersCleared.push(name);
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
const snapPhases = snapshotState.roadmap.phases;
|
|
2191
|
+
const curPhases = currentState.roadmap.phases;
|
|
2192
|
+
const snapPhaseMap = new Map(snapPhases.map((p) => [p.id, p]));
|
|
2193
|
+
const curPhaseMap = new Map(curPhases.map((p) => [p.id, p]));
|
|
2194
|
+
const phasesAdded = [];
|
|
2195
|
+
const phasesRemoved = [];
|
|
2196
|
+
const phasesStatusChanged = [];
|
|
2197
|
+
for (const [id, curPhase] of curPhaseMap) {
|
|
2198
|
+
const snapPhase = snapPhaseMap.get(id);
|
|
2199
|
+
if (!snapPhase) {
|
|
2200
|
+
phasesAdded.push({ id, name: curPhase.name });
|
|
2201
|
+
} else {
|
|
2202
|
+
const snapStatus = snapshotState.phaseStatus(id);
|
|
2203
|
+
const curStatus = currentState.phaseStatus(id);
|
|
2204
|
+
if (snapStatus !== curStatus) {
|
|
2205
|
+
phasesStatusChanged.push({
|
|
2206
|
+
id,
|
|
2207
|
+
name: curPhase.name,
|
|
2208
|
+
from: snapStatus,
|
|
2209
|
+
to: curStatus
|
|
2210
|
+
});
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
for (const [id, snapPhase] of snapPhaseMap) {
|
|
2215
|
+
if (!curPhaseMap.has(id)) {
|
|
2216
|
+
phasesRemoved.push({ id, name: snapPhase.name });
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
return {
|
|
2220
|
+
tickets: { added: ticketsAdded, removed: ticketsRemoved, statusChanged: ticketsStatusChanged },
|
|
2221
|
+
issues: { added: issuesAdded, resolved: issuesResolved, statusChanged: issuesStatusChanged },
|
|
2222
|
+
blockers: { added: blockersAdded, cleared: blockersCleared },
|
|
2223
|
+
phases: { added: phasesAdded, removed: phasesRemoved, statusChanged: phasesStatusChanged }
|
|
2224
|
+
};
|
|
2225
|
+
}
|
|
2226
|
+
function buildRecap(currentState, snapshotInfo) {
|
|
2227
|
+
const next = nextTicket(currentState);
|
|
2228
|
+
const nextTicketAction = next.kind === "found" ? { id: next.ticket.id, title: next.ticket.title, phase: next.ticket.phase } : null;
|
|
2229
|
+
const highSeverityIssues = currentState.issues.filter(
|
|
2230
|
+
(i) => i.status !== "resolved" && (i.severity === "critical" || i.severity === "high")
|
|
2231
|
+
).map((i) => ({ id: i.id, title: i.title, severity: i.severity }));
|
|
2232
|
+
if (!snapshotInfo) {
|
|
2233
|
+
return {
|
|
2234
|
+
snapshot: null,
|
|
2235
|
+
changes: null,
|
|
2236
|
+
suggestedActions: {
|
|
2237
|
+
nextTicket: nextTicketAction,
|
|
2238
|
+
highSeverityIssues,
|
|
2239
|
+
recentlyClearedBlockers: []
|
|
2240
|
+
},
|
|
2241
|
+
partial: false
|
|
2242
|
+
};
|
|
2243
|
+
}
|
|
2244
|
+
const { snapshot, filename } = snapshotInfo;
|
|
2245
|
+
const snapshotState = new ProjectState({
|
|
2246
|
+
tickets: snapshot.tickets,
|
|
2247
|
+
issues: snapshot.issues,
|
|
2248
|
+
roadmap: snapshot.roadmap,
|
|
2249
|
+
config: snapshot.config,
|
|
2250
|
+
handoverFilenames: []
|
|
2251
|
+
});
|
|
2252
|
+
const changes = diffStates(snapshotState, currentState);
|
|
2253
|
+
const recentlyClearedBlockers = changes.blockers.cleared;
|
|
2254
|
+
return {
|
|
2255
|
+
snapshot: { filename, createdAt: snapshot.createdAt },
|
|
2256
|
+
changes,
|
|
2257
|
+
suggestedActions: {
|
|
2258
|
+
nextTicket: nextTicketAction,
|
|
2259
|
+
highSeverityIssues,
|
|
2260
|
+
recentlyClearedBlockers
|
|
2261
|
+
},
|
|
2262
|
+
partial: (snapshot.warnings ?? []).length > 0
|
|
2263
|
+
};
|
|
2264
|
+
}
|
|
2265
|
+
function formatSnapshotFilename(date) {
|
|
2266
|
+
const y = date.getUTCFullYear();
|
|
2267
|
+
const mo = String(date.getUTCMonth() + 1).padStart(2, "0");
|
|
2268
|
+
const d = String(date.getUTCDate()).padStart(2, "0");
|
|
2269
|
+
const h = String(date.getUTCHours()).padStart(2, "0");
|
|
2270
|
+
const mi = String(date.getUTCMinutes()).padStart(2, "0");
|
|
2271
|
+
const s = String(date.getUTCSeconds()).padStart(2, "0");
|
|
2272
|
+
const ms = String(date.getUTCMilliseconds()).padStart(3, "0");
|
|
2273
|
+
return `${y}-${mo}-${d}T${h}-${mi}-${s}-${ms}.json`;
|
|
2274
|
+
}
|
|
2275
|
+
async function listSnapshotFiles(dir) {
|
|
2276
|
+
try {
|
|
2277
|
+
const entries = await readdir3(dir);
|
|
2278
|
+
return entries.filter((f) => f.endsWith(".json") && !f.startsWith(".")).sort().reverse();
|
|
2279
|
+
} catch {
|
|
2280
|
+
return [];
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
async function pruneSnapshots(dir) {
|
|
2284
|
+
const files = await listSnapshotFiles(dir);
|
|
2285
|
+
if (files.length <= MAX_SNAPSHOTS) return 0;
|
|
2286
|
+
const toRemove = files.slice(MAX_SNAPSHOTS);
|
|
2287
|
+
for (const f of toRemove) {
|
|
2288
|
+
try {
|
|
2289
|
+
await unlink2(join5(dir, f));
|
|
2290
|
+
} catch {
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
return toRemove.length;
|
|
2294
|
+
}
|
|
2295
|
+
|
|
1802
2296
|
// src/cli/run.ts
|
|
1803
2297
|
init_errors();
|
|
1804
2298
|
|
|
1805
2299
|
// src/cli/helpers.ts
|
|
1806
2300
|
init_esm_shims();
|
|
1807
|
-
import { resolve as
|
|
2301
|
+
import { resolve as resolve5, relative as relative3, extname as extname3 } from "path";
|
|
1808
2302
|
import { lstat as lstat2 } from "fs/promises";
|
|
1809
2303
|
var CliValidationError = class extends Error {
|
|
1810
2304
|
constructor(code, message) {
|
|
@@ -1877,10 +2371,10 @@ async function parseHandoverFilename(raw, handoversDir) {
|
|
|
1877
2371
|
`Invalid handover filename "${raw}": must have .md extension`
|
|
1878
2372
|
);
|
|
1879
2373
|
}
|
|
1880
|
-
const resolvedDir =
|
|
1881
|
-
const resolvedCandidate =
|
|
2374
|
+
const resolvedDir = resolve5(handoversDir);
|
|
2375
|
+
const resolvedCandidate = resolve5(handoversDir, raw);
|
|
1882
2376
|
const rel = relative3(resolvedDir, resolvedCandidate);
|
|
1883
|
-
if (!rel || rel.startsWith("..") ||
|
|
2377
|
+
if (!rel || rel.startsWith("..") || resolve5(resolvedDir, rel) !== resolvedCandidate) {
|
|
1884
2378
|
throw new CliValidationError(
|
|
1885
2379
|
"invalid_input",
|
|
1886
2380
|
`Invalid handover filename "${raw}": resolves outside handovers directory`
|
|
@@ -1941,7 +2435,7 @@ async function runReadCommand(format, handler) {
|
|
|
1941
2435
|
return;
|
|
1942
2436
|
}
|
|
1943
2437
|
const { state, warnings } = await loadProject(root);
|
|
1944
|
-
const handoversDir =
|
|
2438
|
+
const handoversDir = join6(root, ".story", "handovers");
|
|
1945
2439
|
const result = await handler({ state, warnings, root, handoversDir, format });
|
|
1946
2440
|
writeOutput(result.output);
|
|
1947
2441
|
let exitCode = result.exitCode ?? ExitCode.OK;
|
|
@@ -1976,7 +2470,7 @@ async function runDeleteCommand(format, force, handler) {
|
|
|
1976
2470
|
return;
|
|
1977
2471
|
}
|
|
1978
2472
|
const { state, warnings } = await loadProject(root);
|
|
1979
|
-
const handoversDir =
|
|
2473
|
+
const handoversDir = join6(root, ".story", "handovers");
|
|
1980
2474
|
if (!force && hasIntegrityWarnings(warnings)) {
|
|
1981
2475
|
writeOutput(
|
|
1982
2476
|
formatError(
|
|
@@ -2506,7 +3000,7 @@ async function handleIssueDelete(id, format, root) {
|
|
|
2506
3000
|
|
|
2507
3001
|
// src/cli/commands/phase.ts
|
|
2508
3002
|
init_esm_shims();
|
|
2509
|
-
import { join as
|
|
3003
|
+
import { join as join7, resolve as resolve6 } from "path";
|
|
2510
3004
|
var PHASE_ID_REGEX = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
2511
3005
|
var PHASE_ID_MAX_LENGTH = 40;
|
|
2512
3006
|
function validatePhaseId(id) {
|
|
@@ -2689,7 +3183,7 @@ async function handlePhaseDelete(id, reassign, format, root) {
|
|
|
2689
3183
|
}
|
|
2690
3184
|
const targetLeaves = state.phaseTickets(reassign);
|
|
2691
3185
|
let maxOrder = targetLeaves.length > 0 ? targetLeaves[targetLeaves.length - 1].order : 0;
|
|
2692
|
-
const wrapDir =
|
|
3186
|
+
const wrapDir = resolve6(root, ".story");
|
|
2693
3187
|
const operations = [];
|
|
2694
3188
|
const sortedTickets = [...affectedTickets].sort((a, b) => a.order - b.order);
|
|
2695
3189
|
for (const ticket of sortedTickets) {
|
|
@@ -2697,21 +3191,21 @@ async function handlePhaseDelete(id, reassign, format, root) {
|
|
|
2697
3191
|
const updated = { ...ticket, phase: reassign, order: maxOrder };
|
|
2698
3192
|
const parsed = TicketSchema.parse(updated);
|
|
2699
3193
|
const content = serializeJSON(parsed);
|
|
2700
|
-
const target =
|
|
3194
|
+
const target = join7(wrapDir, "tickets", `${parsed.id}.json`);
|
|
2701
3195
|
operations.push({ op: "write", target, content });
|
|
2702
3196
|
}
|
|
2703
3197
|
for (const issue of affectedIssues) {
|
|
2704
3198
|
const updated = { ...issue, phase: reassign };
|
|
2705
3199
|
const parsed = IssueSchema.parse(updated);
|
|
2706
3200
|
const content = serializeJSON(parsed);
|
|
2707
|
-
const target =
|
|
3201
|
+
const target = join7(wrapDir, "issues", `${parsed.id}.json`);
|
|
2708
3202
|
operations.push({ op: "write", target, content });
|
|
2709
3203
|
}
|
|
2710
3204
|
const newPhases = state.roadmap.phases.filter((p) => p.id !== id);
|
|
2711
3205
|
const newRoadmap = { ...state.roadmap, phases: newPhases };
|
|
2712
3206
|
const parsedRoadmap = RoadmapSchema.parse(newRoadmap);
|
|
2713
3207
|
const roadmapContent = serializeJSON(parsedRoadmap);
|
|
2714
|
-
const roadmapTarget =
|
|
3208
|
+
const roadmapTarget = join7(wrapDir, "roadmap.json");
|
|
2715
3209
|
operations.push({ op: "write", target: roadmapTarget, content: roadmapContent });
|
|
2716
3210
|
await runTransactionUnlocked(root, operations);
|
|
2717
3211
|
} else {
|
|
@@ -2795,6 +3289,39 @@ function registerInitCommand(yargs2) {
|
|
|
2795
3289
|
);
|
|
2796
3290
|
}
|
|
2797
3291
|
|
|
3292
|
+
// src/cli/commands/recap.ts
|
|
3293
|
+
init_esm_shims();
|
|
3294
|
+
async function handleRecap(ctx) {
|
|
3295
|
+
const snapshotInfo = await loadLatestSnapshot(ctx.root);
|
|
3296
|
+
const recap = buildRecap(ctx.state, snapshotInfo);
|
|
3297
|
+
return { output: formatRecap(recap, ctx.state, ctx.format) };
|
|
3298
|
+
}
|
|
3299
|
+
|
|
3300
|
+
// src/cli/commands/export.ts
|
|
3301
|
+
init_esm_shims();
|
|
3302
|
+
function handleExport(ctx, mode, phaseId) {
|
|
3303
|
+
if (mode === "phase") {
|
|
3304
|
+
if (!phaseId) {
|
|
3305
|
+
throw new CliValidationError("invalid_input", "Missing --phase value");
|
|
3306
|
+
}
|
|
3307
|
+
const phase = ctx.state.roadmap.phases.find((p) => p.id === phaseId);
|
|
3308
|
+
if (!phase) {
|
|
3309
|
+
throw new CliValidationError("not_found", `Phase "${phaseId}" not found in roadmap`);
|
|
3310
|
+
}
|
|
3311
|
+
}
|
|
3312
|
+
return { output: formatExport(ctx.state, mode, phaseId, ctx.format) };
|
|
3313
|
+
}
|
|
3314
|
+
|
|
3315
|
+
// src/cli/commands/snapshot.ts
|
|
3316
|
+
init_esm_shims();
|
|
3317
|
+
async function handleSnapshot(root, format) {
|
|
3318
|
+
let result;
|
|
3319
|
+
await withProjectLock(root, { strict: false }, async (loadResult) => {
|
|
3320
|
+
result = await saveSnapshot(root, loadResult);
|
|
3321
|
+
});
|
|
3322
|
+
return { output: formatSnapshotResult(result, format) };
|
|
3323
|
+
}
|
|
3324
|
+
|
|
2798
3325
|
// src/cli/register.ts
|
|
2799
3326
|
function registerStatusCommand(yargs2) {
|
|
2800
3327
|
return yargs2.command(
|
|
@@ -3787,9 +4314,89 @@ function registerPhaseCommand(yargs2) {
|
|
|
3787
4314
|
}
|
|
3788
4315
|
);
|
|
3789
4316
|
}
|
|
4317
|
+
function registerSnapshotCommand(yargs2) {
|
|
4318
|
+
return yargs2.command(
|
|
4319
|
+
"snapshot",
|
|
4320
|
+
"Save current project state for session diffs",
|
|
4321
|
+
(y) => addFormatOption(y),
|
|
4322
|
+
async (argv) => {
|
|
4323
|
+
const format = parseOutputFormat(argv.format);
|
|
4324
|
+
const root = (await Promise.resolve().then(() => (init_project_root_discovery(), project_root_discovery_exports))).discoverProjectRoot();
|
|
4325
|
+
if (!root) {
|
|
4326
|
+
writeOutput(
|
|
4327
|
+
formatError("not_found", "No .story/ project found.", format)
|
|
4328
|
+
);
|
|
4329
|
+
process.exitCode = ExitCode.USER_ERROR;
|
|
4330
|
+
return;
|
|
4331
|
+
}
|
|
4332
|
+
try {
|
|
4333
|
+
const result = await handleSnapshot(root, format);
|
|
4334
|
+
writeOutput(result.output);
|
|
4335
|
+
process.exitCode = result.exitCode ?? ExitCode.OK;
|
|
4336
|
+
} catch (err) {
|
|
4337
|
+
if (err instanceof CliValidationError) {
|
|
4338
|
+
writeOutput(formatError(err.code, err.message, format));
|
|
4339
|
+
process.exitCode = ExitCode.USER_ERROR;
|
|
4340
|
+
return;
|
|
4341
|
+
}
|
|
4342
|
+
const { ProjectLoaderError: ProjectLoaderError2 } = await Promise.resolve().then(() => (init_errors(), errors_exports));
|
|
4343
|
+
if (err instanceof ProjectLoaderError2) {
|
|
4344
|
+
writeOutput(formatError(err.code, err.message, format));
|
|
4345
|
+
process.exitCode = ExitCode.USER_ERROR;
|
|
4346
|
+
return;
|
|
4347
|
+
}
|
|
4348
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
4349
|
+
writeOutput(formatError("io_error", message, format));
|
|
4350
|
+
process.exitCode = ExitCode.USER_ERROR;
|
|
4351
|
+
}
|
|
4352
|
+
}
|
|
4353
|
+
);
|
|
4354
|
+
}
|
|
4355
|
+
function registerRecapCommand(yargs2) {
|
|
4356
|
+
return yargs2.command(
|
|
4357
|
+
"recap",
|
|
4358
|
+
"Session diff \u2014 changes since last snapshot + suggested actions",
|
|
4359
|
+
(y) => addFormatOption(y),
|
|
4360
|
+
async (argv) => {
|
|
4361
|
+
const format = parseOutputFormat(argv.format);
|
|
4362
|
+
await runReadCommand(format, handleRecap);
|
|
4363
|
+
}
|
|
4364
|
+
);
|
|
4365
|
+
}
|
|
4366
|
+
function registerExportCommand(yargs2) {
|
|
4367
|
+
return yargs2.command(
|
|
4368
|
+
"export",
|
|
4369
|
+
"Self-contained project document for sharing",
|
|
4370
|
+
(y) => addFormatOption(
|
|
4371
|
+
y.option("phase", {
|
|
4372
|
+
type: "string",
|
|
4373
|
+
describe: "Export a single phase by ID"
|
|
4374
|
+
}).option("all", {
|
|
4375
|
+
type: "boolean",
|
|
4376
|
+
describe: "Export entire project"
|
|
4377
|
+
}).conflicts("phase", "all").check((argv) => {
|
|
4378
|
+
if (!argv.phase && !argv.all) {
|
|
4379
|
+
throw new Error(
|
|
4380
|
+
"Specify either --phase <id> or --all"
|
|
4381
|
+
);
|
|
4382
|
+
}
|
|
4383
|
+
return true;
|
|
4384
|
+
})
|
|
4385
|
+
),
|
|
4386
|
+
async (argv) => {
|
|
4387
|
+
const format = parseOutputFormat(argv.format);
|
|
4388
|
+
const mode = argv.all ? "all" : "phase";
|
|
4389
|
+
const phaseId = argv.phase ?? null;
|
|
4390
|
+
await runReadCommand(
|
|
4391
|
+
format,
|
|
4392
|
+
(ctx) => handleExport(ctx, mode, phaseId)
|
|
4393
|
+
);
|
|
4394
|
+
}
|
|
4395
|
+
);
|
|
4396
|
+
}
|
|
3790
4397
|
|
|
3791
4398
|
// src/cli/index.ts
|
|
3792
|
-
var version = "0.1.
|
|
4399
|
+
var version = "0.1.4";
|
|
3793
4400
|
var HandledError = class extends Error {
|
|
3794
4401
|
constructor() {
|
|
3795
4402
|
super("HANDLED_ERROR");
|
|
@@ -3819,6 +4426,9 @@ cli = registerIssueCommand(cli);
|
|
|
3819
4426
|
cli = registerHandoverCommand(cli);
|
|
3820
4427
|
cli = registerBlockerCommand(cli);
|
|
3821
4428
|
cli = registerValidateCommand(cli);
|
|
4429
|
+
cli = registerSnapshotCommand(cli);
|
|
4430
|
+
cli = registerRecapCommand(cli);
|
|
4431
|
+
cli = registerExportCommand(cli);
|
|
3822
4432
|
function handleUnexpectedError(err) {
|
|
3823
4433
|
if (err instanceof HandledError) return;
|
|
3824
4434
|
const message = err instanceof Error ? err.message : String(err);
|