@anthropologies/claudestory 0.1.10 → 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +648 -159
- package/dist/index.d.ts +24 -16
- package/dist/index.js +29 -12
- package/dist/mcp.js +728 -60
- package/package.json +1 -1
- package/src/skill/SKILL.md +130 -2
- package/src/skill/reference.md +15 -6
package/dist/mcp.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
// src/mcp/index.ts
|
|
4
4
|
import { realpathSync, existsSync as existsSync6 } from "fs";
|
|
5
|
-
import { resolve as
|
|
5
|
+
import { resolve as resolve8, join as join9, isAbsolute } from "path";
|
|
6
|
+
import { z as z9 } from "zod";
|
|
6
7
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7
8
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
8
9
|
|
|
@@ -155,15 +156,9 @@ var IssueSchema = z3.object({
|
|
|
155
156
|
import { z as z4 } from "zod";
|
|
156
157
|
var NoteSchema = z4.object({
|
|
157
158
|
id: NoteIdSchema,
|
|
158
|
-
title: z4.
|
|
159
|
+
title: z4.string().nullable(),
|
|
159
160
|
content: z4.string().refine((v) => v.trim().length > 0, "Content cannot be empty"),
|
|
160
|
-
tags: z4.
|
|
161
|
-
(v) => {
|
|
162
|
-
const raw = Array.isArray(v) ? v : [];
|
|
163
|
-
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);
|
|
164
|
-
},
|
|
165
|
-
z4.array(z4.string())
|
|
166
|
-
),
|
|
161
|
+
tags: z4.array(z4.string()),
|
|
167
162
|
status: z4.enum(NOTE_STATUSES),
|
|
168
163
|
createdDate: DateSchema,
|
|
169
164
|
updatedDate: DateSchema
|
|
@@ -589,6 +584,12 @@ async function writeTicketUnlocked(ticket, root) {
|
|
|
589
584
|
const json = serializeJSON(parsed);
|
|
590
585
|
await atomicWrite(targetPath, json);
|
|
591
586
|
}
|
|
587
|
+
async function writeTicket(ticket, root) {
|
|
588
|
+
const wrapDir = resolve2(root, ".story");
|
|
589
|
+
await withLock(wrapDir, async () => {
|
|
590
|
+
await writeTicketUnlocked(ticket, root);
|
|
591
|
+
});
|
|
592
|
+
}
|
|
592
593
|
async function writeIssueUnlocked(issue, root) {
|
|
593
594
|
const parsed = IssueSchema.parse(issue);
|
|
594
595
|
if (!ISSUE_ID_REGEX.test(parsed.id)) {
|
|
@@ -603,6 +604,104 @@ async function writeIssueUnlocked(issue, root) {
|
|
|
603
604
|
const json = serializeJSON(parsed);
|
|
604
605
|
await atomicWrite(targetPath, json);
|
|
605
606
|
}
|
|
607
|
+
async function writeIssue(issue, root) {
|
|
608
|
+
const wrapDir = resolve2(root, ".story");
|
|
609
|
+
await withLock(wrapDir, async () => {
|
|
610
|
+
await writeIssueUnlocked(issue, root);
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
async function writeRoadmapUnlocked(roadmap, root) {
|
|
614
|
+
const parsed = RoadmapSchema.parse(roadmap);
|
|
615
|
+
const wrapDir = resolve2(root, ".story");
|
|
616
|
+
const targetPath = join3(wrapDir, "roadmap.json");
|
|
617
|
+
await guardPath(targetPath, wrapDir);
|
|
618
|
+
const json = serializeJSON(parsed);
|
|
619
|
+
await atomicWrite(targetPath, json);
|
|
620
|
+
}
|
|
621
|
+
async function writeRoadmap(roadmap, root) {
|
|
622
|
+
const wrapDir = resolve2(root, ".story");
|
|
623
|
+
await withLock(wrapDir, async () => {
|
|
624
|
+
await writeRoadmapUnlocked(roadmap, root);
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
async function writeConfig(config, root) {
|
|
628
|
+
const parsed = ConfigSchema.parse(config);
|
|
629
|
+
const wrapDir = resolve2(root, ".story");
|
|
630
|
+
const targetPath = join3(wrapDir, "config.json");
|
|
631
|
+
await guardPath(targetPath, wrapDir);
|
|
632
|
+
const json = serializeJSON(parsed);
|
|
633
|
+
await withLock(wrapDir, async () => {
|
|
634
|
+
await atomicWrite(targetPath, json);
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
async function deleteTicket(id, root, options) {
|
|
638
|
+
if (!TICKET_ID_REGEX.test(id)) {
|
|
639
|
+
throw new ProjectLoaderError(
|
|
640
|
+
"invalid_input",
|
|
641
|
+
`Invalid ticket ID: ${id}`
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
const wrapDir = resolve2(root, ".story");
|
|
645
|
+
const targetPath = join3(wrapDir, "tickets", `${id}.json`);
|
|
646
|
+
await guardPath(targetPath, wrapDir);
|
|
647
|
+
await withLock(wrapDir, async () => {
|
|
648
|
+
if (!options?.force) {
|
|
649
|
+
const { state } = await loadProjectUnlocked(resolve2(root));
|
|
650
|
+
const blocking = state.ticketsBlocking(id);
|
|
651
|
+
if (blocking.length > 0) {
|
|
652
|
+
throw new ProjectLoaderError(
|
|
653
|
+
"conflict",
|
|
654
|
+
`Cannot delete ${id}: referenced in blockedBy by ${blocking.join(", ")}`
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
const children = state.childrenOf(id);
|
|
658
|
+
if (children.length > 0) {
|
|
659
|
+
throw new ProjectLoaderError(
|
|
660
|
+
"conflict",
|
|
661
|
+
`Cannot delete ${id}: has child tickets ${children.join(", ")}`
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
const refs = state.issuesReferencing(id);
|
|
665
|
+
if (refs.length > 0) {
|
|
666
|
+
throw new ProjectLoaderError(
|
|
667
|
+
"conflict",
|
|
668
|
+
`Cannot delete ${id}: referenced by issues ${refs.join(", ")}`
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
try {
|
|
673
|
+
await stat(targetPath);
|
|
674
|
+
} catch {
|
|
675
|
+
throw new ProjectLoaderError(
|
|
676
|
+
"not_found",
|
|
677
|
+
`Ticket file not found: tickets/${id}.json`
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
await unlink(targetPath);
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
async function deleteIssue(id, root) {
|
|
684
|
+
if (!ISSUE_ID_REGEX.test(id)) {
|
|
685
|
+
throw new ProjectLoaderError(
|
|
686
|
+
"invalid_input",
|
|
687
|
+
`Invalid issue ID: ${id}`
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
const wrapDir = resolve2(root, ".story");
|
|
691
|
+
const targetPath = join3(wrapDir, "issues", `${id}.json`);
|
|
692
|
+
await guardPath(targetPath, wrapDir);
|
|
693
|
+
await withLock(wrapDir, async () => {
|
|
694
|
+
try {
|
|
695
|
+
await stat(targetPath);
|
|
696
|
+
} catch {
|
|
697
|
+
throw new ProjectLoaderError(
|
|
698
|
+
"not_found",
|
|
699
|
+
`Issue file not found: issues/${id}.json`
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
await unlink(targetPath);
|
|
703
|
+
});
|
|
704
|
+
}
|
|
606
705
|
async function writeNoteUnlocked(note, root) {
|
|
607
706
|
const parsed = NoteSchema.parse(note);
|
|
608
707
|
if (!NOTE_ID_REGEX.test(parsed.id)) {
|
|
@@ -618,6 +717,34 @@ async function writeNoteUnlocked(note, root) {
|
|
|
618
717
|
const json = serializeJSON(parsed);
|
|
619
718
|
await atomicWrite(targetPath, json);
|
|
620
719
|
}
|
|
720
|
+
async function writeNote(note, root) {
|
|
721
|
+
const wrapDir = resolve2(root, ".story");
|
|
722
|
+
await withLock(wrapDir, async () => {
|
|
723
|
+
await writeNoteUnlocked(note, root);
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
async function deleteNote(id, root) {
|
|
727
|
+
if (!NOTE_ID_REGEX.test(id)) {
|
|
728
|
+
throw new ProjectLoaderError(
|
|
729
|
+
"invalid_input",
|
|
730
|
+
`Invalid note ID: ${id}`
|
|
731
|
+
);
|
|
732
|
+
}
|
|
733
|
+
const wrapDir = resolve2(root, ".story");
|
|
734
|
+
const targetPath = join3(wrapDir, "notes", `${id}.json`);
|
|
735
|
+
await guardPath(targetPath, wrapDir);
|
|
736
|
+
await withLock(wrapDir, async () => {
|
|
737
|
+
try {
|
|
738
|
+
await stat(targetPath);
|
|
739
|
+
} catch {
|
|
740
|
+
throw new ProjectLoaderError(
|
|
741
|
+
"not_found",
|
|
742
|
+
`Note file not found: notes/${id}.json`
|
|
743
|
+
);
|
|
744
|
+
}
|
|
745
|
+
await unlink(targetPath);
|
|
746
|
+
});
|
|
747
|
+
}
|
|
621
748
|
async function withProjectLock(root, options, handler) {
|
|
622
749
|
const absRoot = resolve2(root);
|
|
623
750
|
const wrapDir = join3(absRoot, ".story");
|
|
@@ -1586,6 +1713,16 @@ function formatRecap(recap, state, format) {
|
|
|
1586
1713
|
lines.push(`- ${escapeMarkdownInline(name)} \u2014 **new**`);
|
|
1587
1714
|
}
|
|
1588
1715
|
}
|
|
1716
|
+
if (changes.handovers && (changes.handovers.added.length > 0 || changes.handovers.removed.length > 0)) {
|
|
1717
|
+
lines.push("");
|
|
1718
|
+
lines.push("## Handovers");
|
|
1719
|
+
for (const h of changes.handovers.added) {
|
|
1720
|
+
lines.push(`- ${h} \u2014 **new**`);
|
|
1721
|
+
}
|
|
1722
|
+
for (const h of changes.handovers.removed) {
|
|
1723
|
+
lines.push(`- ${h} \u2014 removed`);
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1589
1726
|
if (changes.notes && (changes.notes.added.length > 0 || changes.notes.removed.length > 0 || changes.notes.updated.length > 0)) {
|
|
1590
1727
|
lines.push("");
|
|
1591
1728
|
lines.push("## Notes");
|
|
@@ -1807,7 +1944,35 @@ function formatFullExport(state, format) {
|
|
|
1807
1944
|
return lines.join("\n");
|
|
1808
1945
|
}
|
|
1809
1946
|
function hasAnyChanges(diff) {
|
|
1810
|
-
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;
|
|
1947
|
+
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 || (diff.handovers?.added.length ?? 0) > 0 || (diff.handovers?.removed.length ?? 0) > 0;
|
|
1948
|
+
}
|
|
1949
|
+
function formatSelftestResult(result, format) {
|
|
1950
|
+
if (format === "json") {
|
|
1951
|
+
return JSON.stringify(successEnvelope(result), null, 2);
|
|
1952
|
+
}
|
|
1953
|
+
const lines = ["# Self-test Report", ""];
|
|
1954
|
+
const entities = ["ticket", "issue", "note"];
|
|
1955
|
+
for (const entity of entities) {
|
|
1956
|
+
const checks = result.results.filter((r) => r.entity === entity);
|
|
1957
|
+
if (checks.length === 0) continue;
|
|
1958
|
+
lines.push(`## ${entity.charAt(0).toUpperCase() + entity.slice(1)}`);
|
|
1959
|
+
for (const check of checks) {
|
|
1960
|
+
const mark = check.passed ? "[x]" : "[ ]";
|
|
1961
|
+
const suffix = check.passed ? "" : ` \u2014 ${check.detail}`;
|
|
1962
|
+
lines.push(`- ${mark} ${check.step}${suffix}`);
|
|
1963
|
+
}
|
|
1964
|
+
lines.push("");
|
|
1965
|
+
}
|
|
1966
|
+
if (result.cleanupErrors.length > 0) {
|
|
1967
|
+
lines.push("## Cleanup Warnings");
|
|
1968
|
+
lines.push("");
|
|
1969
|
+
for (const err of result.cleanupErrors) {
|
|
1970
|
+
lines.push(`- ${err}`);
|
|
1971
|
+
}
|
|
1972
|
+
lines.push("");
|
|
1973
|
+
}
|
|
1974
|
+
lines.push(`Result: ${result.passed}/${result.total} passed`);
|
|
1975
|
+
return lines.join("\n");
|
|
1811
1976
|
}
|
|
1812
1977
|
function truncate(text, maxLen) {
|
|
1813
1978
|
if (text.length <= maxLen) return text;
|
|
@@ -2446,6 +2611,12 @@ async function handleTicketUpdate(id, updates, format, root) {
|
|
|
2446
2611
|
`Unknown ticket status "${updates.status}": must be one of ${TICKET_STATUSES.join(", ")}`
|
|
2447
2612
|
);
|
|
2448
2613
|
}
|
|
2614
|
+
if (updates.type !== void 0 && !TICKET_TYPES.includes(updates.type)) {
|
|
2615
|
+
throw new CliValidationError(
|
|
2616
|
+
"invalid_input",
|
|
2617
|
+
`Unknown ticket type "${updates.type}": must be one of ${TICKET_TYPES.join(", ")}`
|
|
2618
|
+
);
|
|
2619
|
+
}
|
|
2449
2620
|
let updatedTicket;
|
|
2450
2621
|
await withProjectLock(root, { strict: true }, async ({ state }) => {
|
|
2451
2622
|
const existing = state.ticketByID(id);
|
|
@@ -2461,21 +2632,26 @@ async function handleTicketUpdate(id, updates, format, root) {
|
|
|
2461
2632
|
if (updates.parentTicket) {
|
|
2462
2633
|
validateParentTicket(updates.parentTicket, id, state);
|
|
2463
2634
|
}
|
|
2464
|
-
const
|
|
2465
|
-
if (updates.title !== void 0) ticket.title = updates.title;
|
|
2466
|
-
if (updates.description !== void 0) ticket.description = updates.description;
|
|
2467
|
-
if (updates.phase !== void 0) ticket.phase = updates.phase;
|
|
2468
|
-
if (updates.order !== void 0) ticket.order = updates.order;
|
|
2469
|
-
if (updates.blockedBy !== void 0) ticket.blockedBy = updates.blockedBy;
|
|
2470
|
-
if (updates.parentTicket !== void 0) ticket.parentTicket = updates.parentTicket === null ? void 0 : updates.parentTicket;
|
|
2635
|
+
const statusChanges = {};
|
|
2471
2636
|
if (updates.status !== void 0 && updates.status !== existing.status) {
|
|
2472
|
-
|
|
2637
|
+
statusChanges.status = updates.status;
|
|
2473
2638
|
if (updates.status === "complete" && existing.status !== "complete") {
|
|
2474
|
-
|
|
2639
|
+
statusChanges.completedDate = todayISO();
|
|
2475
2640
|
} else if (updates.status !== "complete" && existing.status === "complete") {
|
|
2476
|
-
|
|
2641
|
+
statusChanges.completedDate = null;
|
|
2477
2642
|
}
|
|
2478
2643
|
}
|
|
2644
|
+
const ticket = {
|
|
2645
|
+
...existing,
|
|
2646
|
+
...updates.title !== void 0 && { title: updates.title },
|
|
2647
|
+
...updates.type !== void 0 && { type: updates.type },
|
|
2648
|
+
...updates.description !== void 0 && { description: updates.description },
|
|
2649
|
+
...updates.phase !== void 0 && { phase: updates.phase },
|
|
2650
|
+
...updates.order !== void 0 && { order: updates.order },
|
|
2651
|
+
...updates.blockedBy !== void 0 && { blockedBy: updates.blockedBy },
|
|
2652
|
+
...updates.parentTicket !== void 0 && { parentTicket: updates.parentTicket },
|
|
2653
|
+
...statusChanges
|
|
2654
|
+
};
|
|
2479
2655
|
validatePostWriteState(ticket, state, false);
|
|
2480
2656
|
await writeTicketUnlocked(ticket, root);
|
|
2481
2657
|
updatedTicket = ticket;
|
|
@@ -2564,6 +2740,9 @@ async function handleIssueCreate(args, format, root) {
|
|
|
2564
2740
|
}
|
|
2565
2741
|
let createdIssue;
|
|
2566
2742
|
await withProjectLock(root, { strict: true }, async ({ state }) => {
|
|
2743
|
+
if (args.phase && !state.roadmap.phases.some((p) => p.id === args.phase)) {
|
|
2744
|
+
throw new CliValidationError("invalid_input", `Phase "${args.phase}" not found in roadmap`);
|
|
2745
|
+
}
|
|
2567
2746
|
if (args.relatedTickets.length > 0) {
|
|
2568
2747
|
validateRelatedTickets(args.relatedTickets, state);
|
|
2569
2748
|
}
|
|
@@ -2611,25 +2790,36 @@ async function handleIssueUpdate(id, updates, format, root) {
|
|
|
2611
2790
|
if (!existing) {
|
|
2612
2791
|
throw new CliValidationError("not_found", `Issue ${id} not found`);
|
|
2613
2792
|
}
|
|
2793
|
+
if (updates.phase !== void 0 && updates.phase !== null) {
|
|
2794
|
+
if (!state.roadmap.phases.some((p) => p.id === updates.phase)) {
|
|
2795
|
+
throw new CliValidationError("invalid_input", `Phase "${updates.phase}" not found in roadmap`);
|
|
2796
|
+
}
|
|
2797
|
+
}
|
|
2614
2798
|
if (updates.relatedTickets) {
|
|
2615
2799
|
validateRelatedTickets(updates.relatedTickets, state);
|
|
2616
2800
|
}
|
|
2617
|
-
const
|
|
2618
|
-
if (updates.title !== void 0) issue.title = updates.title;
|
|
2619
|
-
if (updates.severity !== void 0) issue.severity = updates.severity;
|
|
2620
|
-
if (updates.impact !== void 0) issue.impact = updates.impact;
|
|
2621
|
-
if (updates.resolution !== void 0) issue.resolution = updates.resolution;
|
|
2622
|
-
if (updates.components !== void 0) issue.components = updates.components;
|
|
2623
|
-
if (updates.relatedTickets !== void 0) issue.relatedTickets = updates.relatedTickets;
|
|
2624
|
-
if (updates.location !== void 0) issue.location = updates.location;
|
|
2801
|
+
const statusChanges = {};
|
|
2625
2802
|
if (updates.status !== void 0 && updates.status !== existing.status) {
|
|
2626
|
-
|
|
2803
|
+
statusChanges.status = updates.status;
|
|
2627
2804
|
if (updates.status === "resolved" && existing.status !== "resolved") {
|
|
2628
|
-
|
|
2805
|
+
statusChanges.resolvedDate = todayISO();
|
|
2629
2806
|
} else if (updates.status !== "resolved" && existing.status === "resolved") {
|
|
2630
|
-
|
|
2807
|
+
statusChanges.resolvedDate = null;
|
|
2631
2808
|
}
|
|
2632
2809
|
}
|
|
2810
|
+
const issue = {
|
|
2811
|
+
...existing,
|
|
2812
|
+
...updates.title !== void 0 && { title: updates.title },
|
|
2813
|
+
...updates.severity !== void 0 && { severity: updates.severity },
|
|
2814
|
+
...updates.impact !== void 0 && { impact: updates.impact },
|
|
2815
|
+
...updates.resolution !== void 0 && { resolution: updates.resolution },
|
|
2816
|
+
...updates.components !== void 0 && { components: updates.components },
|
|
2817
|
+
...updates.relatedTickets !== void 0 && { relatedTickets: updates.relatedTickets },
|
|
2818
|
+
...updates.location !== void 0 && { location: updates.location },
|
|
2819
|
+
...updates.order !== void 0 && { order: updates.order },
|
|
2820
|
+
...updates.phase !== void 0 && { phase: updates.phase },
|
|
2821
|
+
...statusChanges
|
|
2822
|
+
};
|
|
2633
2823
|
validatePostWriteIssueState(issue, state, false);
|
|
2634
2824
|
await writeIssueUnlocked(issue, root);
|
|
2635
2825
|
updatedIssue = issue;
|
|
@@ -2660,6 +2850,7 @@ var SnapshotV1Schema = z7.object({
|
|
|
2660
2850
|
tickets: z7.array(TicketSchema),
|
|
2661
2851
|
issues: z7.array(IssueSchema),
|
|
2662
2852
|
notes: z7.array(NoteSchema).optional().default([]),
|
|
2853
|
+
handoverFilenames: z7.array(z7.string()).optional().default([]),
|
|
2663
2854
|
warnings: z7.array(LoadWarningSchema).optional()
|
|
2664
2855
|
});
|
|
2665
2856
|
var MAX_SNAPSHOTS = 20;
|
|
@@ -2679,6 +2870,7 @@ async function saveSnapshot(root, loadResult) {
|
|
|
2679
2870
|
tickets: [...state.tickets],
|
|
2680
2871
|
issues: [...state.issues],
|
|
2681
2872
|
notes: [...state.notes],
|
|
2873
|
+
handoverFilenames: [...state.handoverFilenames],
|
|
2682
2874
|
...warnings.length > 0 ? {
|
|
2683
2875
|
warnings: warnings.map((w) => ({
|
|
2684
2876
|
type: w.type,
|
|
@@ -2831,12 +3023,23 @@ function diffStates(snapshotState, currentState) {
|
|
|
2831
3023
|
notesRemoved.push({ id, title: snap.title });
|
|
2832
3024
|
}
|
|
2833
3025
|
}
|
|
3026
|
+
const snapHandovers = new Set(snapshotState.handoverFilenames);
|
|
3027
|
+
const curHandovers = new Set(currentState.handoverFilenames);
|
|
3028
|
+
const handoversAdded = [];
|
|
3029
|
+
const handoversRemoved = [];
|
|
3030
|
+
for (const h of curHandovers) {
|
|
3031
|
+
if (!snapHandovers.has(h)) handoversAdded.push(h);
|
|
3032
|
+
}
|
|
3033
|
+
for (const h of snapHandovers) {
|
|
3034
|
+
if (!curHandovers.has(h)) handoversRemoved.push(h);
|
|
3035
|
+
}
|
|
2834
3036
|
return {
|
|
2835
3037
|
tickets: { added: ticketsAdded, removed: ticketsRemoved, statusChanged: ticketsStatusChanged, descriptionChanged: ticketsDescriptionChanged },
|
|
2836
3038
|
issues: { added: issuesAdded, resolved: issuesResolved, statusChanged: issuesStatusChanged, impactChanged: issuesImpactChanged },
|
|
2837
3039
|
blockers: { added: blockersAdded, cleared: blockersCleared },
|
|
2838
3040
|
phases: { added: phasesAdded, removed: phasesRemoved, statusChanged: phasesStatusChanged },
|
|
2839
|
-
notes: { added: notesAdded, removed: notesRemoved, updated: notesUpdated }
|
|
3041
|
+
notes: { added: notesAdded, removed: notesRemoved, updated: notesUpdated },
|
|
3042
|
+
handovers: { added: handoversAdded, removed: handoversRemoved }
|
|
2840
3043
|
};
|
|
2841
3044
|
}
|
|
2842
3045
|
function buildRecap(currentState, snapshotInfo) {
|
|
@@ -2864,7 +3067,7 @@ function buildRecap(currentState, snapshotInfo) {
|
|
|
2864
3067
|
notes: snapshot.notes ?? [],
|
|
2865
3068
|
roadmap: snapshot.roadmap,
|
|
2866
3069
|
config: snapshot.config,
|
|
2867
|
-
handoverFilenames: []
|
|
3070
|
+
handoverFilenames: snapshot.handoverFilenames ?? []
|
|
2868
3071
|
});
|
|
2869
3072
|
const changes = diffStates(snapshotState, currentState);
|
|
2870
3073
|
const recentlyClearedBlockers = changes.blockers.cleared;
|
|
@@ -2996,23 +3199,22 @@ async function handleNoteUpdate(id, updates, format, root) {
|
|
|
2996
3199
|
if (!existing) {
|
|
2997
3200
|
throw new CliValidationError("not_found", `Note ${id} not found`);
|
|
2998
3201
|
}
|
|
2999
|
-
const
|
|
3000
|
-
if (updates.content !== void 0) {
|
|
3001
|
-
note.content = updates.content;
|
|
3002
|
-
}
|
|
3003
|
-
if (updates.title !== void 0) {
|
|
3004
|
-
const trimmed = updates.title?.trim();
|
|
3005
|
-
note.title = !trimmed ? null : updates.title;
|
|
3006
|
-
}
|
|
3202
|
+
const tagsUpdate = {};
|
|
3007
3203
|
if (updates.clearTags) {
|
|
3008
|
-
|
|
3204
|
+
tagsUpdate.tags = [];
|
|
3009
3205
|
} else if (updates.tags !== void 0) {
|
|
3010
|
-
|
|
3011
|
-
}
|
|
3012
|
-
if (updates.status !== void 0) {
|
|
3013
|
-
note.status = updates.status;
|
|
3206
|
+
tagsUpdate.tags = normalizeTags(updates.tags);
|
|
3014
3207
|
}
|
|
3015
|
-
note
|
|
3208
|
+
const note = {
|
|
3209
|
+
...existing,
|
|
3210
|
+
...updates.content !== void 0 && { content: updates.content },
|
|
3211
|
+
...updates.title !== void 0 && {
|
|
3212
|
+
title: !updates.title?.trim() ? null : updates.title
|
|
3213
|
+
},
|
|
3214
|
+
...tagsUpdate,
|
|
3215
|
+
...updates.status !== void 0 && { status: updates.status },
|
|
3216
|
+
updatedDate: todayISO()
|
|
3217
|
+
};
|
|
3016
3218
|
await writeNoteUnlocked(note, root);
|
|
3017
3219
|
updatedNote = note;
|
|
3018
3220
|
});
|
|
@@ -3286,8 +3488,258 @@ function handleExport(ctx, mode, phaseId) {
|
|
|
3286
3488
|
return { output: formatExport(ctx.state, mode, phaseId, ctx.format) };
|
|
3287
3489
|
}
|
|
3288
3490
|
|
|
3491
|
+
// src/cli/commands/selftest.ts
|
|
3492
|
+
async function handleSelftest(root, format, failAfter) {
|
|
3493
|
+
const results = [];
|
|
3494
|
+
const createdIds = [];
|
|
3495
|
+
const cleanupErrors = [];
|
|
3496
|
+
function record(entity, step, passed2, detail) {
|
|
3497
|
+
results.push({ entity, step, passed: passed2, detail });
|
|
3498
|
+
if (failAfter !== void 0 && results.filter((r) => r.passed).length >= failAfter) {
|
|
3499
|
+
throw new Error(`failAfter(${failAfter}): induced failure for testing`);
|
|
3500
|
+
}
|
|
3501
|
+
}
|
|
3502
|
+
try {
|
|
3503
|
+
let ticketId;
|
|
3504
|
+
try {
|
|
3505
|
+
const { state } = await loadProject(root);
|
|
3506
|
+
ticketId = nextTicketID(state.tickets);
|
|
3507
|
+
const today = todayISO();
|
|
3508
|
+
const ticket = {
|
|
3509
|
+
id: ticketId,
|
|
3510
|
+
title: "selftest ticket",
|
|
3511
|
+
type: "chore",
|
|
3512
|
+
status: "open",
|
|
3513
|
+
phase: null,
|
|
3514
|
+
order: 0,
|
|
3515
|
+
description: "Integration smoke test \u2014 will be deleted.",
|
|
3516
|
+
createdDate: today,
|
|
3517
|
+
completedDate: null,
|
|
3518
|
+
blockedBy: [],
|
|
3519
|
+
parentTicket: null
|
|
3520
|
+
};
|
|
3521
|
+
await writeTicket(ticket, root);
|
|
3522
|
+
createdIds.push({ type: "ticket", id: ticketId });
|
|
3523
|
+
record("ticket", "create", true, `Created ${ticketId}`);
|
|
3524
|
+
} catch (err) {
|
|
3525
|
+
record("ticket", "create", false, errMsg(err));
|
|
3526
|
+
}
|
|
3527
|
+
if (ticketId) {
|
|
3528
|
+
try {
|
|
3529
|
+
const { state } = await loadProject(root);
|
|
3530
|
+
const found = state.ticketByID(ticketId);
|
|
3531
|
+
if (!found) throw new Error(`${ticketId} not found after create`);
|
|
3532
|
+
record("ticket", "get", true, `Found ${ticketId}`);
|
|
3533
|
+
} catch (err) {
|
|
3534
|
+
record("ticket", "get", false, errMsg(err));
|
|
3535
|
+
}
|
|
3536
|
+
try {
|
|
3537
|
+
const { state } = await loadProject(root);
|
|
3538
|
+
const existing = state.ticketByID(ticketId);
|
|
3539
|
+
if (!existing) throw new Error(`${ticketId} not found for update`);
|
|
3540
|
+
const updated = { ...existing, status: "inprogress" };
|
|
3541
|
+
await writeTicket(updated, root);
|
|
3542
|
+
record("ticket", "update", true, `Updated ${ticketId} status \u2192 inprogress`);
|
|
3543
|
+
} catch (err) {
|
|
3544
|
+
record("ticket", "update", false, errMsg(err));
|
|
3545
|
+
}
|
|
3546
|
+
try {
|
|
3547
|
+
const { state } = await loadProject(root);
|
|
3548
|
+
const found = state.ticketByID(ticketId);
|
|
3549
|
+
if (!found) throw new Error(`${ticketId} not found for verify`);
|
|
3550
|
+
if (found.status !== "inprogress") throw new Error(`Expected inprogress, got ${found.status}`);
|
|
3551
|
+
record("ticket", "verify update", true, `Verified ${ticketId} status = inprogress`);
|
|
3552
|
+
} catch (err) {
|
|
3553
|
+
record("ticket", "verify update", false, errMsg(err));
|
|
3554
|
+
}
|
|
3555
|
+
try {
|
|
3556
|
+
await deleteTicket(ticketId, root, { force: true });
|
|
3557
|
+
createdIds.splice(createdIds.findIndex((c) => c.id === ticketId), 1);
|
|
3558
|
+
record("ticket", "delete", true, `Deleted ${ticketId}`);
|
|
3559
|
+
} catch (err) {
|
|
3560
|
+
record("ticket", "delete", false, errMsg(err));
|
|
3561
|
+
}
|
|
3562
|
+
try {
|
|
3563
|
+
const { state } = await loadProject(root);
|
|
3564
|
+
const found = state.ticketByID(ticketId);
|
|
3565
|
+
if (found) throw new Error(`${ticketId} still exists after delete`);
|
|
3566
|
+
record("ticket", "verify delete", true, `Confirmed ${ticketId} absent`);
|
|
3567
|
+
} catch (err) {
|
|
3568
|
+
record("ticket", "verify delete", false, errMsg(err));
|
|
3569
|
+
}
|
|
3570
|
+
}
|
|
3571
|
+
let issueId;
|
|
3572
|
+
try {
|
|
3573
|
+
const { state } = await loadProject(root);
|
|
3574
|
+
issueId = nextIssueID(state.issues);
|
|
3575
|
+
const today = todayISO();
|
|
3576
|
+
const issue = {
|
|
3577
|
+
id: issueId,
|
|
3578
|
+
title: "selftest issue",
|
|
3579
|
+
status: "open",
|
|
3580
|
+
severity: "low",
|
|
3581
|
+
components: [],
|
|
3582
|
+
impact: "Integration smoke test \u2014 will be deleted.",
|
|
3583
|
+
resolution: null,
|
|
3584
|
+
location: [],
|
|
3585
|
+
discoveredDate: today,
|
|
3586
|
+
resolvedDate: null,
|
|
3587
|
+
relatedTickets: [],
|
|
3588
|
+
order: 0,
|
|
3589
|
+
phase: null
|
|
3590
|
+
};
|
|
3591
|
+
await writeIssue(issue, root);
|
|
3592
|
+
createdIds.push({ type: "issue", id: issueId });
|
|
3593
|
+
record("issue", "create", true, `Created ${issueId}`);
|
|
3594
|
+
} catch (err) {
|
|
3595
|
+
record("issue", "create", false, errMsg(err));
|
|
3596
|
+
}
|
|
3597
|
+
if (issueId) {
|
|
3598
|
+
try {
|
|
3599
|
+
const { state } = await loadProject(root);
|
|
3600
|
+
const found = state.issueByID(issueId);
|
|
3601
|
+
if (!found) throw new Error(`${issueId} not found after create`);
|
|
3602
|
+
record("issue", "get", true, `Found ${issueId}`);
|
|
3603
|
+
} catch (err) {
|
|
3604
|
+
record("issue", "get", false, errMsg(err));
|
|
3605
|
+
}
|
|
3606
|
+
try {
|
|
3607
|
+
const { state } = await loadProject(root);
|
|
3608
|
+
const existing = state.issueByID(issueId);
|
|
3609
|
+
if (!existing) throw new Error(`${issueId} not found for update`);
|
|
3610
|
+
const updated = { ...existing, status: "inprogress" };
|
|
3611
|
+
await writeIssue(updated, root);
|
|
3612
|
+
record("issue", "update", true, `Updated ${issueId} status \u2192 inprogress`);
|
|
3613
|
+
} catch (err) {
|
|
3614
|
+
record("issue", "update", false, errMsg(err));
|
|
3615
|
+
}
|
|
3616
|
+
try {
|
|
3617
|
+
const { state } = await loadProject(root);
|
|
3618
|
+
const found = state.issueByID(issueId);
|
|
3619
|
+
if (!found) throw new Error(`${issueId} not found for verify`);
|
|
3620
|
+
if (found.status !== "inprogress") throw new Error(`Expected inprogress, got ${found.status}`);
|
|
3621
|
+
record("issue", "verify update", true, `Verified ${issueId} status = inprogress`);
|
|
3622
|
+
} catch (err) {
|
|
3623
|
+
record("issue", "verify update", false, errMsg(err));
|
|
3624
|
+
}
|
|
3625
|
+
try {
|
|
3626
|
+
await deleteIssue(issueId, root);
|
|
3627
|
+
createdIds.splice(createdIds.findIndex((c) => c.id === issueId), 1);
|
|
3628
|
+
record("issue", "delete", true, `Deleted ${issueId}`);
|
|
3629
|
+
} catch (err) {
|
|
3630
|
+
record("issue", "delete", false, errMsg(err));
|
|
3631
|
+
}
|
|
3632
|
+
try {
|
|
3633
|
+
const { state } = await loadProject(root);
|
|
3634
|
+
const found = state.issueByID(issueId);
|
|
3635
|
+
if (found) throw new Error(`${issueId} still exists after delete`);
|
|
3636
|
+
record("issue", "verify delete", true, `Confirmed ${issueId} absent`);
|
|
3637
|
+
} catch (err) {
|
|
3638
|
+
record("issue", "verify delete", false, errMsg(err));
|
|
3639
|
+
}
|
|
3640
|
+
}
|
|
3641
|
+
let noteId;
|
|
3642
|
+
try {
|
|
3643
|
+
const { state } = await loadProject(root);
|
|
3644
|
+
noteId = nextNoteID(state.notes);
|
|
3645
|
+
const today = todayISO();
|
|
3646
|
+
const note = {
|
|
3647
|
+
id: noteId,
|
|
3648
|
+
title: "selftest note",
|
|
3649
|
+
content: "Integration smoke test \u2014 will be deleted.",
|
|
3650
|
+
tags: [],
|
|
3651
|
+
status: "active",
|
|
3652
|
+
createdDate: today,
|
|
3653
|
+
updatedDate: today
|
|
3654
|
+
};
|
|
3655
|
+
await writeNote(note, root);
|
|
3656
|
+
createdIds.push({ type: "note", id: noteId });
|
|
3657
|
+
record("note", "create", true, `Created ${noteId}`);
|
|
3658
|
+
} catch (err) {
|
|
3659
|
+
record("note", "create", false, errMsg(err));
|
|
3660
|
+
}
|
|
3661
|
+
if (noteId) {
|
|
3662
|
+
try {
|
|
3663
|
+
const { state } = await loadProject(root);
|
|
3664
|
+
const found = state.noteByID(noteId);
|
|
3665
|
+
if (!found) throw new Error(`${noteId} not found after create`);
|
|
3666
|
+
record("note", "get", true, `Found ${noteId}`);
|
|
3667
|
+
} catch (err) {
|
|
3668
|
+
record("note", "get", false, errMsg(err));
|
|
3669
|
+
}
|
|
3670
|
+
try {
|
|
3671
|
+
const { state } = await loadProject(root);
|
|
3672
|
+
const existing = state.noteByID(noteId);
|
|
3673
|
+
if (!existing) throw new Error(`${noteId} not found for update`);
|
|
3674
|
+
const updated = { ...existing, status: "archived", updatedDate: todayISO() };
|
|
3675
|
+
await writeNote(updated, root);
|
|
3676
|
+
record("note", "update", true, `Updated ${noteId} status \u2192 archived`);
|
|
3677
|
+
} catch (err) {
|
|
3678
|
+
record("note", "update", false, errMsg(err));
|
|
3679
|
+
}
|
|
3680
|
+
try {
|
|
3681
|
+
const { state } = await loadProject(root);
|
|
3682
|
+
const found = state.noteByID(noteId);
|
|
3683
|
+
if (!found) throw new Error(`${noteId} not found for verify`);
|
|
3684
|
+
if (found.status !== "archived") throw new Error(`Expected archived, got ${found.status}`);
|
|
3685
|
+
record("note", "verify update", true, `Verified ${noteId} status = archived`);
|
|
3686
|
+
} catch (err) {
|
|
3687
|
+
record("note", "verify update", false, errMsg(err));
|
|
3688
|
+
}
|
|
3689
|
+
try {
|
|
3690
|
+
await deleteNote(noteId, root);
|
|
3691
|
+
createdIds.splice(createdIds.findIndex((c) => c.id === noteId), 1);
|
|
3692
|
+
record("note", "delete", true, `Deleted ${noteId}`);
|
|
3693
|
+
} catch (err) {
|
|
3694
|
+
record("note", "delete", false, errMsg(err));
|
|
3695
|
+
}
|
|
3696
|
+
try {
|
|
3697
|
+
const { state } = await loadProject(root);
|
|
3698
|
+
const found = state.noteByID(noteId);
|
|
3699
|
+
if (found) throw new Error(`${noteId} still exists after delete`);
|
|
3700
|
+
record("note", "verify delete", true, `Confirmed ${noteId} absent`);
|
|
3701
|
+
} catch (err) {
|
|
3702
|
+
record("note", "verify delete", false, errMsg(err));
|
|
3703
|
+
}
|
|
3704
|
+
}
|
|
3705
|
+
} finally {
|
|
3706
|
+
for (const { type, id } of createdIds.reverse()) {
|
|
3707
|
+
try {
|
|
3708
|
+
if (type === "ticket") await deleteTicket(id, root, { force: true });
|
|
3709
|
+
else if (type === "issue") await deleteIssue(id, root);
|
|
3710
|
+
else await deleteNote(id, root);
|
|
3711
|
+
} catch (err) {
|
|
3712
|
+
cleanupErrors.push(`Failed to delete ${type} ${id}: ${errMsg(err)}`);
|
|
3713
|
+
}
|
|
3714
|
+
}
|
|
3715
|
+
}
|
|
3716
|
+
const passed = results.filter((r) => r.passed).length;
|
|
3717
|
+
const failed = results.filter((r) => !r.passed).length;
|
|
3718
|
+
const result = {
|
|
3719
|
+
passed,
|
|
3720
|
+
failed,
|
|
3721
|
+
total: results.length,
|
|
3722
|
+
results,
|
|
3723
|
+
cleanupErrors
|
|
3724
|
+
};
|
|
3725
|
+
return { output: formatSelftestResult(result, format) };
|
|
3726
|
+
}
|
|
3727
|
+
function errMsg(err) {
|
|
3728
|
+
return err instanceof Error ? err.message : String(err);
|
|
3729
|
+
}
|
|
3730
|
+
|
|
3289
3731
|
// src/cli/commands/phase.ts
|
|
3290
3732
|
import { join as join6, resolve as resolve6 } from "path";
|
|
3733
|
+
var PHASE_ID_REGEX = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
3734
|
+
var PHASE_ID_MAX_LENGTH = 40;
|
|
3735
|
+
function validatePhaseId(id) {
|
|
3736
|
+
if (id.length > PHASE_ID_MAX_LENGTH) {
|
|
3737
|
+
throw new CliValidationError("invalid_input", `Phase ID "${id}" exceeds ${PHASE_ID_MAX_LENGTH} characters`);
|
|
3738
|
+
}
|
|
3739
|
+
if (!PHASE_ID_REGEX.test(id)) {
|
|
3740
|
+
throw new CliValidationError("invalid_input", `Phase ID "${id}" must be lowercase alphanumeric with hyphens (e.g. "my-phase")`);
|
|
3741
|
+
}
|
|
3742
|
+
}
|
|
3291
3743
|
function handlePhaseList(ctx) {
|
|
3292
3744
|
return { output: formatPhaseList(ctx.state, ctx.format) };
|
|
3293
3745
|
}
|
|
@@ -3333,6 +3785,46 @@ function handlePhaseTickets(phaseId, ctx) {
|
|
|
3333
3785
|
}
|
|
3334
3786
|
return { output: formatPhaseTickets(phaseId, ctx.state, ctx.format) };
|
|
3335
3787
|
}
|
|
3788
|
+
async function handlePhaseCreate(args, format, root) {
|
|
3789
|
+
validatePhaseId(args.id);
|
|
3790
|
+
if (args.atStart && args.after) {
|
|
3791
|
+
throw new CliValidationError("invalid_input", "Cannot use both --after and --at-start");
|
|
3792
|
+
}
|
|
3793
|
+
if (!args.atStart && !args.after) {
|
|
3794
|
+
throw new CliValidationError("invalid_input", "Must specify either --after <phase-id> or --at-start");
|
|
3795
|
+
}
|
|
3796
|
+
let createdPhase;
|
|
3797
|
+
await withProjectLock(root, { strict: true }, async ({ state }) => {
|
|
3798
|
+
if (state.roadmap.phases.some((p) => p.id === args.id)) {
|
|
3799
|
+
throw new CliValidationError("conflict", `Phase "${args.id}" already exists`);
|
|
3800
|
+
}
|
|
3801
|
+
const phase = {
|
|
3802
|
+
id: args.id,
|
|
3803
|
+
label: args.label,
|
|
3804
|
+
name: args.name,
|
|
3805
|
+
description: args.description,
|
|
3806
|
+
...args.summary !== void 0 && { summary: args.summary }
|
|
3807
|
+
};
|
|
3808
|
+
const newPhases = [...state.roadmap.phases];
|
|
3809
|
+
if (args.atStart) {
|
|
3810
|
+
newPhases.unshift(phase);
|
|
3811
|
+
} else {
|
|
3812
|
+
const afterIdx = newPhases.findIndex((p) => p.id === args.after);
|
|
3813
|
+
if (afterIdx < 0) {
|
|
3814
|
+
throw new CliValidationError("not_found", `Phase "${args.after}" not found`);
|
|
3815
|
+
}
|
|
3816
|
+
newPhases.splice(afterIdx + 1, 0, phase);
|
|
3817
|
+
}
|
|
3818
|
+
const newRoadmap = { ...state.roadmap, phases: newPhases };
|
|
3819
|
+
await writeRoadmapUnlocked(newRoadmap, root);
|
|
3820
|
+
createdPhase = phase;
|
|
3821
|
+
});
|
|
3822
|
+
if (!createdPhase) throw new Error("Phase not created");
|
|
3823
|
+
if (format === "json") {
|
|
3824
|
+
return { output: JSON.stringify(successEnvelope(createdPhase), null, 2) };
|
|
3825
|
+
}
|
|
3826
|
+
return { output: `Created phase ${createdPhase.id}: ${createdPhase.name}` };
|
|
3827
|
+
}
|
|
3336
3828
|
|
|
3337
3829
|
// src/mcp/tools.ts
|
|
3338
3830
|
var INFRASTRUCTURE_ERROR_CODES = [
|
|
@@ -3592,7 +4084,8 @@ function registerAllTools(server, pinnedRoot) {
|
|
|
3592
4084
|
id: z8.string().regex(TICKET_ID_REGEX).describe("Ticket ID (e.g. T-001)"),
|
|
3593
4085
|
status: z8.enum(TICKET_STATUSES).optional().describe("New status: open, inprogress, complete"),
|
|
3594
4086
|
title: z8.string().optional().describe("New title"),
|
|
3595
|
-
|
|
4087
|
+
type: z8.enum(TICKET_TYPES).optional().describe("New type: task, feature, chore"),
|
|
4088
|
+
order: z8.number().int().optional().describe("New sort order"),
|
|
3596
4089
|
description: z8.string().optional().describe("New description"),
|
|
3597
4090
|
phase: z8.string().nullable().optional().describe("New phase ID (null to clear)"),
|
|
3598
4091
|
parentTicket: z8.string().regex(TICKET_ID_REGEX).nullable().optional().describe("Parent ticket ID (null to clear)"),
|
|
@@ -3605,6 +4098,7 @@ function registerAllTools(server, pinnedRoot) {
|
|
|
3605
4098
|
{
|
|
3606
4099
|
status: args.status,
|
|
3607
4100
|
title: args.title,
|
|
4101
|
+
type: args.type,
|
|
3608
4102
|
order: args.order,
|
|
3609
4103
|
description: args.description,
|
|
3610
4104
|
phase: args.phase,
|
|
@@ -3653,7 +4147,9 @@ function registerAllTools(server, pinnedRoot) {
|
|
|
3653
4147
|
resolution: z8.string().nullable().optional().describe("Resolution description (null to clear)"),
|
|
3654
4148
|
components: z8.array(z8.string()).optional().describe("Affected components"),
|
|
3655
4149
|
relatedTickets: z8.array(z8.string().regex(TICKET_ID_REGEX)).optional().describe("Related ticket IDs"),
|
|
3656
|
-
location: z8.array(z8.string()).optional().describe("File locations")
|
|
4150
|
+
location: z8.array(z8.string()).optional().describe("File locations"),
|
|
4151
|
+
order: z8.number().int().optional().describe("New sort order"),
|
|
4152
|
+
phase: z8.string().nullable().optional().describe("New phase ID (null to clear)")
|
|
3657
4153
|
}
|
|
3658
4154
|
}, (args) => runMcpWriteTool(
|
|
3659
4155
|
pinnedRoot,
|
|
@@ -3667,7 +4163,9 @@ function registerAllTools(server, pinnedRoot) {
|
|
|
3667
4163
|
resolution: args.resolution,
|
|
3668
4164
|
components: args.components,
|
|
3669
4165
|
relatedTickets: args.relatedTickets,
|
|
3670
|
-
location: args.location
|
|
4166
|
+
location: args.location,
|
|
4167
|
+
order: args.order,
|
|
4168
|
+
phase: args.phase
|
|
3671
4169
|
},
|
|
3672
4170
|
format,
|
|
3673
4171
|
root
|
|
@@ -3732,6 +4230,125 @@ function registerAllTools(server, pinnedRoot) {
|
|
|
3732
4230
|
root
|
|
3733
4231
|
)
|
|
3734
4232
|
));
|
|
4233
|
+
server.registerTool("claudestory_phase_create", {
|
|
4234
|
+
description: "Create a new phase in the roadmap. Exactly one of after or atStart is required for positioning.",
|
|
4235
|
+
inputSchema: {
|
|
4236
|
+
id: z8.string().describe("Phase ID \u2014 lowercase alphanumeric with hyphens (e.g. 'my-phase')"),
|
|
4237
|
+
name: z8.string().describe("Phase display name"),
|
|
4238
|
+
label: z8.string().describe("Phase label (e.g. 'PHASE 1')"),
|
|
4239
|
+
description: z8.string().describe("Phase description"),
|
|
4240
|
+
summary: z8.string().optional().describe("One-line summary for compact display"),
|
|
4241
|
+
after: z8.string().optional().describe("Insert after this phase ID"),
|
|
4242
|
+
atStart: z8.boolean().optional().describe("Insert at beginning of roadmap")
|
|
4243
|
+
}
|
|
4244
|
+
}, (args) => runMcpWriteTool(
|
|
4245
|
+
pinnedRoot,
|
|
4246
|
+
(root, format) => handlePhaseCreate(
|
|
4247
|
+
{
|
|
4248
|
+
id: args.id,
|
|
4249
|
+
name: args.name,
|
|
4250
|
+
label: args.label,
|
|
4251
|
+
description: args.description,
|
|
4252
|
+
summary: args.summary,
|
|
4253
|
+
after: args.after,
|
|
4254
|
+
atStart: args.atStart ?? false
|
|
4255
|
+
},
|
|
4256
|
+
format,
|
|
4257
|
+
root
|
|
4258
|
+
)
|
|
4259
|
+
));
|
|
4260
|
+
server.registerTool("claudestory_selftest", {
|
|
4261
|
+
description: "Integration smoke test \u2014 creates, updates, and deletes test entities to verify the full pipeline"
|
|
4262
|
+
}, () => runMcpWriteTool(
|
|
4263
|
+
pinnedRoot,
|
|
4264
|
+
(root, format) => handleSelftest(root, format)
|
|
4265
|
+
));
|
|
4266
|
+
}
|
|
4267
|
+
|
|
4268
|
+
// src/core/init.ts
|
|
4269
|
+
import { mkdir as mkdir4, stat as stat2 } from "fs/promises";
|
|
4270
|
+
import { join as join8, resolve as resolve7 } from "path";
|
|
4271
|
+
async function initProject(root, options) {
|
|
4272
|
+
const absRoot = resolve7(root);
|
|
4273
|
+
const wrapDir = join8(absRoot, ".story");
|
|
4274
|
+
let exists = false;
|
|
4275
|
+
try {
|
|
4276
|
+
const s = await stat2(wrapDir);
|
|
4277
|
+
if (s.isDirectory()) exists = true;
|
|
4278
|
+
} catch (err) {
|
|
4279
|
+
if (err.code !== "ENOENT") {
|
|
4280
|
+
throw new ProjectLoaderError(
|
|
4281
|
+
"io_error",
|
|
4282
|
+
`Cannot check .story/ directory: ${err.message}`,
|
|
4283
|
+
err
|
|
4284
|
+
);
|
|
4285
|
+
}
|
|
4286
|
+
}
|
|
4287
|
+
if (exists && !options.force) {
|
|
4288
|
+
throw new ProjectLoaderError(
|
|
4289
|
+
"conflict",
|
|
4290
|
+
".story/ already exists. Use --force to overwrite config and roadmap."
|
|
4291
|
+
);
|
|
4292
|
+
}
|
|
4293
|
+
await mkdir4(join8(wrapDir, "tickets"), { recursive: true });
|
|
4294
|
+
await mkdir4(join8(wrapDir, "issues"), { recursive: true });
|
|
4295
|
+
await mkdir4(join8(wrapDir, "handovers"), { recursive: true });
|
|
4296
|
+
await mkdir4(join8(wrapDir, "notes"), { recursive: true });
|
|
4297
|
+
const created = [
|
|
4298
|
+
".story/config.json",
|
|
4299
|
+
".story/roadmap.json",
|
|
4300
|
+
".story/tickets/",
|
|
4301
|
+
".story/issues/",
|
|
4302
|
+
".story/handovers/",
|
|
4303
|
+
".story/notes/"
|
|
4304
|
+
];
|
|
4305
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
4306
|
+
const config = {
|
|
4307
|
+
version: 2,
|
|
4308
|
+
schemaVersion: CURRENT_SCHEMA_VERSION,
|
|
4309
|
+
project: options.name,
|
|
4310
|
+
type: options.type ?? "generic",
|
|
4311
|
+
language: options.language ?? "unknown",
|
|
4312
|
+
features: {
|
|
4313
|
+
tickets: true,
|
|
4314
|
+
issues: true,
|
|
4315
|
+
handovers: true,
|
|
4316
|
+
roadmap: true,
|
|
4317
|
+
reviews: true
|
|
4318
|
+
}
|
|
4319
|
+
};
|
|
4320
|
+
const roadmap = {
|
|
4321
|
+
title: options.name,
|
|
4322
|
+
date: today,
|
|
4323
|
+
phases: options.phases ?? [
|
|
4324
|
+
{
|
|
4325
|
+
id: "p0",
|
|
4326
|
+
label: "PHASE 0",
|
|
4327
|
+
name: "Setup",
|
|
4328
|
+
description: "Initial project setup."
|
|
4329
|
+
}
|
|
4330
|
+
],
|
|
4331
|
+
blockers: []
|
|
4332
|
+
};
|
|
4333
|
+
await writeConfig(config, absRoot);
|
|
4334
|
+
await writeRoadmap(roadmap, absRoot);
|
|
4335
|
+
const warnings = [];
|
|
4336
|
+
if (options.force && exists) {
|
|
4337
|
+
try {
|
|
4338
|
+
const { warnings: loadWarnings } = await loadProject(absRoot);
|
|
4339
|
+
for (const w of loadWarnings) {
|
|
4340
|
+
if (INTEGRITY_WARNING_TYPES.includes(w.type)) {
|
|
4341
|
+
warnings.push(`${w.file}: ${w.message}`);
|
|
4342
|
+
}
|
|
4343
|
+
}
|
|
4344
|
+
} catch {
|
|
4345
|
+
}
|
|
4346
|
+
}
|
|
4347
|
+
return {
|
|
4348
|
+
root: absRoot,
|
|
4349
|
+
created,
|
|
4350
|
+
warnings
|
|
4351
|
+
};
|
|
3735
4352
|
}
|
|
3736
4353
|
|
|
3737
4354
|
// src/mcp/index.ts
|
|
@@ -3746,10 +4363,10 @@ function tryDiscoverRoot() {
|
|
|
3746
4363
|
`);
|
|
3747
4364
|
return null;
|
|
3748
4365
|
}
|
|
3749
|
-
const resolved =
|
|
4366
|
+
const resolved = resolve8(envRoot);
|
|
3750
4367
|
try {
|
|
3751
4368
|
const canonical = realpathSync(resolved);
|
|
3752
|
-
if (existsSync6(
|
|
4369
|
+
if (existsSync6(join9(canonical, CONFIG_PATH2))) {
|
|
3753
4370
|
return canonical;
|
|
3754
4371
|
}
|
|
3755
4372
|
process.stderr.write(`Warning: No .story/config.json at ${canonical}
|
|
@@ -3767,12 +4384,68 @@ function tryDiscoverRoot() {
|
|
|
3767
4384
|
return null;
|
|
3768
4385
|
}
|
|
3769
4386
|
}
|
|
4387
|
+
function registerDegradedTools(server) {
|
|
4388
|
+
const degradedStatus = server.registerTool("claudestory_status", {
|
|
4389
|
+
description: "Project summary \u2014 returns guidance if no .story/ project found"
|
|
4390
|
+
}, () => Promise.resolve({
|
|
4391
|
+
content: [{ type: "text", text: "No .story/ project found. Use claudestory_init to create one, or navigate to a directory with .story/." }],
|
|
4392
|
+
isError: true
|
|
4393
|
+
}));
|
|
4394
|
+
const degradedInit = server.registerTool("claudestory_init", {
|
|
4395
|
+
description: "Initialize a new .story/ project in the current directory",
|
|
4396
|
+
inputSchema: {
|
|
4397
|
+
name: z9.string().describe("Project name"),
|
|
4398
|
+
type: z9.string().optional().describe("Project type (e.g. npm, macapp, cargo, generic)"),
|
|
4399
|
+
language: z9.string().optional().describe("Primary language (e.g. typescript, swift, rust)")
|
|
4400
|
+
}
|
|
4401
|
+
}, async (args) => {
|
|
4402
|
+
let result;
|
|
4403
|
+
try {
|
|
4404
|
+
const projectRoot = realpathSync(process.cwd());
|
|
4405
|
+
result = await initProject(projectRoot, {
|
|
4406
|
+
name: args.name,
|
|
4407
|
+
type: args.type,
|
|
4408
|
+
language: args.language,
|
|
4409
|
+
phases: []
|
|
4410
|
+
});
|
|
4411
|
+
} catch (err) {
|
|
4412
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
4413
|
+
return { content: [{ type: "text", text: `[init_error] ${msg}` }], isError: true };
|
|
4414
|
+
}
|
|
4415
|
+
try {
|
|
4416
|
+
degradedStatus.remove();
|
|
4417
|
+
degradedInit.remove();
|
|
4418
|
+
registerAllTools(server, result.root);
|
|
4419
|
+
} catch (swapErr) {
|
|
4420
|
+
process.stderr.write(`claudestory: tool-swap failed after init: ${swapErr instanceof Error ? swapErr.message : String(swapErr)}
|
|
4421
|
+
`);
|
|
4422
|
+
try {
|
|
4423
|
+
registerDegradedTools(server);
|
|
4424
|
+
} catch {
|
|
4425
|
+
}
|
|
4426
|
+
return { content: [{ type: "text", text: `Initialized .story/ project "${args.name}" at ${result.root}
|
|
4427
|
+
|
|
4428
|
+
Warning: tool registration failed. Restart the MCP server for full tool access.` }] };
|
|
4429
|
+
}
|
|
4430
|
+
process.stderr.write(`claudestory: initialized at ${result.root}
|
|
4431
|
+
`);
|
|
4432
|
+
const lines = [
|
|
4433
|
+
`Initialized .story/ project "${args.name}" at ${result.root}`,
|
|
4434
|
+
`Created: ${result.created.join(", ")}`
|
|
4435
|
+
];
|
|
4436
|
+
if (result.warnings.length > 0) {
|
|
4437
|
+
lines.push(`Warnings: ${result.warnings.join("; ")}`);
|
|
4438
|
+
}
|
|
4439
|
+
lines.push("", "All claudestory tools are now available. Use claudestory_phase_create to add phases and claudestory_ticket_create to add tickets.");
|
|
4440
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
4441
|
+
});
|
|
4442
|
+
}
|
|
3770
4443
|
async function main() {
|
|
3771
4444
|
const root = tryDiscoverRoot();
|
|
3772
4445
|
const server = new McpServer(
|
|
3773
4446
|
{ name: "claudestory", version },
|
|
3774
4447
|
{
|
|
3775
|
-
instructions: root ? "Start with claudestory_status for a project overview, then claudestory_ticket_next for the highest-priority work, then claudestory_handover_latest for session context." : "No .story/ project found
|
|
4448
|
+
instructions: root ? "Start with claudestory_status for a project overview, then claudestory_ticket_next for the highest-priority work, then claudestory_handover_latest for session context." : "No .story/ project found. Use claudestory_init to initialize a new project, or navigate to a directory with .story/."
|
|
3776
4449
|
}
|
|
3777
4450
|
);
|
|
3778
4451
|
if (root) {
|
|
@@ -3780,13 +4453,8 @@ async function main() {
|
|
|
3780
4453
|
process.stderr.write(`claudestory MCP server running (root: ${root})
|
|
3781
4454
|
`);
|
|
3782
4455
|
} else {
|
|
3783
|
-
server
|
|
3784
|
-
|
|
3785
|
-
}, () => Promise.resolve({
|
|
3786
|
-
content: [{ type: "text", text: "No .story/ project found. Navigate to a directory containing .story/ or set CLAUDESTORY_PROJECT_ROOT." }],
|
|
3787
|
-
isError: true
|
|
3788
|
-
}));
|
|
3789
|
-
process.stderr.write("claudestory MCP server running (no project found \u2014 tools will report errors)\n");
|
|
4456
|
+
registerDegradedTools(server);
|
|
4457
|
+
process.stderr.write("claudestory MCP server running (no project \u2014 claudestory_init available)\n");
|
|
3790
4458
|
}
|
|
3791
4459
|
const transport = new StdioServerTransport();
|
|
3792
4460
|
await server.connect(transport);
|