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