@anthropologies/claudestory 0.1.7 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -2
- package/dist/cli.js +2086 -153
- package/dist/index.d.ts +170 -1
- package/dist/index.js +620 -101
- 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,8 +1504,285 @@ function dfsBlocked(id, state, visited, inStack, findings) {
|
|
|
1381
1504
|
visited.add(id);
|
|
1382
1505
|
}
|
|
1383
1506
|
|
|
1507
|
+
// src/core/recommend.ts
|
|
1508
|
+
var SEVERITY_RANK = {
|
|
1509
|
+
critical: 4,
|
|
1510
|
+
high: 3,
|
|
1511
|
+
medium: 2,
|
|
1512
|
+
low: 1
|
|
1513
|
+
};
|
|
1514
|
+
var PHASE_DISTANCE_PENALTY = 100;
|
|
1515
|
+
var MAX_PHASE_PENALTY = 400;
|
|
1516
|
+
var CATEGORY_PRIORITY = {
|
|
1517
|
+
validation_errors: 1,
|
|
1518
|
+
critical_issue: 2,
|
|
1519
|
+
inprogress_ticket: 3,
|
|
1520
|
+
high_impact_unblock: 4,
|
|
1521
|
+
near_complete_umbrella: 5,
|
|
1522
|
+
phase_momentum: 6,
|
|
1523
|
+
quick_win: 7,
|
|
1524
|
+
open_issue: 8
|
|
1525
|
+
};
|
|
1526
|
+
function recommend(state, count) {
|
|
1527
|
+
const effectiveCount = Math.max(1, Math.min(10, count));
|
|
1528
|
+
const dedup = /* @__PURE__ */ new Map();
|
|
1529
|
+
const phaseIndex = buildPhaseIndex(state);
|
|
1530
|
+
const generators = [
|
|
1531
|
+
() => generateValidationSuggestions(state),
|
|
1532
|
+
() => generateCriticalIssues(state),
|
|
1533
|
+
() => generateInProgressTickets(state, phaseIndex),
|
|
1534
|
+
() => generateHighImpactUnblocks(state),
|
|
1535
|
+
() => generateNearCompleteUmbrellas(state, phaseIndex),
|
|
1536
|
+
() => generatePhaseMomentum(state),
|
|
1537
|
+
() => generateQuickWins(state, phaseIndex),
|
|
1538
|
+
() => generateOpenIssues(state)
|
|
1539
|
+
];
|
|
1540
|
+
for (const gen of generators) {
|
|
1541
|
+
for (const rec of gen()) {
|
|
1542
|
+
const existing = dedup.get(rec.id);
|
|
1543
|
+
if (!existing || rec.score > existing.score) {
|
|
1544
|
+
dedup.set(rec.id, rec);
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
const curPhase = currentPhase(state);
|
|
1549
|
+
const curPhaseIdx = curPhase ? phaseIndex.get(curPhase.id) ?? 0 : 0;
|
|
1550
|
+
for (const [id, rec] of dedup) {
|
|
1551
|
+
if (rec.kind !== "ticket") continue;
|
|
1552
|
+
const ticket = state.ticketByID(id);
|
|
1553
|
+
if (!ticket || ticket.phase == null) continue;
|
|
1554
|
+
const ticketPhaseIdx = phaseIndex.get(ticket.phase);
|
|
1555
|
+
if (ticketPhaseIdx === void 0) continue;
|
|
1556
|
+
const phasesAhead = ticketPhaseIdx - curPhaseIdx;
|
|
1557
|
+
if (phasesAhead > 0) {
|
|
1558
|
+
const penalty = Math.min(phasesAhead * PHASE_DISTANCE_PENALTY, MAX_PHASE_PENALTY);
|
|
1559
|
+
dedup.set(id, {
|
|
1560
|
+
...rec,
|
|
1561
|
+
score: rec.score - penalty,
|
|
1562
|
+
reason: rec.reason + " (future phase)"
|
|
1563
|
+
});
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
const all = [...dedup.values()].sort((a, b) => {
|
|
1567
|
+
if (b.score !== a.score) return b.score - a.score;
|
|
1568
|
+
const catDiff = CATEGORY_PRIORITY[a.category] - CATEGORY_PRIORITY[b.category];
|
|
1569
|
+
if (catDiff !== 0) return catDiff;
|
|
1570
|
+
return a.id.localeCompare(b.id);
|
|
1571
|
+
});
|
|
1572
|
+
return {
|
|
1573
|
+
recommendations: all.slice(0, effectiveCount),
|
|
1574
|
+
totalCandidates: all.length
|
|
1575
|
+
};
|
|
1576
|
+
}
|
|
1577
|
+
function generateValidationSuggestions(state) {
|
|
1578
|
+
const result = validateProject(state);
|
|
1579
|
+
if (result.errorCount === 0) return [];
|
|
1580
|
+
return [
|
|
1581
|
+
{
|
|
1582
|
+
id: "validate",
|
|
1583
|
+
kind: "action",
|
|
1584
|
+
title: "Run claudestory validate",
|
|
1585
|
+
category: "validation_errors",
|
|
1586
|
+
reason: `${result.errorCount} validation error${result.errorCount === 1 ? "" : "s"} \u2014 fix before other work`,
|
|
1587
|
+
score: 1e3
|
|
1588
|
+
}
|
|
1589
|
+
];
|
|
1590
|
+
}
|
|
1591
|
+
function generateCriticalIssues(state) {
|
|
1592
|
+
const issues = state.issues.filter(
|
|
1593
|
+
(i) => i.status !== "resolved" && (i.severity === "critical" || i.severity === "high")
|
|
1594
|
+
).sort((a, b) => {
|
|
1595
|
+
const sevDiff = SEVERITY_RANK[b.severity] - SEVERITY_RANK[a.severity];
|
|
1596
|
+
if (sevDiff !== 0) return sevDiff;
|
|
1597
|
+
return b.discoveredDate.localeCompare(a.discoveredDate);
|
|
1598
|
+
});
|
|
1599
|
+
return issues.map((issue, index) => ({
|
|
1600
|
+
id: issue.id,
|
|
1601
|
+
kind: "issue",
|
|
1602
|
+
title: issue.title,
|
|
1603
|
+
category: "critical_issue",
|
|
1604
|
+
reason: issue.status === "inprogress" ? `${capitalize(issue.severity)} severity issue \u2014 in-progress, ensure it's being addressed` : `${capitalize(issue.severity)} severity issue \u2014 address before new features`,
|
|
1605
|
+
score: 900 - Math.min(index, 99)
|
|
1606
|
+
}));
|
|
1607
|
+
}
|
|
1608
|
+
function generateInProgressTickets(state, phaseIndex) {
|
|
1609
|
+
const tickets = state.leafTickets.filter(
|
|
1610
|
+
(t) => t.status === "inprogress"
|
|
1611
|
+
);
|
|
1612
|
+
const sorted = sortByPhaseAndOrder(tickets, phaseIndex);
|
|
1613
|
+
return sorted.map((ticket, index) => ({
|
|
1614
|
+
id: ticket.id,
|
|
1615
|
+
kind: "ticket",
|
|
1616
|
+
title: ticket.title,
|
|
1617
|
+
category: "inprogress_ticket",
|
|
1618
|
+
reason: "In-progress \u2014 finish what's started",
|
|
1619
|
+
score: 800 - Math.min(index, 99)
|
|
1620
|
+
}));
|
|
1621
|
+
}
|
|
1622
|
+
function generateHighImpactUnblocks(state) {
|
|
1623
|
+
const candidates = [];
|
|
1624
|
+
for (const ticket of state.leafTickets) {
|
|
1625
|
+
if (ticket.status === "complete") continue;
|
|
1626
|
+
if (state.isBlocked(ticket)) continue;
|
|
1627
|
+
const wouldUnblock = ticketsUnblockedBy(ticket.id, state);
|
|
1628
|
+
if (wouldUnblock.length >= 2) {
|
|
1629
|
+
candidates.push({ ticket, unblockCount: wouldUnblock.length });
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
candidates.sort((a, b) => b.unblockCount - a.unblockCount);
|
|
1633
|
+
return candidates.map(({ ticket, unblockCount }, index) => ({
|
|
1634
|
+
id: ticket.id,
|
|
1635
|
+
kind: "ticket",
|
|
1636
|
+
title: ticket.title,
|
|
1637
|
+
category: "high_impact_unblock",
|
|
1638
|
+
reason: `Completing this unblocks ${unblockCount} other ticket${unblockCount === 1 ? "" : "s"}`,
|
|
1639
|
+
score: 700 - Math.min(index, 99)
|
|
1640
|
+
}));
|
|
1641
|
+
}
|
|
1642
|
+
function generateNearCompleteUmbrellas(state, phaseIndex) {
|
|
1643
|
+
const candidates = [];
|
|
1644
|
+
for (const umbrellaId of state.umbrellaIDs) {
|
|
1645
|
+
const progress = umbrellaProgress(umbrellaId, state);
|
|
1646
|
+
if (!progress) continue;
|
|
1647
|
+
if (progress.total < 2) continue;
|
|
1648
|
+
if (progress.status === "complete") continue;
|
|
1649
|
+
const ratio = progress.complete / progress.total;
|
|
1650
|
+
if (ratio < 0.8) continue;
|
|
1651
|
+
const leaves = descendantLeaves(umbrellaId, state);
|
|
1652
|
+
const incomplete = leaves.filter((t) => t.status !== "complete");
|
|
1653
|
+
const sorted = sortByPhaseAndOrder(incomplete, phaseIndex);
|
|
1654
|
+
if (sorted.length === 0) continue;
|
|
1655
|
+
const umbrella = state.ticketByID(umbrellaId);
|
|
1656
|
+
candidates.push({
|
|
1657
|
+
umbrellaId,
|
|
1658
|
+
umbrellaTitle: umbrella?.title ?? umbrellaId,
|
|
1659
|
+
firstIncompleteLeaf: sorted[0],
|
|
1660
|
+
complete: progress.complete,
|
|
1661
|
+
total: progress.total,
|
|
1662
|
+
ratio
|
|
1663
|
+
});
|
|
1664
|
+
}
|
|
1665
|
+
candidates.sort((a, b) => b.ratio - a.ratio);
|
|
1666
|
+
return candidates.map((c, index) => ({
|
|
1667
|
+
id: c.firstIncompleteLeaf.id,
|
|
1668
|
+
kind: "ticket",
|
|
1669
|
+
title: c.firstIncompleteLeaf.title,
|
|
1670
|
+
category: "near_complete_umbrella",
|
|
1671
|
+
reason: `${c.complete}/${c.total} complete in umbrella ${c.umbrellaId} \u2014 close it out`,
|
|
1672
|
+
score: 600 - Math.min(index, 99)
|
|
1673
|
+
}));
|
|
1674
|
+
}
|
|
1675
|
+
function generatePhaseMomentum(state) {
|
|
1676
|
+
const outcome = nextTicket(state);
|
|
1677
|
+
if (outcome.kind !== "found") return [];
|
|
1678
|
+
const ticket = outcome.ticket;
|
|
1679
|
+
return [
|
|
1680
|
+
{
|
|
1681
|
+
id: ticket.id,
|
|
1682
|
+
kind: "ticket",
|
|
1683
|
+
title: ticket.title,
|
|
1684
|
+
category: "phase_momentum",
|
|
1685
|
+
reason: `Next in phase order (${ticket.phase ?? "none"})`,
|
|
1686
|
+
score: 500
|
|
1687
|
+
}
|
|
1688
|
+
];
|
|
1689
|
+
}
|
|
1690
|
+
function generateQuickWins(state, phaseIndex) {
|
|
1691
|
+
const tickets = state.leafTickets.filter(
|
|
1692
|
+
(t) => t.status === "open" && t.type === "chore" && !state.isBlocked(t)
|
|
1693
|
+
);
|
|
1694
|
+
const sorted = sortByPhaseAndOrder(tickets, phaseIndex);
|
|
1695
|
+
return sorted.map((ticket, index) => ({
|
|
1696
|
+
id: ticket.id,
|
|
1697
|
+
kind: "ticket",
|
|
1698
|
+
title: ticket.title,
|
|
1699
|
+
category: "quick_win",
|
|
1700
|
+
reason: "Chore \u2014 quick win",
|
|
1701
|
+
score: 400 - Math.min(index, 99)
|
|
1702
|
+
}));
|
|
1703
|
+
}
|
|
1704
|
+
function generateOpenIssues(state) {
|
|
1705
|
+
const issues = state.issues.filter(
|
|
1706
|
+
(i) => i.status !== "resolved" && (i.severity === "medium" || i.severity === "low")
|
|
1707
|
+
).sort((a, b) => {
|
|
1708
|
+
const sevDiff = SEVERITY_RANK[b.severity] - SEVERITY_RANK[a.severity];
|
|
1709
|
+
if (sevDiff !== 0) return sevDiff;
|
|
1710
|
+
return b.discoveredDate.localeCompare(a.discoveredDate);
|
|
1711
|
+
});
|
|
1712
|
+
return issues.map((issue, index) => ({
|
|
1713
|
+
id: issue.id,
|
|
1714
|
+
kind: "issue",
|
|
1715
|
+
title: issue.title,
|
|
1716
|
+
category: "open_issue",
|
|
1717
|
+
reason: issue.status === "inprogress" ? `${capitalize(issue.severity)} severity issue \u2014 in-progress` : `${capitalize(issue.severity)} severity issue`,
|
|
1718
|
+
score: 300 - Math.min(index, 99)
|
|
1719
|
+
}));
|
|
1720
|
+
}
|
|
1721
|
+
function capitalize(s) {
|
|
1722
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
1723
|
+
}
|
|
1724
|
+
function buildPhaseIndex(state) {
|
|
1725
|
+
const index = /* @__PURE__ */ new Map();
|
|
1726
|
+
state.roadmap.phases.forEach((p, i) => index.set(p.id, i));
|
|
1727
|
+
return index;
|
|
1728
|
+
}
|
|
1729
|
+
function sortByPhaseAndOrder(tickets, phaseIndex) {
|
|
1730
|
+
return [...tickets].sort((a, b) => {
|
|
1731
|
+
const aPhase = (a.phase != null ? phaseIndex.get(a.phase) : void 0) ?? Number.MAX_SAFE_INTEGER;
|
|
1732
|
+
const bPhase = (b.phase != null ? phaseIndex.get(b.phase) : void 0) ?? Number.MAX_SAFE_INTEGER;
|
|
1733
|
+
if (aPhase !== bPhase) return aPhase - bPhase;
|
|
1734
|
+
return a.order - b.order;
|
|
1735
|
+
});
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
// src/core/id-allocation.ts
|
|
1739
|
+
var TICKET_NUMERIC_REGEX = /^T-(\d+)[a-z]?$/;
|
|
1740
|
+
var ISSUE_NUMERIC_REGEX = /^ISS-(\d+)$/;
|
|
1741
|
+
var NOTE_NUMERIC_REGEX = /^N-(\d+)$/;
|
|
1742
|
+
function nextTicketID(tickets) {
|
|
1743
|
+
let max = 0;
|
|
1744
|
+
for (const t of tickets) {
|
|
1745
|
+
if (!TICKET_ID_REGEX.test(t.id)) continue;
|
|
1746
|
+
const match = t.id.match(TICKET_NUMERIC_REGEX);
|
|
1747
|
+
if (match?.[1]) {
|
|
1748
|
+
const num = parseInt(match[1], 10);
|
|
1749
|
+
if (num > max) max = num;
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
return `T-${String(max + 1).padStart(3, "0")}`;
|
|
1753
|
+
}
|
|
1754
|
+
function nextIssueID(issues) {
|
|
1755
|
+
let max = 0;
|
|
1756
|
+
for (const i of issues) {
|
|
1757
|
+
if (!ISSUE_ID_REGEX.test(i.id)) continue;
|
|
1758
|
+
const match = i.id.match(ISSUE_NUMERIC_REGEX);
|
|
1759
|
+
if (match?.[1]) {
|
|
1760
|
+
const num = parseInt(match[1], 10);
|
|
1761
|
+
if (num > max) max = num;
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
return `ISS-${String(max + 1).padStart(3, "0")}`;
|
|
1765
|
+
}
|
|
1766
|
+
function nextNoteID(notes) {
|
|
1767
|
+
let max = 0;
|
|
1768
|
+
for (const n of notes) {
|
|
1769
|
+
if (!NOTE_ID_REGEX.test(n.id)) continue;
|
|
1770
|
+
const match = n.id.match(NOTE_NUMERIC_REGEX);
|
|
1771
|
+
if (match?.[1]) {
|
|
1772
|
+
const num = parseInt(match[1], 10);
|
|
1773
|
+
if (num > max) max = num;
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
return `N-${String(max + 1).padStart(3, "0")}`;
|
|
1777
|
+
}
|
|
1778
|
+
function nextOrder(phaseId, state) {
|
|
1779
|
+
const tickets = state.phaseTickets(phaseId);
|
|
1780
|
+
if (tickets.length === 0) return 10;
|
|
1781
|
+
return tickets[tickets.length - 1].order + 10;
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1384
1784
|
// src/core/init.ts
|
|
1385
|
-
import { mkdir, stat as stat2 } from "fs/promises";
|
|
1785
|
+
import { mkdir as mkdir2, stat as stat2 } from "fs/promises";
|
|
1386
1786
|
import { join as join4, resolve as resolve3 } from "path";
|
|
1387
1787
|
async function initProject(root, options) {
|
|
1388
1788
|
const absRoot = resolve3(root);
|
|
@@ -1406,9 +1806,18 @@ async function initProject(root, options) {
|
|
|
1406
1806
|
".story/ already exists. Use --force to overwrite config and roadmap."
|
|
1407
1807
|
);
|
|
1408
1808
|
}
|
|
1409
|
-
await
|
|
1410
|
-
await
|
|
1411
|
-
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 });
|
|
1813
|
+
const created = [
|
|
1814
|
+
".story/config.json",
|
|
1815
|
+
".story/roadmap.json",
|
|
1816
|
+
".story/tickets/",
|
|
1817
|
+
".story/issues/",
|
|
1818
|
+
".story/handovers/",
|
|
1819
|
+
".story/notes/"
|
|
1820
|
+
];
|
|
1412
1821
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1413
1822
|
const config = {
|
|
1414
1823
|
version: 2,
|
|
@@ -1453,42 +1862,37 @@ async function initProject(root, options) {
|
|
|
1453
1862
|
}
|
|
1454
1863
|
return {
|
|
1455
1864
|
root: absRoot,
|
|
1456
|
-
created
|
|
1457
|
-
".story/config.json",
|
|
1458
|
-
".story/roadmap.json",
|
|
1459
|
-
".story/tickets/",
|
|
1460
|
-
".story/issues/",
|
|
1461
|
-
".story/handovers/"
|
|
1462
|
-
],
|
|
1865
|
+
created,
|
|
1463
1866
|
warnings
|
|
1464
1867
|
};
|
|
1465
1868
|
}
|
|
1466
1869
|
|
|
1467
1870
|
// src/core/snapshot.ts
|
|
1468
|
-
import { readdir as readdir3, readFile as readFile3, mkdir as
|
|
1871
|
+
import { readdir as readdir3, readFile as readFile3, mkdir as mkdir3, unlink as unlink2 } from "fs/promises";
|
|
1469
1872
|
import { existsSync as existsSync4 } from "fs";
|
|
1470
1873
|
import { join as join5, resolve as resolve4 } from "path";
|
|
1471
|
-
import { z as
|
|
1472
|
-
var LoadWarningSchema =
|
|
1473
|
-
type:
|
|
1474
|
-
file:
|
|
1475
|
-
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()
|
|
1476
1879
|
});
|
|
1477
|
-
var SnapshotV1Schema =
|
|
1478
|
-
version:
|
|
1479
|
-
createdAt:
|
|
1480
|
-
project:
|
|
1880
|
+
var SnapshotV1Schema = z7.object({
|
|
1881
|
+
version: z7.literal(1),
|
|
1882
|
+
createdAt: z7.string().datetime({ offset: true }),
|
|
1883
|
+
project: z7.string(),
|
|
1481
1884
|
config: ConfigSchema,
|
|
1482
1885
|
roadmap: RoadmapSchema,
|
|
1483
|
-
tickets:
|
|
1484
|
-
issues:
|
|
1485
|
-
|
|
1886
|
+
tickets: z7.array(TicketSchema),
|
|
1887
|
+
issues: z7.array(IssueSchema),
|
|
1888
|
+
notes: z7.array(NoteSchema).optional().default([]),
|
|
1889
|
+
warnings: z7.array(LoadWarningSchema).optional()
|
|
1486
1890
|
});
|
|
1487
1891
|
var MAX_SNAPSHOTS = 20;
|
|
1488
1892
|
async function saveSnapshot(root, loadResult) {
|
|
1489
1893
|
const absRoot = resolve4(root);
|
|
1490
1894
|
const snapshotsDir = join5(absRoot, ".story", "snapshots");
|
|
1491
|
-
await
|
|
1895
|
+
await mkdir3(snapshotsDir, { recursive: true });
|
|
1492
1896
|
const { state, warnings } = loadResult;
|
|
1493
1897
|
const now = /* @__PURE__ */ new Date();
|
|
1494
1898
|
const filename = formatSnapshotFilename(now);
|
|
@@ -1500,6 +1904,7 @@ async function saveSnapshot(root, loadResult) {
|
|
|
1500
1904
|
roadmap: state.roadmap,
|
|
1501
1905
|
tickets: [...state.tickets],
|
|
1502
1906
|
issues: [...state.issues],
|
|
1907
|
+
notes: [...state.notes],
|
|
1503
1908
|
...warnings.length > 0 ? {
|
|
1504
1909
|
warnings: warnings.map((w) => ({
|
|
1505
1910
|
type: w.type,
|
|
@@ -1540,12 +1945,18 @@ function diffStates(snapshotState, currentState) {
|
|
|
1540
1945
|
const ticketsAdded = [];
|
|
1541
1946
|
const ticketsRemoved = [];
|
|
1542
1947
|
const ticketsStatusChanged = [];
|
|
1948
|
+
const ticketsDescriptionChanged = [];
|
|
1543
1949
|
for (const [id, cur] of curTickets) {
|
|
1544
1950
|
const snap = snapTickets.get(id);
|
|
1545
1951
|
if (!snap) {
|
|
1546
1952
|
ticketsAdded.push({ id, title: cur.title });
|
|
1547
|
-
} else
|
|
1548
|
-
|
|
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
|
+
}
|
|
1549
1960
|
}
|
|
1550
1961
|
}
|
|
1551
1962
|
for (const [id, snap] of snapTickets) {
|
|
@@ -1558,15 +1969,21 @@ function diffStates(snapshotState, currentState) {
|
|
|
1558
1969
|
const issuesAdded = [];
|
|
1559
1970
|
const issuesResolved = [];
|
|
1560
1971
|
const issuesStatusChanged = [];
|
|
1972
|
+
const issuesImpactChanged = [];
|
|
1561
1973
|
for (const [id, cur] of curIssues) {
|
|
1562
1974
|
const snap = snapIssues.get(id);
|
|
1563
1975
|
if (!snap) {
|
|
1564
1976
|
issuesAdded.push({ id, title: cur.title });
|
|
1565
|
-
} else
|
|
1566
|
-
if (
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
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 });
|
|
1570
1987
|
}
|
|
1571
1988
|
}
|
|
1572
1989
|
}
|
|
@@ -1615,11 +2032,37 @@ function diffStates(snapshotState, currentState) {
|
|
|
1615
2032
|
phasesRemoved.push({ id, name: snapPhase.name });
|
|
1616
2033
|
}
|
|
1617
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
|
+
}
|
|
1618
2060
|
return {
|
|
1619
|
-
tickets: { added: ticketsAdded, removed: ticketsRemoved, statusChanged: ticketsStatusChanged },
|
|
1620
|
-
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 },
|
|
1621
2063
|
blockers: { added: blockersAdded, cleared: blockersCleared },
|
|
1622
|
-
phases: { added: phasesAdded, removed: phasesRemoved, statusChanged: phasesStatusChanged }
|
|
2064
|
+
phases: { added: phasesAdded, removed: phasesRemoved, statusChanged: phasesStatusChanged },
|
|
2065
|
+
notes: { added: notesAdded, removed: notesRemoved, updated: notesUpdated }
|
|
1623
2066
|
};
|
|
1624
2067
|
}
|
|
1625
2068
|
function buildRecap(currentState, snapshotInfo) {
|
|
@@ -1644,6 +2087,7 @@ function buildRecap(currentState, snapshotInfo) {
|
|
|
1644
2087
|
const snapshotState = new ProjectState({
|
|
1645
2088
|
tickets: snapshot.tickets,
|
|
1646
2089
|
issues: snapshot.issues,
|
|
2090
|
+
notes: snapshot.notes ?? [],
|
|
1647
2091
|
roadmap: snapshot.roadmap,
|
|
1648
2092
|
config: snapshot.config,
|
|
1649
2093
|
handoverFilenames: []
|
|
@@ -1742,6 +2186,8 @@ function formatStatus(state, format) {
|
|
|
1742
2186
|
openTickets: state.leafTicketCount - state.completeLeafTicketCount,
|
|
1743
2187
|
blockedTickets: state.blockedCount,
|
|
1744
2188
|
openIssues: state.openIssueCount,
|
|
2189
|
+
activeNotes: state.activeNoteCount,
|
|
2190
|
+
archivedNotes: state.archivedNoteCount,
|
|
1745
2191
|
handovers: state.handoverFilenames.length,
|
|
1746
2192
|
phases: phases.map((p) => ({
|
|
1747
2193
|
id: p.phase.id,
|
|
@@ -1758,6 +2204,7 @@ function formatStatus(state, format) {
|
|
|
1758
2204
|
"",
|
|
1759
2205
|
`Tickets: ${state.completeLeafTicketCount}/${state.leafTicketCount} complete, ${state.blockedCount} blocked`,
|
|
1760
2206
|
`Issues: ${state.openIssueCount} open`,
|
|
2207
|
+
`Notes: ${state.activeNoteCount} active, ${state.archivedNoteCount} archived`,
|
|
1761
2208
|
`Handovers: ${state.handoverFilenames.length}`,
|
|
1762
2209
|
"",
|
|
1763
2210
|
"## Phases",
|
|
@@ -1982,6 +2429,7 @@ function formatInitResult(result, format) {
|
|
|
1982
2429
|
if (result.warnings.length > 0) {
|
|
1983
2430
|
lines.push("", `Warning: ${result.warnings.length} corrupt file(s) found. Run \`claudestory validate\` to inspect.`);
|
|
1984
2431
|
}
|
|
2432
|
+
lines.push("", "Tip: Run `claudestory setup-skill` to install the /story skill for Claude Code.");
|
|
1985
2433
|
return lines.join("\n");
|
|
1986
2434
|
}
|
|
1987
2435
|
function formatHandoverList(filenames, format) {
|
|
@@ -2045,7 +2493,7 @@ function formatRecap(recap, state, format) {
|
|
|
2045
2493
|
}
|
|
2046
2494
|
}
|
|
2047
2495
|
const ticketChanges = changes.tickets;
|
|
2048
|
-
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) {
|
|
2049
2497
|
lines.push("");
|
|
2050
2498
|
lines.push("## Tickets");
|
|
2051
2499
|
for (const t of ticketChanges.statusChanged) {
|
|
@@ -2057,9 +2505,12 @@ function formatRecap(recap, state, format) {
|
|
|
2057
2505
|
for (const t of ticketChanges.removed) {
|
|
2058
2506
|
lines.push(`- ${t.id}: ${escapeMarkdownInline(t.title)} \u2014 **removed**`);
|
|
2059
2507
|
}
|
|
2508
|
+
for (const t of ticketChanges.descriptionChanged) {
|
|
2509
|
+
lines.push(`- ${t.id}: description updated`);
|
|
2510
|
+
}
|
|
2060
2511
|
}
|
|
2061
2512
|
const issueChanges = changes.issues;
|
|
2062
|
-
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) {
|
|
2063
2514
|
lines.push("");
|
|
2064
2515
|
lines.push("## Issues");
|
|
2065
2516
|
for (const i of issueChanges.resolved) {
|
|
@@ -2071,6 +2522,9 @@ function formatRecap(recap, state, format) {
|
|
|
2071
2522
|
for (const i of issueChanges.added) {
|
|
2072
2523
|
lines.push(`- ${i.id}: ${escapeMarkdownInline(i.title)} \u2014 **new**`);
|
|
2073
2524
|
}
|
|
2525
|
+
for (const i of issueChanges.impactChanged) {
|
|
2526
|
+
lines.push(`- ${i.id}: impact updated`);
|
|
2527
|
+
}
|
|
2074
2528
|
}
|
|
2075
2529
|
if (changes.blockers.added.length > 0 || changes.blockers.cleared.length > 0) {
|
|
2076
2530
|
lines.push("");
|
|
@@ -2082,6 +2536,19 @@ function formatRecap(recap, state, format) {
|
|
|
2082
2536
|
lines.push(`- ${escapeMarkdownInline(name)} \u2014 **new**`);
|
|
2083
2537
|
}
|
|
2084
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
|
+
}
|
|
2085
2552
|
}
|
|
2086
2553
|
}
|
|
2087
2554
|
const actions = recap.suggestedActions;
|
|
@@ -2220,6 +2687,12 @@ function formatFullExport(state, format) {
|
|
|
2220
2687
|
severity: i.severity,
|
|
2221
2688
|
status: i.status
|
|
2222
2689
|
})),
|
|
2690
|
+
notes: state.notes.map((n) => ({
|
|
2691
|
+
id: n.id,
|
|
2692
|
+
title: n.title,
|
|
2693
|
+
status: n.status,
|
|
2694
|
+
tags: n.tags
|
|
2695
|
+
})),
|
|
2223
2696
|
blockers: state.roadmap.blockers.map((b) => ({
|
|
2224
2697
|
name: b.name,
|
|
2225
2698
|
cleared: isBlockerCleared(b),
|
|
@@ -2235,6 +2708,7 @@ function formatFullExport(state, format) {
|
|
|
2235
2708
|
lines.push("");
|
|
2236
2709
|
lines.push(`Tickets: ${state.completeLeafTicketCount}/${state.leafTicketCount} complete`);
|
|
2237
2710
|
lines.push(`Issues: ${state.openIssueCount} open`);
|
|
2711
|
+
lines.push(`Notes: ${state.activeNoteCount} active, ${state.archivedNoteCount} archived`);
|
|
2238
2712
|
lines.push("");
|
|
2239
2713
|
lines.push("## Phases");
|
|
2240
2714
|
for (const p of phases) {
|
|
@@ -2261,6 +2735,16 @@ function formatFullExport(state, format) {
|
|
|
2261
2735
|
lines.push(`- ${i.id} [${i.severity}]: ${escapeMarkdownInline(i.title)}${resolved}`);
|
|
2262
2736
|
}
|
|
2263
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
|
+
}
|
|
2264
2748
|
const blockers = state.roadmap.blockers;
|
|
2265
2749
|
if (blockers.length > 0) {
|
|
2266
2750
|
lines.push("");
|
|
@@ -2273,7 +2757,7 @@ function formatFullExport(state, format) {
|
|
|
2273
2757
|
return lines.join("\n");
|
|
2274
2758
|
}
|
|
2275
2759
|
function hasAnyChanges(diff) {
|
|
2276
|
-
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;
|
|
2277
2761
|
}
|
|
2278
2762
|
function truncate(text, maxLen) {
|
|
2279
2763
|
if (text.length <= maxLen) return text;
|
|
@@ -2284,6 +2768,29 @@ function formatTicketOneLiner(t, state) {
|
|
|
2284
2768
|
const blocked = state.isBlocked(t) ? " [BLOCKED]" : "";
|
|
2285
2769
|
return `${status} ${t.id}: ${escapeMarkdownInline(t.title)}${blocked}`;
|
|
2286
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
|
+
}
|
|
2287
2794
|
export {
|
|
2288
2795
|
BlockerSchema,
|
|
2289
2796
|
CURRENT_SCHEMA_VERSION,
|
|
@@ -2299,6 +2806,10 @@ export {
|
|
|
2299
2806
|
ISSUE_STATUSES,
|
|
2300
2807
|
IssueIdSchema,
|
|
2301
2808
|
IssueSchema,
|
|
2809
|
+
NOTE_ID_REGEX,
|
|
2810
|
+
NOTE_STATUSES,
|
|
2811
|
+
NoteIdSchema,
|
|
2812
|
+
NoteSchema,
|
|
2302
2813
|
OUTPUT_FORMATS,
|
|
2303
2814
|
PhaseSchema,
|
|
2304
2815
|
ProjectLoaderError,
|
|
@@ -2315,7 +2826,9 @@ export {
|
|
|
2315
2826
|
buildRecap,
|
|
2316
2827
|
currentPhase,
|
|
2317
2828
|
deleteIssue,
|
|
2829
|
+
deleteNote,
|
|
2318
2830
|
deleteTicket,
|
|
2831
|
+
descendantLeaves,
|
|
2319
2832
|
diffStates,
|
|
2320
2833
|
discoverProjectRoot,
|
|
2321
2834
|
errorEnvelope,
|
|
@@ -2336,6 +2849,7 @@ export {
|
|
|
2336
2849
|
formatPhaseList,
|
|
2337
2850
|
formatPhaseTickets,
|
|
2338
2851
|
formatRecap,
|
|
2852
|
+
formatRecommendations,
|
|
2339
2853
|
formatSnapshotResult,
|
|
2340
2854
|
formatStatus,
|
|
2341
2855
|
formatTicket,
|
|
@@ -2349,12 +2863,15 @@ export {
|
|
|
2349
2863
|
loadProject,
|
|
2350
2864
|
mergeValidation,
|
|
2351
2865
|
nextIssueID,
|
|
2866
|
+
nextNoteID,
|
|
2352
2867
|
nextOrder,
|
|
2353
2868
|
nextTicket,
|
|
2354
2869
|
nextTicketID,
|
|
2870
|
+
nextTickets,
|
|
2355
2871
|
partialEnvelope,
|
|
2356
2872
|
phasesWithStatus,
|
|
2357
2873
|
readHandover,
|
|
2874
|
+
recommend,
|
|
2358
2875
|
runTransaction,
|
|
2359
2876
|
runTransactionUnlocked,
|
|
2360
2877
|
saveSnapshot,
|
|
@@ -2368,6 +2885,8 @@ export {
|
|
|
2368
2885
|
writeConfig,
|
|
2369
2886
|
writeIssue,
|
|
2370
2887
|
writeIssueUnlocked,
|
|
2888
|
+
writeNote,
|
|
2889
|
+
writeNoteUnlocked,
|
|
2371
2890
|
writeRoadmap,
|
|
2372
2891
|
writeRoadmapUnlocked,
|
|
2373
2892
|
writeTicket,
|