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