@anthropologies/claudestory 0.1.10 → 0.1.12
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 +2593 -386
- package/dist/index.d.ts +65 -49
- package/dist/index.js +51 -16
- package/dist/mcp.js +4795 -2476
- package/package.json +1 -1
- package/src/skill/SKILL.md +130 -2
- package/src/skill/reference.md +15 -6
package/dist/cli.js
CHANGED
|
@@ -197,15 +197,9 @@ var init_note = __esm({
|
|
|
197
197
|
init_types();
|
|
198
198
|
NoteSchema = z4.object({
|
|
199
199
|
id: NoteIdSchema,
|
|
200
|
-
title: z4.
|
|
200
|
+
title: z4.string().nullable(),
|
|
201
201
|
content: z4.string().refine((v) => v.trim().length > 0, "Content cannot be empty"),
|
|
202
|
-
tags: z4.
|
|
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
|
-
),
|
|
202
|
+
tags: z4.array(z4.string()),
|
|
209
203
|
status: z4.enum(NOTE_STATUSES),
|
|
210
204
|
createdDate: DateSchema,
|
|
211
205
|
updatedDate: DateSchema
|
|
@@ -267,7 +261,8 @@ var init_config = __esm({
|
|
|
267
261
|
project: z6.string().min(1),
|
|
268
262
|
type: z6.string(),
|
|
269
263
|
language: z6.string(),
|
|
270
|
-
features: FeaturesSchema
|
|
264
|
+
features: FeaturesSchema,
|
|
265
|
+
recipe: z6.string().optional()
|
|
271
266
|
}).passthrough();
|
|
272
267
|
}
|
|
273
268
|
});
|
|
@@ -677,6 +672,12 @@ async function writeTicketUnlocked(ticket, root) {
|
|
|
677
672
|
const json = serializeJSON(parsed);
|
|
678
673
|
await atomicWrite(targetPath, json);
|
|
679
674
|
}
|
|
675
|
+
async function writeTicket(ticket, root) {
|
|
676
|
+
const wrapDir = resolve2(root, ".story");
|
|
677
|
+
await withLock(wrapDir, async () => {
|
|
678
|
+
await writeTicketUnlocked(ticket, root);
|
|
679
|
+
});
|
|
680
|
+
}
|
|
680
681
|
async function writeIssueUnlocked(issue, root) {
|
|
681
682
|
const parsed = IssueSchema.parse(issue);
|
|
682
683
|
if (!ISSUE_ID_REGEX.test(parsed.id)) {
|
|
@@ -691,6 +692,12 @@ async function writeIssueUnlocked(issue, root) {
|
|
|
691
692
|
const json = serializeJSON(parsed);
|
|
692
693
|
await atomicWrite(targetPath, json);
|
|
693
694
|
}
|
|
695
|
+
async function writeIssue(issue, root) {
|
|
696
|
+
const wrapDir = resolve2(root, ".story");
|
|
697
|
+
await withLock(wrapDir, async () => {
|
|
698
|
+
await writeIssueUnlocked(issue, root);
|
|
699
|
+
});
|
|
700
|
+
}
|
|
694
701
|
async function writeRoadmapUnlocked(roadmap, root) {
|
|
695
702
|
const parsed = RoadmapSchema.parse(roadmap);
|
|
696
703
|
const wrapDir = resolve2(root, ".story");
|
|
@@ -798,6 +805,12 @@ async function writeNoteUnlocked(note, root) {
|
|
|
798
805
|
const json = serializeJSON(parsed);
|
|
799
806
|
await atomicWrite(targetPath, json);
|
|
800
807
|
}
|
|
808
|
+
async function writeNote(note, root) {
|
|
809
|
+
const wrapDir = resolve2(root, ".story");
|
|
810
|
+
await withLock(wrapDir, async () => {
|
|
811
|
+
await writeNoteUnlocked(note, root);
|
|
812
|
+
});
|
|
813
|
+
}
|
|
801
814
|
async function deleteNote(id, root) {
|
|
802
815
|
if (!NOTE_ID_REGEX.test(id)) {
|
|
803
816
|
throw new ProjectLoaderError(
|
|
@@ -1519,6 +1532,7 @@ __export(output_formatter_exports, {
|
|
|
1519
1532
|
formatRecap: () => formatRecap,
|
|
1520
1533
|
formatRecommendations: () => formatRecommendations,
|
|
1521
1534
|
formatReference: () => formatReference,
|
|
1535
|
+
formatSelftestResult: () => formatSelftestResult,
|
|
1522
1536
|
formatSnapshotResult: () => formatSnapshotResult,
|
|
1523
1537
|
formatStatus: () => formatStatus,
|
|
1524
1538
|
formatTicket: () => formatTicket,
|
|
@@ -2016,6 +2030,16 @@ function formatRecap(recap, state, format) {
|
|
|
2016
2030
|
lines.push(`- ${escapeMarkdownInline(name)} \u2014 **new**`);
|
|
2017
2031
|
}
|
|
2018
2032
|
}
|
|
2033
|
+
if (changes.handovers && (changes.handovers.added.length > 0 || changes.handovers.removed.length > 0)) {
|
|
2034
|
+
lines.push("");
|
|
2035
|
+
lines.push("## Handovers");
|
|
2036
|
+
for (const h of changes.handovers.added) {
|
|
2037
|
+
lines.push(`- ${h} \u2014 **new**`);
|
|
2038
|
+
}
|
|
2039
|
+
for (const h of changes.handovers.removed) {
|
|
2040
|
+
lines.push(`- ${h} \u2014 removed`);
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2019
2043
|
if (changes.notes && (changes.notes.added.length > 0 || changes.notes.removed.length > 0 || changes.notes.updated.length > 0)) {
|
|
2020
2044
|
lines.push("");
|
|
2021
2045
|
lines.push("## Notes");
|
|
@@ -2237,7 +2261,35 @@ function formatFullExport(state, format) {
|
|
|
2237
2261
|
return lines.join("\n");
|
|
2238
2262
|
}
|
|
2239
2263
|
function hasAnyChanges(diff) {
|
|
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;
|
|
2264
|
+
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;
|
|
2265
|
+
}
|
|
2266
|
+
function formatSelftestResult(result, format) {
|
|
2267
|
+
if (format === "json") {
|
|
2268
|
+
return JSON.stringify(successEnvelope(result), null, 2);
|
|
2269
|
+
}
|
|
2270
|
+
const lines = ["# Self-test Report", ""];
|
|
2271
|
+
const entities = ["ticket", "issue", "note"];
|
|
2272
|
+
for (const entity of entities) {
|
|
2273
|
+
const checks = result.results.filter((r) => r.entity === entity);
|
|
2274
|
+
if (checks.length === 0) continue;
|
|
2275
|
+
lines.push(`## ${entity.charAt(0).toUpperCase() + entity.slice(1)}`);
|
|
2276
|
+
for (const check of checks) {
|
|
2277
|
+
const mark = check.passed ? "[x]" : "[ ]";
|
|
2278
|
+
const suffix = check.passed ? "" : ` \u2014 ${check.detail}`;
|
|
2279
|
+
lines.push(`- ${mark} ${check.step}${suffix}`);
|
|
2280
|
+
}
|
|
2281
|
+
lines.push("");
|
|
2282
|
+
}
|
|
2283
|
+
if (result.cleanupErrors.length > 0) {
|
|
2284
|
+
lines.push("## Cleanup Warnings");
|
|
2285
|
+
lines.push("");
|
|
2286
|
+
for (const err of result.cleanupErrors) {
|
|
2287
|
+
lines.push(`- ${err}`);
|
|
2288
|
+
}
|
|
2289
|
+
lines.push("");
|
|
2290
|
+
}
|
|
2291
|
+
lines.push(`Result: ${result.passed}/${result.total} passed`);
|
|
2292
|
+
return lines.join("\n");
|
|
2241
2293
|
}
|
|
2242
2294
|
function truncate(text, maxLen) {
|
|
2243
2295
|
if (text.length <= maxLen) return text;
|
|
@@ -2721,9 +2773,9 @@ async function handleHandoverCreate(content, slugRaw, format, root) {
|
|
|
2721
2773
|
const datePrefix = `${date}-`;
|
|
2722
2774
|
const seqRegex = new RegExp(`^${date}-(\\d{2})-`);
|
|
2723
2775
|
let maxSeq = 0;
|
|
2724
|
-
const { readdirSync } = await import("fs");
|
|
2776
|
+
const { readdirSync: readdirSync2 } = await import("fs");
|
|
2725
2777
|
try {
|
|
2726
|
-
for (const f of
|
|
2778
|
+
for (const f of readdirSync2(handoversDir)) {
|
|
2727
2779
|
const m = f.match(seqRegex);
|
|
2728
2780
|
if (m) {
|
|
2729
2781
|
const n = parseInt(m[1], 10);
|
|
@@ -2814,11 +2866,9 @@ async function handleBlockerClear(name, note, format, root) {
|
|
|
2814
2866
|
const updated = {
|
|
2815
2867
|
...existing,
|
|
2816
2868
|
cleared: true,
|
|
2817
|
-
clearedDate: todayISO()
|
|
2869
|
+
clearedDate: todayISO(),
|
|
2870
|
+
...note !== void 0 && { note }
|
|
2818
2871
|
};
|
|
2819
|
-
if (note !== void 0) {
|
|
2820
|
-
updated.note = note;
|
|
2821
|
-
}
|
|
2822
2872
|
const newBlockers = [...state.roadmap.blockers];
|
|
2823
2873
|
newBlockers[idx] = updated;
|
|
2824
2874
|
const newRoadmap = { ...state.roadmap, blockers: newBlockers };
|
|
@@ -3046,6 +3096,12 @@ async function handleTicketUpdate(id, updates, format, root) {
|
|
|
3046
3096
|
`Unknown ticket status "${updates.status}": must be one of ${TICKET_STATUSES.join(", ")}`
|
|
3047
3097
|
);
|
|
3048
3098
|
}
|
|
3099
|
+
if (updates.type !== void 0 && !TICKET_TYPES.includes(updates.type)) {
|
|
3100
|
+
throw new CliValidationError(
|
|
3101
|
+
"invalid_input",
|
|
3102
|
+
`Unknown ticket type "${updates.type}": must be one of ${TICKET_TYPES.join(", ")}`
|
|
3103
|
+
);
|
|
3104
|
+
}
|
|
3049
3105
|
let updatedTicket;
|
|
3050
3106
|
await withProjectLock(root, { strict: true }, async ({ state }) => {
|
|
3051
3107
|
const existing = state.ticketByID(id);
|
|
@@ -3061,21 +3117,26 @@ async function handleTicketUpdate(id, updates, format, root) {
|
|
|
3061
3117
|
if (updates.parentTicket) {
|
|
3062
3118
|
validateParentTicket(updates.parentTicket, id, state);
|
|
3063
3119
|
}
|
|
3064
|
-
const
|
|
3065
|
-
if (updates.title !== void 0) ticket.title = updates.title;
|
|
3066
|
-
if (updates.description !== void 0) ticket.description = updates.description;
|
|
3067
|
-
if (updates.phase !== void 0) ticket.phase = updates.phase;
|
|
3068
|
-
if (updates.order !== void 0) ticket.order = updates.order;
|
|
3069
|
-
if (updates.blockedBy !== void 0) ticket.blockedBy = updates.blockedBy;
|
|
3070
|
-
if (updates.parentTicket !== void 0) ticket.parentTicket = updates.parentTicket === null ? void 0 : updates.parentTicket;
|
|
3120
|
+
const statusChanges = {};
|
|
3071
3121
|
if (updates.status !== void 0 && updates.status !== existing.status) {
|
|
3072
|
-
|
|
3122
|
+
statusChanges.status = updates.status;
|
|
3073
3123
|
if (updates.status === "complete" && existing.status !== "complete") {
|
|
3074
|
-
|
|
3124
|
+
statusChanges.completedDate = todayISO();
|
|
3075
3125
|
} else if (updates.status !== "complete" && existing.status === "complete") {
|
|
3076
|
-
|
|
3126
|
+
statusChanges.completedDate = null;
|
|
3077
3127
|
}
|
|
3078
3128
|
}
|
|
3129
|
+
const ticket = {
|
|
3130
|
+
...existing,
|
|
3131
|
+
...updates.title !== void 0 && { title: updates.title },
|
|
3132
|
+
...updates.type !== void 0 && { type: updates.type },
|
|
3133
|
+
...updates.description !== void 0 && { description: updates.description },
|
|
3134
|
+
...updates.phase !== void 0 && { phase: updates.phase },
|
|
3135
|
+
...updates.order !== void 0 && { order: updates.order },
|
|
3136
|
+
...updates.blockedBy !== void 0 && { blockedBy: updates.blockedBy },
|
|
3137
|
+
...updates.parentTicket !== void 0 && { parentTicket: updates.parentTicket },
|
|
3138
|
+
...statusChanges
|
|
3139
|
+
};
|
|
3079
3140
|
validatePostWriteState(ticket, state, false);
|
|
3080
3141
|
await writeTicketUnlocked(ticket, root);
|
|
3081
3142
|
updatedTicket = ticket;
|
|
@@ -3191,6 +3252,9 @@ async function handleIssueCreate(args, format, root) {
|
|
|
3191
3252
|
}
|
|
3192
3253
|
let createdIssue;
|
|
3193
3254
|
await withProjectLock(root, { strict: true }, async ({ state }) => {
|
|
3255
|
+
if (args.phase && !state.roadmap.phases.some((p) => p.id === args.phase)) {
|
|
3256
|
+
throw new CliValidationError("invalid_input", `Phase "${args.phase}" not found in roadmap`);
|
|
3257
|
+
}
|
|
3194
3258
|
if (args.relatedTickets.length > 0) {
|
|
3195
3259
|
validateRelatedTickets(args.relatedTickets, state);
|
|
3196
3260
|
}
|
|
@@ -3238,25 +3302,36 @@ async function handleIssueUpdate(id, updates, format, root) {
|
|
|
3238
3302
|
if (!existing) {
|
|
3239
3303
|
throw new CliValidationError("not_found", `Issue ${id} not found`);
|
|
3240
3304
|
}
|
|
3305
|
+
if (updates.phase !== void 0 && updates.phase !== null) {
|
|
3306
|
+
if (!state.roadmap.phases.some((p) => p.id === updates.phase)) {
|
|
3307
|
+
throw new CliValidationError("invalid_input", `Phase "${updates.phase}" not found in roadmap`);
|
|
3308
|
+
}
|
|
3309
|
+
}
|
|
3241
3310
|
if (updates.relatedTickets) {
|
|
3242
3311
|
validateRelatedTickets(updates.relatedTickets, state);
|
|
3243
3312
|
}
|
|
3244
|
-
const
|
|
3245
|
-
if (updates.title !== void 0) issue.title = updates.title;
|
|
3246
|
-
if (updates.severity !== void 0) issue.severity = updates.severity;
|
|
3247
|
-
if (updates.impact !== void 0) issue.impact = updates.impact;
|
|
3248
|
-
if (updates.resolution !== void 0) issue.resolution = updates.resolution;
|
|
3249
|
-
if (updates.components !== void 0) issue.components = updates.components;
|
|
3250
|
-
if (updates.relatedTickets !== void 0) issue.relatedTickets = updates.relatedTickets;
|
|
3251
|
-
if (updates.location !== void 0) issue.location = updates.location;
|
|
3313
|
+
const statusChanges = {};
|
|
3252
3314
|
if (updates.status !== void 0 && updates.status !== existing.status) {
|
|
3253
|
-
|
|
3315
|
+
statusChanges.status = updates.status;
|
|
3254
3316
|
if (updates.status === "resolved" && existing.status !== "resolved") {
|
|
3255
|
-
|
|
3317
|
+
statusChanges.resolvedDate = todayISO();
|
|
3256
3318
|
} else if (updates.status !== "resolved" && existing.status === "resolved") {
|
|
3257
|
-
|
|
3319
|
+
statusChanges.resolvedDate = null;
|
|
3258
3320
|
}
|
|
3259
3321
|
}
|
|
3322
|
+
const issue = {
|
|
3323
|
+
...existing,
|
|
3324
|
+
...updates.title !== void 0 && { title: updates.title },
|
|
3325
|
+
...updates.severity !== void 0 && { severity: updates.severity },
|
|
3326
|
+
...updates.impact !== void 0 && { impact: updates.impact },
|
|
3327
|
+
...updates.resolution !== void 0 && { resolution: updates.resolution },
|
|
3328
|
+
...updates.components !== void 0 && { components: updates.components },
|
|
3329
|
+
...updates.relatedTickets !== void 0 && { relatedTickets: updates.relatedTickets },
|
|
3330
|
+
...updates.location !== void 0 && { location: updates.location },
|
|
3331
|
+
...updates.order !== void 0 && { order: updates.order },
|
|
3332
|
+
...updates.phase !== void 0 && { phase: updates.phase },
|
|
3333
|
+
...statusChanges
|
|
3334
|
+
};
|
|
3260
3335
|
validatePostWriteIssueState(issue, state, false);
|
|
3261
3336
|
await writeIssueUnlocked(issue, root);
|
|
3262
3337
|
updatedIssue = issue;
|
|
@@ -3289,6 +3364,14 @@ var init_issue2 = __esm({
|
|
|
3289
3364
|
});
|
|
3290
3365
|
|
|
3291
3366
|
// src/core/snapshot.ts
|
|
3367
|
+
var snapshot_exports = {};
|
|
3368
|
+
__export(snapshot_exports, {
|
|
3369
|
+
SnapshotV1Schema: () => SnapshotV1Schema,
|
|
3370
|
+
buildRecap: () => buildRecap,
|
|
3371
|
+
diffStates: () => diffStates,
|
|
3372
|
+
loadLatestSnapshot: () => loadLatestSnapshot,
|
|
3373
|
+
saveSnapshot: () => saveSnapshot
|
|
3374
|
+
});
|
|
3292
3375
|
import { readdir as readdir3, readFile as readFile3, mkdir as mkdir3, unlink as unlink2 } from "fs/promises";
|
|
3293
3376
|
import { existsSync as existsSync5 } from "fs";
|
|
3294
3377
|
import { join as join5, resolve as resolve5 } from "path";
|
|
@@ -3309,6 +3392,7 @@ async function saveSnapshot(root, loadResult) {
|
|
|
3309
3392
|
tickets: [...state.tickets],
|
|
3310
3393
|
issues: [...state.issues],
|
|
3311
3394
|
notes: [...state.notes],
|
|
3395
|
+
handoverFilenames: [...state.handoverFilenames],
|
|
3312
3396
|
...warnings.length > 0 ? {
|
|
3313
3397
|
warnings: warnings.map((w) => ({
|
|
3314
3398
|
type: w.type,
|
|
@@ -3461,12 +3545,23 @@ function diffStates(snapshotState, currentState) {
|
|
|
3461
3545
|
notesRemoved.push({ id, title: snap.title });
|
|
3462
3546
|
}
|
|
3463
3547
|
}
|
|
3548
|
+
const snapHandovers = new Set(snapshotState.handoverFilenames);
|
|
3549
|
+
const curHandovers = new Set(currentState.handoverFilenames);
|
|
3550
|
+
const handoversAdded = [];
|
|
3551
|
+
const handoversRemoved = [];
|
|
3552
|
+
for (const h of curHandovers) {
|
|
3553
|
+
if (!snapHandovers.has(h)) handoversAdded.push(h);
|
|
3554
|
+
}
|
|
3555
|
+
for (const h of snapHandovers) {
|
|
3556
|
+
if (!curHandovers.has(h)) handoversRemoved.push(h);
|
|
3557
|
+
}
|
|
3464
3558
|
return {
|
|
3465
3559
|
tickets: { added: ticketsAdded, removed: ticketsRemoved, statusChanged: ticketsStatusChanged, descriptionChanged: ticketsDescriptionChanged },
|
|
3466
3560
|
issues: { added: issuesAdded, resolved: issuesResolved, statusChanged: issuesStatusChanged, impactChanged: issuesImpactChanged },
|
|
3467
3561
|
blockers: { added: blockersAdded, cleared: blockersCleared },
|
|
3468
3562
|
phases: { added: phasesAdded, removed: phasesRemoved, statusChanged: phasesStatusChanged },
|
|
3469
|
-
notes: { added: notesAdded, removed: notesRemoved, updated: notesUpdated }
|
|
3563
|
+
notes: { added: notesAdded, removed: notesRemoved, updated: notesUpdated },
|
|
3564
|
+
handovers: { added: handoversAdded, removed: handoversRemoved }
|
|
3470
3565
|
};
|
|
3471
3566
|
}
|
|
3472
3567
|
function buildRecap(currentState, snapshotInfo) {
|
|
@@ -3494,7 +3589,7 @@ function buildRecap(currentState, snapshotInfo) {
|
|
|
3494
3589
|
notes: snapshot.notes ?? [],
|
|
3495
3590
|
roadmap: snapshot.roadmap,
|
|
3496
3591
|
config: snapshot.config,
|
|
3497
|
-
handoverFilenames: []
|
|
3592
|
+
handoverFilenames: snapshot.handoverFilenames ?? []
|
|
3498
3593
|
});
|
|
3499
3594
|
const changes = diffStates(snapshotState, currentState);
|
|
3500
3595
|
const recentlyClearedBlockers = changes.blockers.cleared;
|
|
@@ -3566,6 +3661,7 @@ var init_snapshot = __esm({
|
|
|
3566
3661
|
tickets: z7.array(TicketSchema),
|
|
3567
3662
|
issues: z7.array(IssueSchema),
|
|
3568
3663
|
notes: z7.array(NoteSchema).optional().default([]),
|
|
3664
|
+
handoverFilenames: z7.array(z7.string()).optional().default([]),
|
|
3569
3665
|
warnings: z7.array(LoadWarningSchema).optional()
|
|
3570
3666
|
});
|
|
3571
3667
|
MAX_SNAPSHOTS = 20;
|
|
@@ -3666,23 +3762,22 @@ async function handleNoteUpdate(id, updates, format, root) {
|
|
|
3666
3762
|
if (!existing) {
|
|
3667
3763
|
throw new CliValidationError("not_found", `Note ${id} not found`);
|
|
3668
3764
|
}
|
|
3669
|
-
const
|
|
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
|
-
}
|
|
3765
|
+
const tagsUpdate = {};
|
|
3677
3766
|
if (updates.clearTags) {
|
|
3678
|
-
|
|
3767
|
+
tagsUpdate.tags = [];
|
|
3679
3768
|
} else if (updates.tags !== void 0) {
|
|
3680
|
-
|
|
3681
|
-
}
|
|
3682
|
-
if (updates.status !== void 0) {
|
|
3683
|
-
note.status = updates.status;
|
|
3769
|
+
tagsUpdate.tags = normalizeTags(updates.tags);
|
|
3684
3770
|
}
|
|
3685
|
-
note
|
|
3771
|
+
const note = {
|
|
3772
|
+
...existing,
|
|
3773
|
+
...updates.content !== void 0 && { content: updates.content },
|
|
3774
|
+
...updates.title !== void 0 && {
|
|
3775
|
+
title: !updates.title?.trim() ? null : updates.title
|
|
3776
|
+
},
|
|
3777
|
+
...tagsUpdate,
|
|
3778
|
+
...updates.status !== void 0 && { status: updates.status },
|
|
3779
|
+
updatedDate: todayISO()
|
|
3780
|
+
};
|
|
3686
3781
|
await writeNoteUnlocked(note, root);
|
|
3687
3782
|
updatedNote = note;
|
|
3688
3783
|
});
|
|
@@ -4005,116 +4100,1864 @@ var init_export = __esm({
|
|
|
4005
4100
|
}
|
|
4006
4101
|
});
|
|
4007
4102
|
|
|
4008
|
-
// src/cli/commands/
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4103
|
+
// src/cli/commands/selftest.ts
|
|
4104
|
+
async function handleSelftest(root, format, failAfter) {
|
|
4105
|
+
const results = [];
|
|
4106
|
+
const createdIds = [];
|
|
4107
|
+
const cleanupErrors = [];
|
|
4108
|
+
function record(entity, step, passed2, detail) {
|
|
4109
|
+
results.push({ entity, step, passed: passed2, detail });
|
|
4110
|
+
if (failAfter !== void 0 && results.filter((r) => r.passed).length >= failAfter) {
|
|
4111
|
+
throw new Error(`failAfter(${failAfter}): induced failure for testing`);
|
|
4112
|
+
}
|
|
4013
4113
|
}
|
|
4014
|
-
|
|
4015
|
-
|
|
4114
|
+
try {
|
|
4115
|
+
let ticketId;
|
|
4116
|
+
try {
|
|
4117
|
+
const { state } = await loadProject(root);
|
|
4118
|
+
ticketId = nextTicketID(state.tickets);
|
|
4119
|
+
const today = todayISO();
|
|
4120
|
+
const ticket = {
|
|
4121
|
+
id: ticketId,
|
|
4122
|
+
title: "selftest ticket",
|
|
4123
|
+
type: "chore",
|
|
4124
|
+
status: "open",
|
|
4125
|
+
phase: null,
|
|
4126
|
+
order: 0,
|
|
4127
|
+
description: "Integration smoke test \u2014 will be deleted.",
|
|
4128
|
+
createdDate: today,
|
|
4129
|
+
completedDate: null,
|
|
4130
|
+
blockedBy: [],
|
|
4131
|
+
parentTicket: null
|
|
4132
|
+
};
|
|
4133
|
+
await writeTicket(ticket, root);
|
|
4134
|
+
createdIds.push({ type: "ticket", id: ticketId });
|
|
4135
|
+
record("ticket", "create", true, `Created ${ticketId}`);
|
|
4136
|
+
} catch (err) {
|
|
4137
|
+
record("ticket", "create", false, errMsg(err));
|
|
4138
|
+
}
|
|
4139
|
+
if (ticketId) {
|
|
4140
|
+
try {
|
|
4141
|
+
const { state } = await loadProject(root);
|
|
4142
|
+
const found = state.ticketByID(ticketId);
|
|
4143
|
+
if (!found) throw new Error(`${ticketId} not found after create`);
|
|
4144
|
+
record("ticket", "get", true, `Found ${ticketId}`);
|
|
4145
|
+
} catch (err) {
|
|
4146
|
+
record("ticket", "get", false, errMsg(err));
|
|
4147
|
+
}
|
|
4148
|
+
try {
|
|
4149
|
+
const { state } = await loadProject(root);
|
|
4150
|
+
const existing = state.ticketByID(ticketId);
|
|
4151
|
+
if (!existing) throw new Error(`${ticketId} not found for update`);
|
|
4152
|
+
const updated = { ...existing, status: "inprogress" };
|
|
4153
|
+
await writeTicket(updated, root);
|
|
4154
|
+
record("ticket", "update", true, `Updated ${ticketId} status \u2192 inprogress`);
|
|
4155
|
+
} catch (err) {
|
|
4156
|
+
record("ticket", "update", false, errMsg(err));
|
|
4157
|
+
}
|
|
4158
|
+
try {
|
|
4159
|
+
const { state } = await loadProject(root);
|
|
4160
|
+
const found = state.ticketByID(ticketId);
|
|
4161
|
+
if (!found) throw new Error(`${ticketId} not found for verify`);
|
|
4162
|
+
if (found.status !== "inprogress") throw new Error(`Expected inprogress, got ${found.status}`);
|
|
4163
|
+
record("ticket", "verify update", true, `Verified ${ticketId} status = inprogress`);
|
|
4164
|
+
} catch (err) {
|
|
4165
|
+
record("ticket", "verify update", false, errMsg(err));
|
|
4166
|
+
}
|
|
4167
|
+
try {
|
|
4168
|
+
await deleteTicket(ticketId, root, { force: true });
|
|
4169
|
+
createdIds.splice(createdIds.findIndex((c) => c.id === ticketId), 1);
|
|
4170
|
+
record("ticket", "delete", true, `Deleted ${ticketId}`);
|
|
4171
|
+
} catch (err) {
|
|
4172
|
+
record("ticket", "delete", false, errMsg(err));
|
|
4173
|
+
}
|
|
4174
|
+
try {
|
|
4175
|
+
const { state } = await loadProject(root);
|
|
4176
|
+
const found = state.ticketByID(ticketId);
|
|
4177
|
+
if (found) throw new Error(`${ticketId} still exists after delete`);
|
|
4178
|
+
record("ticket", "verify delete", true, `Confirmed ${ticketId} absent`);
|
|
4179
|
+
} catch (err) {
|
|
4180
|
+
record("ticket", "verify delete", false, errMsg(err));
|
|
4181
|
+
}
|
|
4182
|
+
}
|
|
4183
|
+
let issueId;
|
|
4184
|
+
try {
|
|
4185
|
+
const { state } = await loadProject(root);
|
|
4186
|
+
issueId = nextIssueID(state.issues);
|
|
4187
|
+
const today = todayISO();
|
|
4188
|
+
const issue = {
|
|
4189
|
+
id: issueId,
|
|
4190
|
+
title: "selftest issue",
|
|
4191
|
+
status: "open",
|
|
4192
|
+
severity: "low",
|
|
4193
|
+
components: [],
|
|
4194
|
+
impact: "Integration smoke test \u2014 will be deleted.",
|
|
4195
|
+
resolution: null,
|
|
4196
|
+
location: [],
|
|
4197
|
+
discoveredDate: today,
|
|
4198
|
+
resolvedDate: null,
|
|
4199
|
+
relatedTickets: [],
|
|
4200
|
+
order: 0,
|
|
4201
|
+
phase: null
|
|
4202
|
+
};
|
|
4203
|
+
await writeIssue(issue, root);
|
|
4204
|
+
createdIds.push({ type: "issue", id: issueId });
|
|
4205
|
+
record("issue", "create", true, `Created ${issueId}`);
|
|
4206
|
+
} catch (err) {
|
|
4207
|
+
record("issue", "create", false, errMsg(err));
|
|
4208
|
+
}
|
|
4209
|
+
if (issueId) {
|
|
4210
|
+
try {
|
|
4211
|
+
const { state } = await loadProject(root);
|
|
4212
|
+
const found = state.issueByID(issueId);
|
|
4213
|
+
if (!found) throw new Error(`${issueId} not found after create`);
|
|
4214
|
+
record("issue", "get", true, `Found ${issueId}`);
|
|
4215
|
+
} catch (err) {
|
|
4216
|
+
record("issue", "get", false, errMsg(err));
|
|
4217
|
+
}
|
|
4218
|
+
try {
|
|
4219
|
+
const { state } = await loadProject(root);
|
|
4220
|
+
const existing = state.issueByID(issueId);
|
|
4221
|
+
if (!existing) throw new Error(`${issueId} not found for update`);
|
|
4222
|
+
const updated = { ...existing, status: "inprogress" };
|
|
4223
|
+
await writeIssue(updated, root);
|
|
4224
|
+
record("issue", "update", true, `Updated ${issueId} status \u2192 inprogress`);
|
|
4225
|
+
} catch (err) {
|
|
4226
|
+
record("issue", "update", false, errMsg(err));
|
|
4227
|
+
}
|
|
4228
|
+
try {
|
|
4229
|
+
const { state } = await loadProject(root);
|
|
4230
|
+
const found = state.issueByID(issueId);
|
|
4231
|
+
if (!found) throw new Error(`${issueId} not found for verify`);
|
|
4232
|
+
if (found.status !== "inprogress") throw new Error(`Expected inprogress, got ${found.status}`);
|
|
4233
|
+
record("issue", "verify update", true, `Verified ${issueId} status = inprogress`);
|
|
4234
|
+
} catch (err) {
|
|
4235
|
+
record("issue", "verify update", false, errMsg(err));
|
|
4236
|
+
}
|
|
4237
|
+
try {
|
|
4238
|
+
await deleteIssue(issueId, root);
|
|
4239
|
+
createdIds.splice(createdIds.findIndex((c) => c.id === issueId), 1);
|
|
4240
|
+
record("issue", "delete", true, `Deleted ${issueId}`);
|
|
4241
|
+
} catch (err) {
|
|
4242
|
+
record("issue", "delete", false, errMsg(err));
|
|
4243
|
+
}
|
|
4244
|
+
try {
|
|
4245
|
+
const { state } = await loadProject(root);
|
|
4246
|
+
const found = state.issueByID(issueId);
|
|
4247
|
+
if (found) throw new Error(`${issueId} still exists after delete`);
|
|
4248
|
+
record("issue", "verify delete", true, `Confirmed ${issueId} absent`);
|
|
4249
|
+
} catch (err) {
|
|
4250
|
+
record("issue", "verify delete", false, errMsg(err));
|
|
4251
|
+
}
|
|
4252
|
+
}
|
|
4253
|
+
let noteId;
|
|
4254
|
+
try {
|
|
4255
|
+
const { state } = await loadProject(root);
|
|
4256
|
+
noteId = nextNoteID(state.notes);
|
|
4257
|
+
const today = todayISO();
|
|
4258
|
+
const note = {
|
|
4259
|
+
id: noteId,
|
|
4260
|
+
title: "selftest note",
|
|
4261
|
+
content: "Integration smoke test \u2014 will be deleted.",
|
|
4262
|
+
tags: [],
|
|
4263
|
+
status: "active",
|
|
4264
|
+
createdDate: today,
|
|
4265
|
+
updatedDate: today
|
|
4266
|
+
};
|
|
4267
|
+
await writeNote(note, root);
|
|
4268
|
+
createdIds.push({ type: "note", id: noteId });
|
|
4269
|
+
record("note", "create", true, `Created ${noteId}`);
|
|
4270
|
+
} catch (err) {
|
|
4271
|
+
record("note", "create", false, errMsg(err));
|
|
4272
|
+
}
|
|
4273
|
+
if (noteId) {
|
|
4274
|
+
try {
|
|
4275
|
+
const { state } = await loadProject(root);
|
|
4276
|
+
const found = state.noteByID(noteId);
|
|
4277
|
+
if (!found) throw new Error(`${noteId} not found after create`);
|
|
4278
|
+
record("note", "get", true, `Found ${noteId}`);
|
|
4279
|
+
} catch (err) {
|
|
4280
|
+
record("note", "get", false, errMsg(err));
|
|
4281
|
+
}
|
|
4282
|
+
try {
|
|
4283
|
+
const { state } = await loadProject(root);
|
|
4284
|
+
const existing = state.noteByID(noteId);
|
|
4285
|
+
if (!existing) throw new Error(`${noteId} not found for update`);
|
|
4286
|
+
const updated = { ...existing, status: "archived", updatedDate: todayISO() };
|
|
4287
|
+
await writeNote(updated, root);
|
|
4288
|
+
record("note", "update", true, `Updated ${noteId} status \u2192 archived`);
|
|
4289
|
+
} catch (err) {
|
|
4290
|
+
record("note", "update", false, errMsg(err));
|
|
4291
|
+
}
|
|
4292
|
+
try {
|
|
4293
|
+
const { state } = await loadProject(root);
|
|
4294
|
+
const found = state.noteByID(noteId);
|
|
4295
|
+
if (!found) throw new Error(`${noteId} not found for verify`);
|
|
4296
|
+
if (found.status !== "archived") throw new Error(`Expected archived, got ${found.status}`);
|
|
4297
|
+
record("note", "verify update", true, `Verified ${noteId} status = archived`);
|
|
4298
|
+
} catch (err) {
|
|
4299
|
+
record("note", "verify update", false, errMsg(err));
|
|
4300
|
+
}
|
|
4301
|
+
try {
|
|
4302
|
+
await deleteNote(noteId, root);
|
|
4303
|
+
createdIds.splice(createdIds.findIndex((c) => c.id === noteId), 1);
|
|
4304
|
+
record("note", "delete", true, `Deleted ${noteId}`);
|
|
4305
|
+
} catch (err) {
|
|
4306
|
+
record("note", "delete", false, errMsg(err));
|
|
4307
|
+
}
|
|
4308
|
+
try {
|
|
4309
|
+
const { state } = await loadProject(root);
|
|
4310
|
+
const found = state.noteByID(noteId);
|
|
4311
|
+
if (found) throw new Error(`${noteId} still exists after delete`);
|
|
4312
|
+
record("note", "verify delete", true, `Confirmed ${noteId} absent`);
|
|
4313
|
+
} catch (err) {
|
|
4314
|
+
record("note", "verify delete", false, errMsg(err));
|
|
4315
|
+
}
|
|
4316
|
+
}
|
|
4317
|
+
} finally {
|
|
4318
|
+
for (const { type, id } of createdIds.reverse()) {
|
|
4319
|
+
try {
|
|
4320
|
+
if (type === "ticket") await deleteTicket(id, root, { force: true });
|
|
4321
|
+
else if (type === "issue") await deleteIssue(id, root);
|
|
4322
|
+
else await deleteNote(id, root);
|
|
4323
|
+
} catch (err) {
|
|
4324
|
+
cleanupErrors.push(`Failed to delete ${type} ${id}: ${errMsg(err)}`);
|
|
4325
|
+
}
|
|
4326
|
+
}
|
|
4016
4327
|
}
|
|
4328
|
+
const passed = results.filter((r) => r.passed).length;
|
|
4329
|
+
const failed = results.filter((r) => !r.passed).length;
|
|
4330
|
+
const result = {
|
|
4331
|
+
passed,
|
|
4332
|
+
failed,
|
|
4333
|
+
total: results.length,
|
|
4334
|
+
results,
|
|
4335
|
+
cleanupErrors
|
|
4336
|
+
};
|
|
4337
|
+
return { output: formatSelftestResult(result, format) };
|
|
4017
4338
|
}
|
|
4018
|
-
function
|
|
4019
|
-
return
|
|
4339
|
+
function errMsg(err) {
|
|
4340
|
+
return err instanceof Error ? err.message : String(err);
|
|
4020
4341
|
}
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4342
|
+
var init_selftest = __esm({
|
|
4343
|
+
"src/cli/commands/selftest.ts"() {
|
|
4344
|
+
"use strict";
|
|
4345
|
+
init_esm_shims();
|
|
4346
|
+
init_project_loader();
|
|
4347
|
+
init_id_allocation();
|
|
4348
|
+
init_output_formatter();
|
|
4349
|
+
init_helpers();
|
|
4029
4350
|
}
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
|
|
4351
|
+
});
|
|
4352
|
+
|
|
4353
|
+
// src/autonomous/session-types.ts
|
|
4354
|
+
import { realpathSync } from "fs";
|
|
4355
|
+
import { z as z8 } from "zod";
|
|
4356
|
+
function deriveClaudeStatus(state, waitingForRetry) {
|
|
4357
|
+
if (waitingForRetry) return "waiting";
|
|
4358
|
+
if (!state) return "idle";
|
|
4359
|
+
if (WORKING_STATES.has(state)) return "working";
|
|
4360
|
+
if (IDLE_STATES.has(state)) return "idle";
|
|
4361
|
+
return "unknown";
|
|
4362
|
+
}
|
|
4363
|
+
function deriveWorkspaceId(projectRoot) {
|
|
4364
|
+
return realpathSync(projectRoot);
|
|
4365
|
+
}
|
|
4366
|
+
var WORKING_STATES, IDLE_STATES, CURRENT_STATUS_SCHEMA_VERSION, WORKFLOW_STATES, WorkflowStateSchema, CURRENT_SESSION_SCHEMA_VERSION, SessionStateSchema;
|
|
4367
|
+
var init_session_types = __esm({
|
|
4368
|
+
"src/autonomous/session-types.ts"() {
|
|
4369
|
+
"use strict";
|
|
4370
|
+
init_esm_shims();
|
|
4371
|
+
WORKING_STATES = /* @__PURE__ */ new Set([
|
|
4372
|
+
"PLAN",
|
|
4373
|
+
"PLAN_REVIEW",
|
|
4374
|
+
"IMPLEMENT",
|
|
4375
|
+
"CODE_REVIEW",
|
|
4376
|
+
"FINALIZE",
|
|
4377
|
+
"COMPACT"
|
|
4378
|
+
]);
|
|
4379
|
+
IDLE_STATES = /* @__PURE__ */ new Set([
|
|
4380
|
+
"INIT",
|
|
4381
|
+
"LOAD_CONTEXT",
|
|
4382
|
+
"PICK_TICKET",
|
|
4383
|
+
"HANDOVER",
|
|
4384
|
+
"COMPLETE",
|
|
4385
|
+
"SESSION_END"
|
|
4386
|
+
]);
|
|
4387
|
+
CURRENT_STATUS_SCHEMA_VERSION = 1;
|
|
4388
|
+
WORKFLOW_STATES = [
|
|
4389
|
+
"INIT",
|
|
4390
|
+
"LOAD_CONTEXT",
|
|
4391
|
+
"PICK_TICKET",
|
|
4392
|
+
"PLAN",
|
|
4393
|
+
"PLAN_REVIEW",
|
|
4394
|
+
"IMPLEMENT",
|
|
4395
|
+
"CODE_REVIEW",
|
|
4396
|
+
"FINALIZE",
|
|
4397
|
+
"COMPACT",
|
|
4398
|
+
"HANDOVER",
|
|
4399
|
+
"COMPLETE",
|
|
4400
|
+
"SESSION_END"
|
|
4401
|
+
];
|
|
4402
|
+
WorkflowStateSchema = z8.enum(WORKFLOW_STATES);
|
|
4403
|
+
CURRENT_SESSION_SCHEMA_VERSION = 1;
|
|
4404
|
+
SessionStateSchema = z8.object({
|
|
4405
|
+
schemaVersion: z8.literal(CURRENT_SESSION_SCHEMA_VERSION),
|
|
4406
|
+
sessionId: z8.string().uuid(),
|
|
4407
|
+
recipe: z8.string(),
|
|
4408
|
+
state: z8.string(),
|
|
4409
|
+
previousState: z8.string().optional(),
|
|
4410
|
+
revision: z8.number().int().min(0),
|
|
4411
|
+
status: z8.enum(["active", "completed", "superseded"]).default("active"),
|
|
4412
|
+
// Ticket in progress
|
|
4413
|
+
ticket: z8.object({
|
|
4414
|
+
id: z8.string(),
|
|
4415
|
+
title: z8.string(),
|
|
4416
|
+
risk: z8.string().optional(),
|
|
4417
|
+
realizedRisk: z8.string().optional(),
|
|
4418
|
+
claimed: z8.boolean().default(false)
|
|
4419
|
+
}).optional(),
|
|
4420
|
+
// Review tracking
|
|
4421
|
+
reviews: z8.object({
|
|
4422
|
+
plan: z8.array(z8.object({
|
|
4423
|
+
round: z8.number(),
|
|
4424
|
+
reviewer: z8.string(),
|
|
4425
|
+
verdict: z8.string(),
|
|
4426
|
+
findingCount: z8.number(),
|
|
4427
|
+
criticalCount: z8.number(),
|
|
4428
|
+
majorCount: z8.number(),
|
|
4429
|
+
suggestionCount: z8.number(),
|
|
4430
|
+
codexSessionId: z8.string().optional(),
|
|
4431
|
+
timestamp: z8.string()
|
|
4432
|
+
})).default([]),
|
|
4433
|
+
code: z8.array(z8.object({
|
|
4434
|
+
round: z8.number(),
|
|
4435
|
+
reviewer: z8.string(),
|
|
4436
|
+
verdict: z8.string(),
|
|
4437
|
+
findingCount: z8.number(),
|
|
4438
|
+
criticalCount: z8.number(),
|
|
4439
|
+
majorCount: z8.number(),
|
|
4440
|
+
suggestionCount: z8.number(),
|
|
4441
|
+
codexSessionId: z8.string().optional(),
|
|
4442
|
+
timestamp: z8.string()
|
|
4443
|
+
})).default([])
|
|
4444
|
+
}).default({ plan: [], code: [] }),
|
|
4445
|
+
// Completed tickets this session
|
|
4446
|
+
completedTickets: z8.array(z8.object({
|
|
4447
|
+
id: z8.string(),
|
|
4448
|
+
title: z8.string().optional(),
|
|
4449
|
+
commitHash: z8.string().optional(),
|
|
4450
|
+
risk: z8.string().optional()
|
|
4451
|
+
})).default([]),
|
|
4452
|
+
// FINALIZE checkpoint
|
|
4453
|
+
finalizeCheckpoint: z8.enum(["staged", "precommit_passed", "committed"]).nullable().default(null),
|
|
4454
|
+
// Git state
|
|
4455
|
+
git: z8.object({
|
|
4456
|
+
branch: z8.string().nullable().default(null),
|
|
4457
|
+
initHead: z8.string().optional(),
|
|
4458
|
+
mergeBase: z8.string().nullable().default(null),
|
|
4459
|
+
expectedHead: z8.string().optional(),
|
|
4460
|
+
baseline: z8.object({
|
|
4461
|
+
porcelain: z8.array(z8.string()).default([]),
|
|
4462
|
+
dirtyTrackedFiles: z8.record(z8.object({ blobHash: z8.string() })).default({}),
|
|
4463
|
+
untrackedPaths: z8.array(z8.string()).default([])
|
|
4464
|
+
}).optional()
|
|
4465
|
+
}).default({ branch: null, mergeBase: null }),
|
|
4466
|
+
// Lease
|
|
4467
|
+
lease: z8.object({
|
|
4468
|
+
workspaceId: z8.string().optional(),
|
|
4469
|
+
lastHeartbeat: z8.string(),
|
|
4470
|
+
expiresAt: z8.string()
|
|
4471
|
+
}),
|
|
4472
|
+
// Context pressure
|
|
4473
|
+
contextPressure: z8.object({
|
|
4474
|
+
level: z8.string().default("low"),
|
|
4475
|
+
guideCallCount: z8.number().default(0),
|
|
4476
|
+
ticketsCompleted: z8.number().default(0),
|
|
4477
|
+
compactionCount: z8.number().default(0),
|
|
4478
|
+
eventsLogBytes: z8.number().default(0)
|
|
4479
|
+
}).default({ level: "low", guideCallCount: 0, ticketsCompleted: 0, compactionCount: 0, eventsLogBytes: 0 }),
|
|
4480
|
+
// Pending project mutation (for crash recovery)
|
|
4481
|
+
pendingProjectMutation: z8.any().nullable().default(null),
|
|
4482
|
+
// COMPACT resume
|
|
4483
|
+
resumeFromRevision: z8.number().nullable().default(null),
|
|
4484
|
+
preCompactState: z8.string().nullable().default(null),
|
|
4485
|
+
// Session metadata
|
|
4486
|
+
waitingForRetry: z8.boolean().default(false),
|
|
4487
|
+
lastGuideCall: z8.string().optional(),
|
|
4488
|
+
startedAt: z8.string(),
|
|
4489
|
+
guideCallCount: z8.number().default(0),
|
|
4490
|
+
// Supersession tracking
|
|
4491
|
+
supersededBy: z8.string().optional(),
|
|
4492
|
+
supersededSession: z8.string().optional(),
|
|
4493
|
+
stealReason: z8.string().optional(),
|
|
4494
|
+
// Recipe overrides
|
|
4495
|
+
config: z8.object({
|
|
4496
|
+
maxTicketsPerSession: z8.number().default(3),
|
|
4497
|
+
compactThreshold: z8.string().default("high"),
|
|
4498
|
+
reviewBackends: z8.array(z8.string()).default(["codex", "agent"])
|
|
4499
|
+
}).default({ maxTicketsPerSession: 3, compactThreshold: "high", reviewBackends: ["codex", "agent"] })
|
|
4500
|
+
}).passthrough();
|
|
4501
|
+
}
|
|
4502
|
+
});
|
|
4503
|
+
|
|
4504
|
+
// src/autonomous/session.ts
|
|
4505
|
+
import { randomUUID } from "crypto";
|
|
4506
|
+
import {
|
|
4507
|
+
mkdirSync,
|
|
4508
|
+
readdirSync,
|
|
4509
|
+
readFileSync,
|
|
4510
|
+
writeFileSync,
|
|
4511
|
+
renameSync,
|
|
4512
|
+
unlinkSync,
|
|
4513
|
+
existsSync as existsSync6,
|
|
4514
|
+
rmSync
|
|
4515
|
+
} from "fs";
|
|
4516
|
+
import { join as join6 } from "path";
|
|
4517
|
+
import lockfile2 from "proper-lockfile";
|
|
4518
|
+
function sessionsRoot(root) {
|
|
4519
|
+
return join6(root, ".story", SESSIONS_DIR);
|
|
4520
|
+
}
|
|
4521
|
+
function sessionDir(root, sessionId) {
|
|
4522
|
+
return join6(sessionsRoot(root), sessionId);
|
|
4523
|
+
}
|
|
4524
|
+
function statePath(dir) {
|
|
4525
|
+
return join6(dir, "state.json");
|
|
4526
|
+
}
|
|
4527
|
+
function eventsPath(dir) {
|
|
4528
|
+
return join6(dir, "events.log");
|
|
4529
|
+
}
|
|
4530
|
+
function createSession(root, recipe, workspaceId) {
|
|
4531
|
+
const id = randomUUID();
|
|
4532
|
+
const dir = sessionDir(root, id);
|
|
4533
|
+
mkdirSync(dir, { recursive: true });
|
|
4534
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4535
|
+
const state = {
|
|
4536
|
+
schemaVersion: CURRENT_SESSION_SCHEMA_VERSION,
|
|
4537
|
+
sessionId: id,
|
|
4538
|
+
recipe,
|
|
4539
|
+
state: "INIT",
|
|
4540
|
+
revision: 0,
|
|
4541
|
+
status: "active",
|
|
4542
|
+
reviews: { plan: [], code: [] },
|
|
4543
|
+
completedTickets: [],
|
|
4544
|
+
finalizeCheckpoint: null,
|
|
4545
|
+
git: { branch: null, mergeBase: null },
|
|
4546
|
+
lease: {
|
|
4547
|
+
workspaceId,
|
|
4548
|
+
lastHeartbeat: now,
|
|
4549
|
+
expiresAt: new Date(Date.now() + LEASE_DURATION_MS).toISOString()
|
|
4550
|
+
},
|
|
4551
|
+
contextPressure: {
|
|
4552
|
+
level: "low",
|
|
4553
|
+
guideCallCount: 0,
|
|
4554
|
+
ticketsCompleted: 0,
|
|
4555
|
+
compactionCount: 0,
|
|
4556
|
+
eventsLogBytes: 0
|
|
4557
|
+
},
|
|
4558
|
+
pendingProjectMutation: null,
|
|
4559
|
+
resumeFromRevision: null,
|
|
4560
|
+
preCompactState: null,
|
|
4561
|
+
waitingForRetry: false,
|
|
4562
|
+
lastGuideCall: now,
|
|
4563
|
+
startedAt: now,
|
|
4564
|
+
guideCallCount: 0,
|
|
4565
|
+
config: {
|
|
4566
|
+
maxTicketsPerSession: 3,
|
|
4567
|
+
compactThreshold: "high",
|
|
4568
|
+
reviewBackends: ["codex", "agent"]
|
|
4039
4569
|
}
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4570
|
+
};
|
|
4571
|
+
writeSessionSync(dir, state);
|
|
4572
|
+
return state;
|
|
4573
|
+
}
|
|
4574
|
+
function readSession(dir) {
|
|
4575
|
+
const path2 = statePath(dir);
|
|
4576
|
+
let raw;
|
|
4577
|
+
try {
|
|
4578
|
+
raw = readFileSync(path2, "utf-8");
|
|
4579
|
+
} catch {
|
|
4580
|
+
return null;
|
|
4044
4581
|
}
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4582
|
+
let parsed;
|
|
4583
|
+
try {
|
|
4584
|
+
parsed = JSON.parse(raw);
|
|
4585
|
+
} catch {
|
|
4586
|
+
return null;
|
|
4049
4587
|
}
|
|
4050
|
-
|
|
4588
|
+
const result = SessionStateSchema.safeParse(parsed);
|
|
4589
|
+
if (!result.success) return null;
|
|
4590
|
+
return result.data;
|
|
4051
4591
|
}
|
|
4052
|
-
function
|
|
4053
|
-
const
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
4592
|
+
function writeSessionSync(dir, state) {
|
|
4593
|
+
const path2 = statePath(dir);
|
|
4594
|
+
const updated = { ...state, revision: state.revision + 1 };
|
|
4595
|
+
const content = JSON.stringify(updated, null, 2) + "\n";
|
|
4596
|
+
const tmp = `${path2}.${process.pid}.tmp`;
|
|
4597
|
+
try {
|
|
4598
|
+
writeFileSync(tmp, content, "utf-8");
|
|
4599
|
+
renameSync(tmp, path2);
|
|
4600
|
+
} catch (err) {
|
|
4601
|
+
try {
|
|
4602
|
+
unlinkSync(tmp);
|
|
4603
|
+
} catch {
|
|
4604
|
+
}
|
|
4605
|
+
throw err;
|
|
4060
4606
|
}
|
|
4061
|
-
return
|
|
4607
|
+
return updated;
|
|
4062
4608
|
}
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
throw new CliValidationError("invalid_input", "Must specify either --after <phase-id> or --at-start");
|
|
4609
|
+
function appendEvent(dir, event) {
|
|
4610
|
+
try {
|
|
4611
|
+
const path2 = eventsPath(dir);
|
|
4612
|
+
const line = JSON.stringify(event) + "\n";
|
|
4613
|
+
writeFileSync(path2, line, { flag: "a", encoding: "utf-8" });
|
|
4614
|
+
} catch {
|
|
4070
4615
|
}
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
}
|
|
4076
|
-
|
|
4077
|
-
id: args.id,
|
|
4078
|
-
label: args.label,
|
|
4079
|
-
name: args.name,
|
|
4080
|
-
description: args.description
|
|
4081
|
-
};
|
|
4082
|
-
if (args.summary !== void 0) {
|
|
4083
|
-
phase.summary = args.summary;
|
|
4084
|
-
}
|
|
4085
|
-
const newPhases = [...state.roadmap.phases];
|
|
4086
|
-
if (args.atStart) {
|
|
4087
|
-
newPhases.unshift(phase);
|
|
4088
|
-
} else {
|
|
4089
|
-
const afterIdx = newPhases.findIndex((p) => p.id === args.after);
|
|
4090
|
-
if (afterIdx < 0) {
|
|
4091
|
-
throw new CliValidationError("not_found", `Phase "${args.after}" not found`);
|
|
4092
|
-
}
|
|
4093
|
-
newPhases.splice(afterIdx + 1, 0, phase);
|
|
4094
|
-
}
|
|
4095
|
-
const newRoadmap = { ...state.roadmap, phases: newPhases };
|
|
4096
|
-
await writeRoadmapUnlocked(newRoadmap, root);
|
|
4097
|
-
createdPhase = phase;
|
|
4098
|
-
});
|
|
4099
|
-
if (!createdPhase) throw new Error("Phase not created");
|
|
4100
|
-
if (format === "json") {
|
|
4101
|
-
return { output: JSON.stringify(successEnvelope(createdPhase), null, 2) };
|
|
4616
|
+
}
|
|
4617
|
+
function deleteSession(root, sessionId) {
|
|
4618
|
+
const dir = sessionDir(root, sessionId);
|
|
4619
|
+
try {
|
|
4620
|
+
rmSync(dir, { recursive: true, force: true });
|
|
4621
|
+
} catch {
|
|
4102
4622
|
}
|
|
4103
|
-
return { output: `Created phase ${createdPhase.id}: ${createdPhase.name}` };
|
|
4104
4623
|
}
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
4624
|
+
function refreshLease(state) {
|
|
4625
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4626
|
+
const newCallCount = state.guideCallCount + 1;
|
|
4627
|
+
return {
|
|
4628
|
+
...state,
|
|
4629
|
+
lease: {
|
|
4630
|
+
...state.lease,
|
|
4631
|
+
lastHeartbeat: now,
|
|
4632
|
+
expiresAt: new Date(Date.now() + LEASE_DURATION_MS).toISOString()
|
|
4633
|
+
},
|
|
4634
|
+
lastGuideCall: now,
|
|
4635
|
+
guideCallCount: newCallCount,
|
|
4636
|
+
contextPressure: {
|
|
4637
|
+
...state.contextPressure,
|
|
4638
|
+
guideCallCount: newCallCount,
|
|
4639
|
+
ticketsCompleted: state.completedTickets?.length ?? 0
|
|
4111
4640
|
}
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
|
|
4641
|
+
};
|
|
4642
|
+
}
|
|
4643
|
+
function isLeaseExpired(state) {
|
|
4644
|
+
if (!state.lease?.expiresAt) return true;
|
|
4645
|
+
const expires = new Date(state.lease.expiresAt).getTime();
|
|
4646
|
+
return Number.isNaN(expires) || expires <= Date.now();
|
|
4647
|
+
}
|
|
4648
|
+
function findActiveSessionFull(root) {
|
|
4649
|
+
const sessDir = sessionsRoot(root);
|
|
4650
|
+
let entries;
|
|
4651
|
+
try {
|
|
4652
|
+
entries = readdirSync(sessDir, { withFileTypes: true });
|
|
4653
|
+
} catch {
|
|
4654
|
+
return null;
|
|
4655
|
+
}
|
|
4656
|
+
let workspaceId;
|
|
4657
|
+
try {
|
|
4658
|
+
workspaceId = deriveWorkspaceId(root);
|
|
4659
|
+
} catch {
|
|
4660
|
+
return null;
|
|
4661
|
+
}
|
|
4662
|
+
let best = null;
|
|
4663
|
+
let bestGuideCall = 0;
|
|
4664
|
+
for (const entry of entries) {
|
|
4665
|
+
if (!entry.isDirectory()) continue;
|
|
4666
|
+
const dir = join6(sessDir, entry.name);
|
|
4667
|
+
const session = readSession(dir);
|
|
4668
|
+
if (!session) continue;
|
|
4669
|
+
if (session.status !== "active") continue;
|
|
4670
|
+
if (session.lease?.workspaceId && session.lease.workspaceId !== workspaceId) continue;
|
|
4671
|
+
if (isLeaseExpired(session)) continue;
|
|
4672
|
+
const guideCall = session.lastGuideCall ? new Date(session.lastGuideCall).getTime() : 0;
|
|
4673
|
+
const guideCallValid = Number.isNaN(guideCall) ? 0 : guideCall;
|
|
4674
|
+
if (!best || guideCallValid > bestGuideCall || guideCallValid === bestGuideCall && session.sessionId > best.state.sessionId) {
|
|
4675
|
+
best = { state: session, dir };
|
|
4676
|
+
bestGuideCall = guideCallValid;
|
|
4677
|
+
}
|
|
4678
|
+
}
|
|
4679
|
+
return best;
|
|
4680
|
+
}
|
|
4681
|
+
function findActiveSessionMinimal(root) {
|
|
4682
|
+
const result = findActiveSessionFull(root);
|
|
4683
|
+
return result?.state ?? null;
|
|
4684
|
+
}
|
|
4685
|
+
function findStaleSessions(root) {
|
|
4686
|
+
const sessDir = sessionsRoot(root);
|
|
4687
|
+
let entries;
|
|
4688
|
+
try {
|
|
4689
|
+
entries = readdirSync(sessDir, { withFileTypes: true });
|
|
4690
|
+
} catch {
|
|
4691
|
+
return [];
|
|
4692
|
+
}
|
|
4693
|
+
let workspaceId;
|
|
4694
|
+
try {
|
|
4695
|
+
workspaceId = deriveWorkspaceId(root);
|
|
4696
|
+
} catch {
|
|
4697
|
+
return [];
|
|
4698
|
+
}
|
|
4699
|
+
const results = [];
|
|
4700
|
+
for (const entry of entries) {
|
|
4701
|
+
if (!entry.isDirectory()) continue;
|
|
4702
|
+
const dir = join6(sessDir, entry.name);
|
|
4703
|
+
const session = readSession(dir);
|
|
4704
|
+
if (!session) continue;
|
|
4705
|
+
if (session.status !== "active") continue;
|
|
4706
|
+
if (session.lease?.workspaceId && session.lease.workspaceId !== workspaceId) continue;
|
|
4707
|
+
if (isLeaseExpired(session)) {
|
|
4708
|
+
results.push({ state: session, dir });
|
|
4709
|
+
}
|
|
4710
|
+
}
|
|
4711
|
+
return results;
|
|
4712
|
+
}
|
|
4713
|
+
function findSessionById(root, sessionId) {
|
|
4714
|
+
const dir = sessionDir(root, sessionId);
|
|
4715
|
+
if (!existsSync6(dir)) return null;
|
|
4716
|
+
const state = readSession(dir);
|
|
4717
|
+
if (!state) return null;
|
|
4718
|
+
return { state, dir };
|
|
4719
|
+
}
|
|
4720
|
+
async function withSessionLock(root, fn) {
|
|
4721
|
+
const sessDir = sessionsRoot(root);
|
|
4722
|
+
mkdirSync(sessDir, { recursive: true });
|
|
4723
|
+
let release;
|
|
4724
|
+
try {
|
|
4725
|
+
release = await lockfile2.lock(sessDir, {
|
|
4726
|
+
retries: { retries: 3, minTimeout: 100, maxTimeout: 1e3 },
|
|
4727
|
+
stale: 3e4,
|
|
4728
|
+
lockfilePath: join6(sessDir, ".lock")
|
|
4729
|
+
});
|
|
4730
|
+
return await fn();
|
|
4731
|
+
} finally {
|
|
4732
|
+
if (release) {
|
|
4733
|
+
try {
|
|
4734
|
+
await release();
|
|
4735
|
+
} catch {
|
|
4736
|
+
}
|
|
4737
|
+
}
|
|
4738
|
+
}
|
|
4739
|
+
}
|
|
4740
|
+
var LEASE_DURATION_MS, SESSIONS_DIR;
|
|
4741
|
+
var init_session = __esm({
|
|
4742
|
+
"src/autonomous/session.ts"() {
|
|
4743
|
+
"use strict";
|
|
4744
|
+
init_esm_shims();
|
|
4745
|
+
init_session_types();
|
|
4746
|
+
LEASE_DURATION_MS = 45 * 60 * 1e3;
|
|
4747
|
+
SESSIONS_DIR = "sessions";
|
|
4748
|
+
}
|
|
4749
|
+
});
|
|
4750
|
+
|
|
4751
|
+
// src/autonomous/state-machine.ts
|
|
4752
|
+
var init_state_machine = __esm({
|
|
4753
|
+
"src/autonomous/state-machine.ts"() {
|
|
4754
|
+
"use strict";
|
|
4755
|
+
init_esm_shims();
|
|
4756
|
+
}
|
|
4757
|
+
});
|
|
4758
|
+
|
|
4759
|
+
// src/autonomous/context-pressure.ts
|
|
4760
|
+
function evaluatePressure(state) {
|
|
4761
|
+
const calls = state.contextPressure?.guideCallCount ?? state.guideCallCount ?? 0;
|
|
4762
|
+
const tickets = state.contextPressure?.ticketsCompleted ?? state.completedTickets?.length ?? 0;
|
|
4763
|
+
const eventsBytes = state.contextPressure?.eventsLogBytes ?? 0;
|
|
4764
|
+
if (calls > 45 || tickets >= 3 || eventsBytes > 5e5) return "critical";
|
|
4765
|
+
if (calls >= 30 || tickets >= 2 || eventsBytes > 2e5) return "high";
|
|
4766
|
+
if (calls >= 15 || tickets >= 1 || eventsBytes > 5e4) return "medium";
|
|
4767
|
+
return "low";
|
|
4768
|
+
}
|
|
4769
|
+
var init_context_pressure = __esm({
|
|
4770
|
+
"src/autonomous/context-pressure.ts"() {
|
|
4771
|
+
"use strict";
|
|
4772
|
+
init_esm_shims();
|
|
4773
|
+
}
|
|
4774
|
+
});
|
|
4775
|
+
|
|
4776
|
+
// src/autonomous/review-depth.ts
|
|
4777
|
+
function assessRisk(diffStats, changedFiles) {
|
|
4778
|
+
let level = "low";
|
|
4779
|
+
if (diffStats) {
|
|
4780
|
+
const total = diffStats.totalLines;
|
|
4781
|
+
if (total > 200) level = "high";
|
|
4782
|
+
else if (total >= 50) level = "medium";
|
|
4783
|
+
}
|
|
4784
|
+
if (changedFiles && level !== "high") {
|
|
4785
|
+
const hasSensitive = changedFiles.some(
|
|
4786
|
+
(f) => SENSITIVE_PATTERNS.some((p) => p.test(f))
|
|
4787
|
+
);
|
|
4788
|
+
if (hasSensitive) {
|
|
4789
|
+
level = level === "low" ? "medium" : "high";
|
|
4790
|
+
}
|
|
4791
|
+
}
|
|
4792
|
+
return level;
|
|
4793
|
+
}
|
|
4794
|
+
function requiredRounds(risk) {
|
|
4795
|
+
switch (risk) {
|
|
4796
|
+
case "low":
|
|
4797
|
+
return 1;
|
|
4798
|
+
case "medium":
|
|
4799
|
+
return 2;
|
|
4800
|
+
case "high":
|
|
4801
|
+
return 3;
|
|
4802
|
+
}
|
|
4803
|
+
}
|
|
4804
|
+
function nextReviewer(previousRounds, backends) {
|
|
4805
|
+
if (backends.length === 0) return "agent";
|
|
4806
|
+
if (backends.length === 1) return backends[0];
|
|
4807
|
+
if (previousRounds.length === 0) return backends[0];
|
|
4808
|
+
const lastReviewer = previousRounds[previousRounds.length - 1].reviewer;
|
|
4809
|
+
const lastIndex = backends.indexOf(lastReviewer);
|
|
4810
|
+
if (lastIndex === -1) return backends[0];
|
|
4811
|
+
return backends[(lastIndex + 1) % backends.length];
|
|
4812
|
+
}
|
|
4813
|
+
var SENSITIVE_PATTERNS;
|
|
4814
|
+
var init_review_depth = __esm({
|
|
4815
|
+
"src/autonomous/review-depth.ts"() {
|
|
4816
|
+
"use strict";
|
|
4817
|
+
init_esm_shims();
|
|
4818
|
+
SENSITIVE_PATTERNS = [
|
|
4819
|
+
/\bauth\b/i,
|
|
4820
|
+
/\bsecurity\b/i,
|
|
4821
|
+
/\bmigration/i,
|
|
4822
|
+
/\bconfig\b/i,
|
|
4823
|
+
/\bmiddleware\b/i,
|
|
4824
|
+
/\.env/i
|
|
4825
|
+
];
|
|
4826
|
+
}
|
|
4827
|
+
});
|
|
4828
|
+
|
|
4829
|
+
// src/autonomous/git-inspector.ts
|
|
4830
|
+
import { execFile } from "child_process";
|
|
4831
|
+
async function git(cwd, args, parse) {
|
|
4832
|
+
return new Promise((resolve9) => {
|
|
4833
|
+
execFile("git", args, { cwd, timeout: GIT_TIMEOUT, maxBuffer: 10 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
4834
|
+
if (err) {
|
|
4835
|
+
const message = stderr?.trim() || err.message || "unknown git error";
|
|
4836
|
+
resolve9({ ok: false, reason: "git_error", message });
|
|
4837
|
+
return;
|
|
4838
|
+
}
|
|
4839
|
+
try {
|
|
4840
|
+
resolve9({ ok: true, data: parse(stdout) });
|
|
4841
|
+
} catch (parseErr) {
|
|
4842
|
+
resolve9({ ok: false, reason: "parse_error", message: parseErr.message });
|
|
4843
|
+
}
|
|
4844
|
+
});
|
|
4845
|
+
});
|
|
4846
|
+
}
|
|
4847
|
+
async function gitStatus(cwd) {
|
|
4848
|
+
return git(
|
|
4849
|
+
cwd,
|
|
4850
|
+
["status", "--porcelain"],
|
|
4851
|
+
(out) => out.split("\n").filter((l) => l.length > 0)
|
|
4852
|
+
);
|
|
4853
|
+
}
|
|
4854
|
+
async function gitHead(cwd) {
|
|
4855
|
+
const hashResult = await git(cwd, ["rev-parse", "HEAD"], (out) => out.trim());
|
|
4856
|
+
if (!hashResult.ok) return hashResult;
|
|
4857
|
+
const branchResult = await gitBranch(cwd);
|
|
4858
|
+
return {
|
|
4859
|
+
ok: true,
|
|
4860
|
+
data: {
|
|
4861
|
+
hash: hashResult.data,
|
|
4862
|
+
branch: branchResult.ok ? branchResult.data : null
|
|
4863
|
+
}
|
|
4864
|
+
};
|
|
4865
|
+
}
|
|
4866
|
+
async function gitBranch(cwd) {
|
|
4867
|
+
return git(cwd, ["symbolic-ref", "--short", "HEAD"], (out) => out.trim());
|
|
4868
|
+
}
|
|
4869
|
+
async function gitMergeBase(cwd, base) {
|
|
4870
|
+
return git(cwd, ["merge-base", "HEAD", base], (out) => out.trim());
|
|
4871
|
+
}
|
|
4872
|
+
async function gitDiffStat(cwd, base) {
|
|
4873
|
+
return git(cwd, ["diff", "--numstat", base], parseDiffNumstat);
|
|
4874
|
+
}
|
|
4875
|
+
async function gitDiffNames(cwd, base) {
|
|
4876
|
+
return git(
|
|
4877
|
+
cwd,
|
|
4878
|
+
["diff", "--name-only", base],
|
|
4879
|
+
(out) => out.split("\n").filter((l) => l.length > 0)
|
|
4880
|
+
);
|
|
4881
|
+
}
|
|
4882
|
+
async function gitBlobHash(cwd, file) {
|
|
4883
|
+
return git(cwd, ["hash-object", file], (out) => out.trim());
|
|
4884
|
+
}
|
|
4885
|
+
async function gitDiffCachedNames(cwd) {
|
|
4886
|
+
return git(
|
|
4887
|
+
cwd,
|
|
4888
|
+
["diff", "--cached", "--name-only"],
|
|
4889
|
+
(out) => out.split("\n").filter((l) => l.length > 0)
|
|
4890
|
+
);
|
|
4891
|
+
}
|
|
4892
|
+
function parseDiffNumstat(out) {
|
|
4893
|
+
const lines = out.split("\n").filter((l) => l.length > 0);
|
|
4894
|
+
let insertions = 0;
|
|
4895
|
+
let deletions = 0;
|
|
4896
|
+
let filesChanged = 0;
|
|
4897
|
+
for (const line of lines) {
|
|
4898
|
+
const parts = line.split(" ");
|
|
4899
|
+
if (parts.length < 3) continue;
|
|
4900
|
+
const added = parseInt(parts[0], 10);
|
|
4901
|
+
const removed = parseInt(parts[1], 10);
|
|
4902
|
+
if (!Number.isNaN(added)) insertions += added;
|
|
4903
|
+
if (!Number.isNaN(removed)) deletions += removed;
|
|
4904
|
+
filesChanged++;
|
|
4905
|
+
}
|
|
4906
|
+
return { filesChanged, insertions, deletions, totalLines: insertions + deletions };
|
|
4907
|
+
}
|
|
4908
|
+
var GIT_TIMEOUT;
|
|
4909
|
+
var init_git_inspector = __esm({
|
|
4910
|
+
"src/autonomous/git-inspector.ts"() {
|
|
4911
|
+
"use strict";
|
|
4912
|
+
init_esm_shims();
|
|
4913
|
+
GIT_TIMEOUT = 1e4;
|
|
4914
|
+
}
|
|
4915
|
+
});
|
|
4916
|
+
|
|
4917
|
+
// src/autonomous/guide.ts
|
|
4918
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync7 } from "fs";
|
|
4919
|
+
import { join as join7 } from "path";
|
|
4920
|
+
async function handleAutonomousGuide(root, args) {
|
|
4921
|
+
const wsId = deriveWorkspaceId(root);
|
|
4922
|
+
const prev = workspaceLocks.get(wsId) ?? Promise.resolve();
|
|
4923
|
+
const current = prev.then(async () => {
|
|
4924
|
+
return withSessionLock(root, () => handleGuideInner(root, args));
|
|
4925
|
+
});
|
|
4926
|
+
workspaceLocks.set(wsId, current.then(() => {
|
|
4927
|
+
}, () => {
|
|
4928
|
+
}));
|
|
4929
|
+
try {
|
|
4930
|
+
return await current;
|
|
4931
|
+
} catch (err) {
|
|
4932
|
+
return guideError(err);
|
|
4933
|
+
} finally {
|
|
4934
|
+
const stored = workspaceLocks.get(wsId);
|
|
4935
|
+
if (stored) {
|
|
4936
|
+
stored.then(() => {
|
|
4937
|
+
if (workspaceLocks.get(wsId) === stored) {
|
|
4938
|
+
workspaceLocks.delete(wsId);
|
|
4939
|
+
}
|
|
4940
|
+
}, () => {
|
|
4941
|
+
if (workspaceLocks.get(wsId) === stored) {
|
|
4942
|
+
workspaceLocks.delete(wsId);
|
|
4943
|
+
}
|
|
4944
|
+
});
|
|
4945
|
+
}
|
|
4946
|
+
}
|
|
4947
|
+
}
|
|
4948
|
+
async function handleGuideInner(root, args) {
|
|
4949
|
+
switch (args.action) {
|
|
4950
|
+
case "start":
|
|
4951
|
+
return handleStart(root, args);
|
|
4952
|
+
case "report":
|
|
4953
|
+
return handleReport(root, args);
|
|
4954
|
+
case "resume":
|
|
4955
|
+
return handleResume(root, args);
|
|
4956
|
+
case "pre_compact":
|
|
4957
|
+
return handlePreCompact(root, args);
|
|
4958
|
+
case "cancel":
|
|
4959
|
+
return handleCancel(root, args);
|
|
4960
|
+
default:
|
|
4961
|
+
return guideError(new Error(`Unknown action: ${args.action}`));
|
|
4962
|
+
}
|
|
4963
|
+
}
|
|
4964
|
+
async function handleStart(root, args) {
|
|
4965
|
+
const existing = findActiveSessionFull(root);
|
|
4966
|
+
if (existing && !isLeaseExpired(existing.state)) {
|
|
4967
|
+
return guideError(new Error(
|
|
4968
|
+
`Active session ${existing.state.sessionId} already exists for this workspace. Use action: "resume" to continue or "cancel" to end it.`
|
|
4969
|
+
));
|
|
4970
|
+
}
|
|
4971
|
+
const staleSessions = findStaleSessions(root);
|
|
4972
|
+
for (const stale of staleSessions) {
|
|
4973
|
+
writeSessionSync(stale.dir, { ...stale.state, status: "superseded" });
|
|
4974
|
+
}
|
|
4975
|
+
const wsId = deriveWorkspaceId(root);
|
|
4976
|
+
const recipe = "coding";
|
|
4977
|
+
const session = createSession(root, recipe, wsId);
|
|
4978
|
+
const dir = sessionDir(root, session.sessionId);
|
|
4979
|
+
try {
|
|
4980
|
+
const headResult = await gitHead(root);
|
|
4981
|
+
if (!headResult.ok) {
|
|
4982
|
+
deleteSession(root, session.sessionId);
|
|
4983
|
+
return guideError(new Error("This directory is not a git repository or git is not available. Autonomous mode requires git."));
|
|
4984
|
+
}
|
|
4985
|
+
const stagedResult = await gitDiffCachedNames(root);
|
|
4986
|
+
if (stagedResult.ok && stagedResult.data.length > 0) {
|
|
4987
|
+
deleteSession(root, session.sessionId);
|
|
4988
|
+
return guideError(new Error(
|
|
4989
|
+
`Cannot start: ${stagedResult.data.length} staged file(s). Unstage with \`git restore --staged .\` or commit them first, then call start again.
|
|
4990
|
+
|
|
4991
|
+
Staged: ${stagedResult.data.join(", ")}`
|
|
4992
|
+
));
|
|
4993
|
+
}
|
|
4994
|
+
const statusResult = await gitStatus(root);
|
|
4995
|
+
let mergeBaseResult = await gitMergeBase(root, "main");
|
|
4996
|
+
if (!mergeBaseResult.ok) mergeBaseResult = await gitMergeBase(root, "master");
|
|
4997
|
+
const porcelainLines = statusResult.ok ? statusResult.data : [];
|
|
4998
|
+
const dirtyTracked = {};
|
|
4999
|
+
const untrackedPaths = [];
|
|
5000
|
+
for (const line of porcelainLines) {
|
|
5001
|
+
if (line.startsWith("??")) {
|
|
5002
|
+
untrackedPaths.push(line.slice(3).trim());
|
|
5003
|
+
} else if (line.length > 3) {
|
|
5004
|
+
const filePath = line.slice(3).trim();
|
|
5005
|
+
const hashResult = await gitBlobHash(root, filePath);
|
|
5006
|
+
dirtyTracked[filePath] = { blobHash: hashResult.ok ? hashResult.data : "" };
|
|
5007
|
+
}
|
|
5008
|
+
}
|
|
5009
|
+
if (Object.keys(dirtyTracked).length > 0) {
|
|
5010
|
+
deleteSession(root, session.sessionId);
|
|
5011
|
+
const dirtyFiles = Object.keys(dirtyTracked).join(", ");
|
|
5012
|
+
return guideError(new Error(
|
|
5013
|
+
`Cannot start: ${Object.keys(dirtyTracked).length} dirty tracked file(s): ${dirtyFiles}. Create a feature branch or stash changes first, then call start again.`
|
|
5014
|
+
));
|
|
5015
|
+
}
|
|
5016
|
+
let updated = {
|
|
5017
|
+
...session,
|
|
5018
|
+
state: "PICK_TICKET",
|
|
5019
|
+
previousState: "INIT",
|
|
5020
|
+
git: {
|
|
5021
|
+
branch: headResult.data.branch,
|
|
5022
|
+
initHead: headResult.data.hash,
|
|
5023
|
+
mergeBase: mergeBaseResult.ok ? mergeBaseResult.data : null,
|
|
5024
|
+
expectedHead: headResult.data.hash,
|
|
5025
|
+
baseline: {
|
|
5026
|
+
porcelain: porcelainLines,
|
|
5027
|
+
dirtyTrackedFiles: dirtyTracked,
|
|
5028
|
+
untrackedPaths
|
|
5029
|
+
}
|
|
5030
|
+
}
|
|
5031
|
+
};
|
|
5032
|
+
const { state: projectState, warnings } = await loadProject(root);
|
|
5033
|
+
const handoversDir = join7(root, ".story", "handovers");
|
|
5034
|
+
const ctx = { state: projectState, warnings, root, handoversDir, format: "md" };
|
|
5035
|
+
let handoverText = "";
|
|
5036
|
+
try {
|
|
5037
|
+
const handoverResult = await handleHandoverLatest(ctx, 3);
|
|
5038
|
+
handoverText = handoverResult.output;
|
|
5039
|
+
} catch {
|
|
5040
|
+
}
|
|
5041
|
+
let recapText = "";
|
|
5042
|
+
try {
|
|
5043
|
+
const snapshotInfo = await loadLatestSnapshot(root);
|
|
5044
|
+
const recap = buildRecap(projectState, snapshotInfo);
|
|
5045
|
+
if (recap.changes) {
|
|
5046
|
+
recapText = "Changes since last snapshot available.";
|
|
5047
|
+
}
|
|
5048
|
+
} catch {
|
|
5049
|
+
}
|
|
5050
|
+
const rulesText = readFileSafe(join7(root, "RULES.md"));
|
|
5051
|
+
const strategiesText = readFileSafe(join7(root, "WORK_STRATEGIES.md"));
|
|
5052
|
+
const digestParts = [
|
|
5053
|
+
handoverText ? `## Recent Handovers
|
|
5054
|
+
|
|
5055
|
+
${handoverText}` : "",
|
|
5056
|
+
recapText ? `## Recap
|
|
5057
|
+
|
|
5058
|
+
${recapText}` : "",
|
|
5059
|
+
rulesText ? `## Development Rules
|
|
5060
|
+
|
|
5061
|
+
${rulesText}` : "",
|
|
5062
|
+
strategiesText ? `## Work Strategies
|
|
5063
|
+
|
|
5064
|
+
${strategiesText}` : ""
|
|
5065
|
+
].filter(Boolean);
|
|
5066
|
+
const digest = digestParts.join("\n\n---\n\n");
|
|
5067
|
+
try {
|
|
5068
|
+
writeFileSync2(join7(dir, "context-digest.md"), digest, "utf-8");
|
|
5069
|
+
} catch {
|
|
5070
|
+
}
|
|
5071
|
+
const nextResult = nextTickets(projectState, 5);
|
|
5072
|
+
let candidatesText = "";
|
|
5073
|
+
if (nextResult.kind === "found") {
|
|
5074
|
+
candidatesText = nextResult.candidates.map(
|
|
5075
|
+
(c, i) => `${i + 1}. **${c.ticket.id}: ${c.ticket.title}** (${c.ticket.type}, phase: ${c.ticket.phase ?? "unphased"})${c.unblockImpact.wouldUnblock.length > 0 ? ` \u2014 unblocks ${c.unblockImpact.wouldUnblock.map((t) => t.id).join(", ")}` : ""}`
|
|
5076
|
+
).join("\n");
|
|
5077
|
+
} else if (nextResult.kind === "all_complete") {
|
|
5078
|
+
candidatesText = "All tickets are complete. No work to do.";
|
|
5079
|
+
} else if (nextResult.kind === "all_blocked") {
|
|
5080
|
+
candidatesText = "All remaining tickets are blocked.";
|
|
5081
|
+
} else {
|
|
5082
|
+
candidatesText = "No tickets found.";
|
|
5083
|
+
}
|
|
5084
|
+
const recResult = recommend(projectState, 5);
|
|
5085
|
+
let recsText = "";
|
|
5086
|
+
if (recResult.recommendations.length > 0) {
|
|
5087
|
+
const ticketRecs = recResult.recommendations.filter((r) => r.kind === "ticket");
|
|
5088
|
+
if (ticketRecs.length > 0) {
|
|
5089
|
+
recsText = "\n\n**Recommended:**\n" + ticketRecs.map(
|
|
5090
|
+
(r) => `- ${r.id}: ${r.title} (${r.reason})`
|
|
5091
|
+
).join("\n");
|
|
5092
|
+
}
|
|
5093
|
+
}
|
|
5094
|
+
updated = refreshLease(updated);
|
|
5095
|
+
const pressure = evaluatePressure(updated);
|
|
5096
|
+
updated = { ...updated, contextPressure: { ...updated.contextPressure, level: pressure } };
|
|
5097
|
+
const written = writeSessionSync(dir, updated);
|
|
5098
|
+
appendEvent(dir, {
|
|
5099
|
+
rev: written.revision,
|
|
5100
|
+
type: "start",
|
|
5101
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5102
|
+
data: { recipe, branch: written.git.branch, head: written.git.initHead }
|
|
5103
|
+
});
|
|
5104
|
+
const instruction = [
|
|
5105
|
+
"# Autonomous Session Started",
|
|
5106
|
+
"",
|
|
5107
|
+
"I've loaded your project context and found ticket candidates.",
|
|
5108
|
+
"",
|
|
5109
|
+
"## Ticket Candidates",
|
|
5110
|
+
"",
|
|
5111
|
+
candidatesText,
|
|
5112
|
+
recsText,
|
|
5113
|
+
"",
|
|
5114
|
+
"Pick a ticket by calling `claudestory_autonomous_guide` with:",
|
|
5115
|
+
"```json",
|
|
5116
|
+
`{ "sessionId": "${updated.sessionId}", "action": "report", "report": { "completedAction": "ticket_picked", "ticketId": "T-XXX" } }`,
|
|
5117
|
+
"```",
|
|
5118
|
+
"",
|
|
5119
|
+
"Replace T-XXX with the ticket ID you want to work on."
|
|
5120
|
+
].join("\n");
|
|
5121
|
+
return guideResult(updated, "PICK_TICKET", {
|
|
5122
|
+
instruction,
|
|
5123
|
+
reminders: [
|
|
5124
|
+
"Do NOT use Claude Code's plan mode \u2014 write plans as markdown files.",
|
|
5125
|
+
"Do NOT ask the user for confirmation or approval.",
|
|
5126
|
+
"Call autonomous_guide after completing each step."
|
|
5127
|
+
],
|
|
5128
|
+
transitionedFrom: "INIT"
|
|
5129
|
+
});
|
|
5130
|
+
} catch (err) {
|
|
5131
|
+
deleteSession(root, session.sessionId);
|
|
5132
|
+
throw err;
|
|
5133
|
+
}
|
|
5134
|
+
}
|
|
5135
|
+
async function handleReport(root, args) {
|
|
5136
|
+
if (!args.sessionId) return guideError(new Error("sessionId is required for report action"));
|
|
5137
|
+
if (!args.report) return guideError(new Error("report field is required for report action"));
|
|
5138
|
+
const info = findSessionById(root, args.sessionId);
|
|
5139
|
+
if (!info) return guideError(new Error(`Session ${args.sessionId} not found`));
|
|
5140
|
+
let state = refreshLease(info.state);
|
|
5141
|
+
const currentState = state.state;
|
|
5142
|
+
const report = args.report;
|
|
5143
|
+
switch (currentState) {
|
|
5144
|
+
case "PICK_TICKET":
|
|
5145
|
+
return handleReportPickTicket(root, info.dir, state, report);
|
|
5146
|
+
case "PLAN":
|
|
5147
|
+
return handleReportPlan(root, info.dir, state, report);
|
|
5148
|
+
case "PLAN_REVIEW":
|
|
5149
|
+
return handleReportPlanReview(root, info.dir, state, report);
|
|
5150
|
+
case "IMPLEMENT":
|
|
5151
|
+
return handleReportImplement(root, info.dir, state, report);
|
|
5152
|
+
case "CODE_REVIEW":
|
|
5153
|
+
return handleReportCodeReview(root, info.dir, state, report);
|
|
5154
|
+
case "FINALIZE":
|
|
5155
|
+
return handleReportFinalize(root, info.dir, state, report);
|
|
5156
|
+
case "COMPLETE":
|
|
5157
|
+
return handleReportComplete(root, info.dir, state, report);
|
|
5158
|
+
case "HANDOVER":
|
|
5159
|
+
return handleReportHandover(root, info.dir, state, report);
|
|
5160
|
+
default:
|
|
5161
|
+
return guideError(new Error(`Cannot report at state ${currentState}`));
|
|
5162
|
+
}
|
|
5163
|
+
}
|
|
5164
|
+
async function handleReportPickTicket(root, dir, state, report) {
|
|
5165
|
+
const ticketId = report.ticketId;
|
|
5166
|
+
if (!ticketId) return guideError(new Error("report.ticketId is required when picking a ticket"));
|
|
5167
|
+
const { state: projectState } = await loadProject(root);
|
|
5168
|
+
const ticket = projectState.ticketByID(ticketId);
|
|
5169
|
+
if (!ticket) return guideError(new Error(`Ticket ${ticketId} not found`));
|
|
5170
|
+
if (projectState.isBlocked(ticket)) return guideError(new Error(`Ticket ${ticketId} is blocked`));
|
|
5171
|
+
const written = writeSessionSync(dir, {
|
|
5172
|
+
...state,
|
|
5173
|
+
state: "PLAN",
|
|
5174
|
+
previousState: "PICK_TICKET",
|
|
5175
|
+
ticket: { id: ticket.id, title: ticket.title, claimed: true }
|
|
5176
|
+
});
|
|
5177
|
+
appendEvent(dir, {
|
|
5178
|
+
rev: written.revision,
|
|
5179
|
+
type: "ticket_picked",
|
|
5180
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5181
|
+
data: { ticketId: ticket.id, title: ticket.title }
|
|
5182
|
+
});
|
|
5183
|
+
return guideResult(written, "PLAN", {
|
|
5184
|
+
instruction: [
|
|
5185
|
+
`# Plan for ${ticket.id}: ${ticket.title}`,
|
|
5186
|
+
"",
|
|
5187
|
+
ticket.description ? `## Ticket Description
|
|
5188
|
+
|
|
5189
|
+
${ticket.description}` : "",
|
|
5190
|
+
"",
|
|
5191
|
+
`Write an implementation plan for this ticket. Save it to \`.story/sessions/${state.sessionId}/plan.md\`.`,
|
|
5192
|
+
"",
|
|
5193
|
+
"When done, call `claudestory_autonomous_guide` with:",
|
|
5194
|
+
"```json",
|
|
5195
|
+
`{ "sessionId": "${state.sessionId}", "action": "report", "report": { "completedAction": "plan_written" } }`,
|
|
5196
|
+
"```"
|
|
5197
|
+
].join("\n"),
|
|
5198
|
+
reminders: [
|
|
5199
|
+
"Write the plan as a markdown file \u2014 do NOT use Claude Code's plan mode.",
|
|
5200
|
+
"Do NOT ask the user for approval."
|
|
5201
|
+
],
|
|
5202
|
+
transitionedFrom: "PICK_TICKET"
|
|
5203
|
+
});
|
|
5204
|
+
}
|
|
5205
|
+
async function handleReportPlan(root, dir, state, report) {
|
|
5206
|
+
const planPath = join7(dir, "plan.md");
|
|
5207
|
+
if (!existsSync7(planPath)) {
|
|
5208
|
+
return guideResult(state, "PLAN", {
|
|
5209
|
+
instruction: `Plan file not found at ${planPath}. Write your plan there and call me again.`,
|
|
5210
|
+
reminders: ["Save plan to .story/sessions/<id>/plan.md"]
|
|
5211
|
+
});
|
|
5212
|
+
}
|
|
5213
|
+
const planContent = readFileSafe(planPath);
|
|
5214
|
+
if (!planContent || planContent.trim().length === 0) {
|
|
5215
|
+
return guideResult(state, "PLAN", {
|
|
5216
|
+
instruction: "Plan file is empty. Write your implementation plan and call me again.",
|
|
5217
|
+
reminders: []
|
|
5218
|
+
});
|
|
5219
|
+
}
|
|
5220
|
+
const risk = assessRisk(void 0, void 0);
|
|
5221
|
+
const written = writeSessionSync(dir, {
|
|
5222
|
+
...state,
|
|
5223
|
+
state: "PLAN_REVIEW",
|
|
5224
|
+
previousState: "PLAN",
|
|
5225
|
+
ticket: state.ticket ? { ...state.ticket, risk } : state.ticket
|
|
5226
|
+
});
|
|
5227
|
+
appendEvent(dir, {
|
|
5228
|
+
rev: written.revision,
|
|
5229
|
+
type: "plan_written",
|
|
5230
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5231
|
+
data: { planLength: planContent.length, risk }
|
|
5232
|
+
});
|
|
5233
|
+
const backends = state.config.reviewBackends;
|
|
5234
|
+
const reviewer = nextReviewer([], backends);
|
|
5235
|
+
const rounds = requiredRounds(risk);
|
|
5236
|
+
return guideResult(written, "PLAN_REVIEW", {
|
|
5237
|
+
instruction: [
|
|
5238
|
+
`# Plan Review \u2014 Round 1 of ${rounds} minimum`,
|
|
5239
|
+
"",
|
|
5240
|
+
`Run a plan review using **${reviewer}**.`,
|
|
5241
|
+
"",
|
|
5242
|
+
reviewer === "codex" ? `Call \`review_plan\` MCP tool with the plan content.` : `Launch a code review agent to review the plan.`,
|
|
5243
|
+
"",
|
|
5244
|
+
"When done, call `claudestory_autonomous_guide` with:",
|
|
5245
|
+
"```json",
|
|
5246
|
+
`{ "sessionId": "${state.sessionId}", "action": "report", "report": { "completedAction": "plan_review_round", "verdict": "<approve|revise|reject>", "findings": [...] } }`,
|
|
5247
|
+
"```"
|
|
5248
|
+
].join("\n"),
|
|
5249
|
+
reminders: ["Report the exact verdict and findings from the reviewer."],
|
|
5250
|
+
transitionedFrom: "PLAN"
|
|
5251
|
+
});
|
|
5252
|
+
}
|
|
5253
|
+
async function handleReportPlanReview(root, dir, state, report) {
|
|
5254
|
+
const verdict = report.verdict;
|
|
5255
|
+
if (!verdict || !["approve", "revise", "request_changes", "reject"].includes(verdict)) {
|
|
5256
|
+
return guideResult(state, "PLAN_REVIEW", {
|
|
5257
|
+
instruction: 'Invalid verdict. Re-submit with verdict: "approve", "revise", "request_changes", or "reject".',
|
|
5258
|
+
reminders: []
|
|
5259
|
+
});
|
|
5260
|
+
}
|
|
5261
|
+
const planReviews = [...state.reviews.plan];
|
|
5262
|
+
const roundNum = planReviews.length + 1;
|
|
5263
|
+
const findings = report.findings ?? [];
|
|
5264
|
+
const backends = state.config.reviewBackends;
|
|
5265
|
+
const reviewerBackend = nextReviewer(planReviews, backends);
|
|
5266
|
+
planReviews.push({
|
|
5267
|
+
round: roundNum,
|
|
5268
|
+
reviewer: reviewerBackend,
|
|
5269
|
+
verdict,
|
|
5270
|
+
findingCount: findings.length,
|
|
5271
|
+
criticalCount: findings.filter((f) => f.severity === "critical").length,
|
|
5272
|
+
majorCount: findings.filter((f) => f.severity === "major").length,
|
|
5273
|
+
suggestionCount: findings.filter((f) => f.severity === "suggestion").length,
|
|
5274
|
+
codexSessionId: report.reviewerSessionId,
|
|
5275
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
5276
|
+
});
|
|
5277
|
+
const risk = state.ticket?.risk ?? "low";
|
|
5278
|
+
const minRounds = requiredRounds(risk);
|
|
5279
|
+
const hasCriticalOrMajor = findings.some(
|
|
5280
|
+
(f) => f.severity === "critical" || f.severity === "major"
|
|
5281
|
+
);
|
|
5282
|
+
let nextState;
|
|
5283
|
+
if (verdict === "reject") {
|
|
5284
|
+
nextState = "PLAN";
|
|
5285
|
+
} else if (verdict === "approve" || !hasCriticalOrMajor && roundNum >= minRounds) {
|
|
5286
|
+
nextState = "IMPLEMENT";
|
|
5287
|
+
} else if (roundNum >= 5) {
|
|
5288
|
+
nextState = "IMPLEMENT";
|
|
5289
|
+
} else {
|
|
5290
|
+
nextState = "PLAN_REVIEW";
|
|
5291
|
+
}
|
|
5292
|
+
const written = writeSessionSync(dir, {
|
|
5293
|
+
...state,
|
|
5294
|
+
state: nextState,
|
|
5295
|
+
previousState: "PLAN_REVIEW",
|
|
5296
|
+
reviews: { ...state.reviews, plan: planReviews }
|
|
5297
|
+
});
|
|
5298
|
+
appendEvent(dir, {
|
|
5299
|
+
rev: written.revision,
|
|
5300
|
+
type: "plan_review",
|
|
5301
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5302
|
+
data: { round: roundNum, verdict, findingCount: findings.length }
|
|
5303
|
+
});
|
|
5304
|
+
if (nextState === "PLAN") {
|
|
5305
|
+
return guideResult(written, "PLAN", {
|
|
5306
|
+
instruction: 'Plan was rejected. Revise and rewrite the plan, then call me with completedAction: "plan_written".',
|
|
5307
|
+
reminders: [],
|
|
5308
|
+
transitionedFrom: "PLAN_REVIEW"
|
|
5309
|
+
});
|
|
5310
|
+
}
|
|
5311
|
+
if (nextState === "IMPLEMENT") {
|
|
5312
|
+
return guideResult(written, "IMPLEMENT", {
|
|
5313
|
+
instruction: [
|
|
5314
|
+
"# Implement",
|
|
5315
|
+
"",
|
|
5316
|
+
"Plan review passed. Implement the plan now.",
|
|
5317
|
+
"",
|
|
5318
|
+
"When done, call `claudestory_autonomous_guide` with:",
|
|
5319
|
+
"```json",
|
|
5320
|
+
`{ "sessionId": "${state.sessionId}", "action": "report", "report": { "completedAction": "implementation_done" } }`,
|
|
5321
|
+
"```"
|
|
5322
|
+
].join("\n"),
|
|
5323
|
+
reminders: ["Call autonomous_guide when implementation is complete."],
|
|
5324
|
+
transitionedFrom: "PLAN_REVIEW"
|
|
5325
|
+
});
|
|
5326
|
+
}
|
|
5327
|
+
const reviewer = nextReviewer(planReviews, backends);
|
|
5328
|
+
return guideResult(written, "PLAN_REVIEW", {
|
|
5329
|
+
instruction: [
|
|
5330
|
+
`# Plan Review \u2014 Round ${roundNum + 1}`,
|
|
5331
|
+
"",
|
|
5332
|
+
hasCriticalOrMajor ? `Round ${roundNum} found ${findings.filter((f) => f.severity === "critical" || f.severity === "major").length} critical/major finding(s). Address them, then re-review with **${reviewer}**.` : `Round ${roundNum} complete. Run round ${roundNum + 1} with **${reviewer}**.`,
|
|
5333
|
+
"",
|
|
5334
|
+
"Report verdict and findings as before."
|
|
5335
|
+
].join("\n"),
|
|
5336
|
+
reminders: ["Address findings before re-reviewing."]
|
|
5337
|
+
});
|
|
5338
|
+
}
|
|
5339
|
+
async function handleReportImplement(root, dir, state, report) {
|
|
5340
|
+
let realizedRisk = state.ticket?.risk ?? "low";
|
|
5341
|
+
const mergeBase = state.git.mergeBase;
|
|
5342
|
+
if (mergeBase) {
|
|
5343
|
+
const diffResult = await gitDiffStat(root, mergeBase);
|
|
5344
|
+
const namesResult = await gitDiffNames(root, mergeBase);
|
|
5345
|
+
if (diffResult.ok) {
|
|
5346
|
+
realizedRisk = assessRisk(diffResult.data, namesResult.ok ? namesResult.data : void 0);
|
|
5347
|
+
}
|
|
5348
|
+
}
|
|
5349
|
+
const backends = state.config.reviewBackends;
|
|
5350
|
+
const codeReviews = state.reviews.code;
|
|
5351
|
+
const reviewer = nextReviewer(codeReviews, backends);
|
|
5352
|
+
const rounds = requiredRounds(realizedRisk);
|
|
5353
|
+
const written = writeSessionSync(dir, {
|
|
5354
|
+
...state,
|
|
5355
|
+
state: "CODE_REVIEW",
|
|
5356
|
+
previousState: "IMPLEMENT",
|
|
5357
|
+
ticket: state.ticket ? { ...state.ticket, realizedRisk } : state.ticket
|
|
5358
|
+
});
|
|
5359
|
+
appendEvent(dir, {
|
|
5360
|
+
rev: written.revision,
|
|
5361
|
+
type: "implementation_done",
|
|
5362
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5363
|
+
data: { realizedRisk }
|
|
5364
|
+
});
|
|
5365
|
+
return guideResult(written, "CODE_REVIEW", {
|
|
5366
|
+
instruction: [
|
|
5367
|
+
`# Code Review \u2014 Round 1 of ${rounds} minimum`,
|
|
5368
|
+
"",
|
|
5369
|
+
`Realized risk: **${realizedRisk}**${realizedRisk !== state.ticket?.risk ? ` (was ${state.ticket?.risk})` : ""}.`,
|
|
5370
|
+
"",
|
|
5371
|
+
`Run a code review using **${reviewer}**. Capture the git diff and pass it to the reviewer.`,
|
|
5372
|
+
"",
|
|
5373
|
+
"When done, report verdict and findings."
|
|
5374
|
+
].join("\n"),
|
|
5375
|
+
reminders: ["Capture diff with `git diff` and pass to reviewer."],
|
|
5376
|
+
transitionedFrom: "IMPLEMENT"
|
|
5377
|
+
});
|
|
5378
|
+
}
|
|
5379
|
+
async function handleReportCodeReview(root, dir, state, report) {
|
|
5380
|
+
const verdict = report.verdict;
|
|
5381
|
+
if (!verdict || !["approve", "revise", "request_changes", "reject"].includes(verdict)) {
|
|
5382
|
+
return guideResult(state, "CODE_REVIEW", {
|
|
5383
|
+
instruction: 'Invalid verdict. Re-submit with verdict: "approve", "revise", "request_changes", or "reject".',
|
|
5384
|
+
reminders: []
|
|
5385
|
+
});
|
|
5386
|
+
}
|
|
5387
|
+
const codeReviews = [...state.reviews.code];
|
|
5388
|
+
const roundNum = codeReviews.length + 1;
|
|
5389
|
+
const findings = report.findings ?? [];
|
|
5390
|
+
const backends = state.config.reviewBackends;
|
|
5391
|
+
const reviewerBackend = nextReviewer(codeReviews, backends);
|
|
5392
|
+
codeReviews.push({
|
|
5393
|
+
round: roundNum,
|
|
5394
|
+
reviewer: reviewerBackend,
|
|
5395
|
+
verdict,
|
|
5396
|
+
findingCount: findings.length,
|
|
5397
|
+
criticalCount: findings.filter((f) => f.severity === "critical").length,
|
|
5398
|
+
majorCount: findings.filter((f) => f.severity === "major").length,
|
|
5399
|
+
suggestionCount: findings.filter((f) => f.severity === "suggestion").length,
|
|
5400
|
+
codexSessionId: report.reviewerSessionId,
|
|
5401
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
5402
|
+
});
|
|
5403
|
+
const risk = state.ticket?.realizedRisk ?? state.ticket?.risk ?? "low";
|
|
5404
|
+
const minRounds = requiredRounds(risk);
|
|
5405
|
+
const hasCriticalOrMajor = findings.some(
|
|
5406
|
+
(f) => f.severity === "critical" || f.severity === "major"
|
|
5407
|
+
);
|
|
5408
|
+
const planRedirect = findings.some((f) => f.recommendedNextState === "PLAN");
|
|
5409
|
+
let nextState;
|
|
5410
|
+
if (verdict === "reject" && planRedirect) {
|
|
5411
|
+
nextState = "PLAN";
|
|
5412
|
+
} else if (verdict === "reject") {
|
|
5413
|
+
nextState = "IMPLEMENT";
|
|
5414
|
+
} else if (verdict === "approve" || !hasCriticalOrMajor && roundNum >= minRounds) {
|
|
5415
|
+
nextState = "FINALIZE";
|
|
5416
|
+
} else if (roundNum >= 5) {
|
|
5417
|
+
nextState = "FINALIZE";
|
|
5418
|
+
} else {
|
|
5419
|
+
nextState = "CODE_REVIEW";
|
|
5420
|
+
}
|
|
5421
|
+
const written = writeSessionSync(dir, {
|
|
5422
|
+
...state,
|
|
5423
|
+
state: nextState,
|
|
5424
|
+
previousState: "CODE_REVIEW",
|
|
5425
|
+
reviews: { ...state.reviews, code: codeReviews }
|
|
5426
|
+
});
|
|
5427
|
+
appendEvent(dir, {
|
|
5428
|
+
rev: written.revision,
|
|
5429
|
+
type: "code_review",
|
|
5430
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5431
|
+
data: { round: roundNum, verdict, findingCount: findings.length }
|
|
5432
|
+
});
|
|
5433
|
+
if (nextState === "PLAN") {
|
|
5434
|
+
return guideResult(written, "PLAN", {
|
|
5435
|
+
instruction: 'Code review recommends rethinking the approach. Revise the plan and call me with completedAction: "plan_written".',
|
|
5436
|
+
reminders: [],
|
|
5437
|
+
transitionedFrom: "CODE_REVIEW"
|
|
5438
|
+
});
|
|
5439
|
+
}
|
|
5440
|
+
if (nextState === "IMPLEMENT") {
|
|
5441
|
+
return guideResult(written, "IMPLEMENT", {
|
|
5442
|
+
instruction: 'Code review requested changes. Fix the issues and call me with completedAction: "implementation_done".',
|
|
5443
|
+
reminders: ["Address all critical/major findings before re-submitting."],
|
|
5444
|
+
transitionedFrom: "CODE_REVIEW"
|
|
5445
|
+
});
|
|
5446
|
+
}
|
|
5447
|
+
if (nextState === "FINALIZE") {
|
|
5448
|
+
return guideResult(written, "FINALIZE", {
|
|
5449
|
+
instruction: [
|
|
5450
|
+
"# Finalize",
|
|
5451
|
+
"",
|
|
5452
|
+
"Code review passed. Time to commit.",
|
|
5453
|
+
"",
|
|
5454
|
+
state.ticket ? `1. Update ticket ${state.ticket.id} status to "complete" in .story/` : "",
|
|
5455
|
+
"2. Stage all changed files (code + .story/ changes)",
|
|
5456
|
+
'3. Call me with completedAction: "files_staged"'
|
|
5457
|
+
].filter(Boolean).join("\n"),
|
|
5458
|
+
reminders: ["Stage both code changes and .story/ ticket update in the same commit."],
|
|
5459
|
+
transitionedFrom: "CODE_REVIEW"
|
|
5460
|
+
});
|
|
5461
|
+
}
|
|
5462
|
+
const reviewer = nextReviewer(codeReviews, backends);
|
|
5463
|
+
return guideResult(written, "CODE_REVIEW", {
|
|
5464
|
+
instruction: `Code review round ${roundNum} found issues. Fix them and re-review with **${reviewer}**.`,
|
|
5465
|
+
reminders: []
|
|
5466
|
+
});
|
|
5467
|
+
}
|
|
5468
|
+
async function handleReportFinalize(root, dir, state, report) {
|
|
5469
|
+
const action = report.completedAction;
|
|
5470
|
+
const checkpoint = state.finalizeCheckpoint;
|
|
5471
|
+
if (action === "files_staged" && (!checkpoint || checkpoint === "staged")) {
|
|
5472
|
+
const stagedResult = await gitDiffCachedNames(root);
|
|
5473
|
+
if (!stagedResult.ok || stagedResult.data.length === 0) {
|
|
5474
|
+
return guideResult(state, "FINALIZE", {
|
|
5475
|
+
instruction: 'No files are staged. Stage your changes and call me again with completedAction: "files_staged".',
|
|
5476
|
+
reminders: []
|
|
5477
|
+
});
|
|
5478
|
+
}
|
|
5479
|
+
const written = writeSessionSync(dir, { ...state, finalizeCheckpoint: "staged" });
|
|
5480
|
+
return guideResult(written, "FINALIZE", {
|
|
5481
|
+
instruction: [
|
|
5482
|
+
"Files staged. Now run pre-commit checks.",
|
|
5483
|
+
"",
|
|
5484
|
+
'Run any pre-commit hooks or linting, then call me with completedAction: "precommit_passed".',
|
|
5485
|
+
'If pre-commit fails, fix the issues, re-stage, and call me with completedAction: "files_staged" again.'
|
|
5486
|
+
].join("\n"),
|
|
5487
|
+
reminders: ["Verify staged set is intact after pre-commit hooks."]
|
|
5488
|
+
});
|
|
5489
|
+
}
|
|
5490
|
+
if (action === "precommit_passed") {
|
|
5491
|
+
if (!checkpoint || checkpoint === null) {
|
|
5492
|
+
return guideResult(state, "FINALIZE", {
|
|
5493
|
+
instruction: 'You must stage files first. Call me with completedAction: "files_staged" after staging.',
|
|
5494
|
+
reminders: []
|
|
5495
|
+
});
|
|
5496
|
+
}
|
|
5497
|
+
const stagedResult = await gitDiffCachedNames(root);
|
|
5498
|
+
if (!stagedResult.ok || stagedResult.data.length === 0) {
|
|
5499
|
+
const written2 = writeSessionSync(dir, { ...state, finalizeCheckpoint: null });
|
|
5500
|
+
return guideResult(written2, "FINALIZE", {
|
|
5501
|
+
instruction: 'Pre-commit hooks appear to have cleared the staging area. Re-stage your changes and call me with completedAction: "files_staged".',
|
|
5502
|
+
reminders: []
|
|
5503
|
+
});
|
|
5504
|
+
}
|
|
5505
|
+
const written = writeSessionSync(dir, { ...state, finalizeCheckpoint: "precommit_passed" });
|
|
5506
|
+
return guideResult(written, "FINALIZE", {
|
|
5507
|
+
instruction: [
|
|
5508
|
+
"Pre-commit passed. Now commit.",
|
|
5509
|
+
"",
|
|
5510
|
+
state.ticket ? `Commit with message: "feat: <description> (${state.ticket.id})"` : "Commit with a descriptive message.",
|
|
5511
|
+
"",
|
|
5512
|
+
'Call me with completedAction: "commit_done" and include the commitHash.'
|
|
5513
|
+
].join("\n"),
|
|
5514
|
+
reminders: []
|
|
5515
|
+
});
|
|
5516
|
+
}
|
|
5517
|
+
if (action === "commit_done") {
|
|
5518
|
+
if (!checkpoint || checkpoint === null) {
|
|
5519
|
+
return guideResult(state, "FINALIZE", {
|
|
5520
|
+
instruction: 'You must stage files first. Call me with completedAction: "files_staged" after staging.',
|
|
5521
|
+
reminders: []
|
|
5522
|
+
});
|
|
5523
|
+
}
|
|
5524
|
+
if (checkpoint === "staged") {
|
|
5525
|
+
return guideResult(state, "FINALIZE", {
|
|
5526
|
+
instruction: 'You must pass pre-commit checks first. Call me with completedAction: "precommit_passed".',
|
|
5527
|
+
reminders: []
|
|
5528
|
+
});
|
|
5529
|
+
}
|
|
5530
|
+
if (checkpoint === "committed") {
|
|
5531
|
+
return guideResult(state, "FINALIZE", {
|
|
5532
|
+
instruction: "Commit was already recorded. Proceeding to completion.",
|
|
5533
|
+
reminders: []
|
|
5534
|
+
});
|
|
5535
|
+
}
|
|
5536
|
+
const commitHash = report.commitHash;
|
|
5537
|
+
if (!commitHash) {
|
|
5538
|
+
return guideResult(state, "FINALIZE", {
|
|
5539
|
+
instruction: "Missing commitHash in report. Call me again with the commit hash.",
|
|
5540
|
+
reminders: []
|
|
5541
|
+
});
|
|
5542
|
+
}
|
|
5543
|
+
const completedTicket = state.ticket ? { id: state.ticket.id, title: state.ticket.title, commitHash, risk: state.ticket.risk } : void 0;
|
|
5544
|
+
const updated = {
|
|
5545
|
+
...state,
|
|
5546
|
+
state: "COMPLETE",
|
|
5547
|
+
previousState: "FINALIZE",
|
|
5548
|
+
finalizeCheckpoint: "committed",
|
|
5549
|
+
completedTickets: completedTicket ? [...state.completedTickets, completedTicket] : state.completedTickets,
|
|
5550
|
+
ticket: void 0
|
|
5551
|
+
};
|
|
5552
|
+
const written = writeSessionSync(dir, updated);
|
|
5553
|
+
appendEvent(dir, {
|
|
5554
|
+
rev: written.revision,
|
|
5555
|
+
type: "commit",
|
|
5556
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5557
|
+
data: { commitHash, ticketId: completedTicket?.id }
|
|
5558
|
+
});
|
|
5559
|
+
return handleReportComplete(root, dir, refreshLease(written), { completedAction: "commit_done" });
|
|
5560
|
+
}
|
|
5561
|
+
return guideResult(state, "FINALIZE", {
|
|
5562
|
+
instruction: 'Unexpected action at FINALIZE. Stage files and call with completedAction: "files_staged", or commit and call with completedAction: "commit_done".',
|
|
5563
|
+
reminders: []
|
|
5564
|
+
});
|
|
5565
|
+
}
|
|
5566
|
+
async function handleReportComplete(root, dir, state, report) {
|
|
5567
|
+
const pressure = evaluatePressure(state);
|
|
5568
|
+
const updated = {
|
|
5569
|
+
...state,
|
|
5570
|
+
state: "COMPLETE",
|
|
5571
|
+
contextPressure: { ...state.contextPressure, level: pressure },
|
|
5572
|
+
finalizeCheckpoint: null
|
|
5573
|
+
};
|
|
5574
|
+
const ticketsDone = updated.completedTickets.length;
|
|
5575
|
+
const maxTickets = updated.config.maxTicketsPerSession;
|
|
5576
|
+
let nextState;
|
|
5577
|
+
let advice = "ok";
|
|
5578
|
+
if (pressure === "critical") {
|
|
5579
|
+
nextState = "HANDOVER";
|
|
5580
|
+
advice = "compact-now";
|
|
5581
|
+
} else if (ticketsDone >= maxTickets) {
|
|
5582
|
+
nextState = "HANDOVER";
|
|
5583
|
+
} else if (pressure === "high") {
|
|
5584
|
+
advice = "consider-compact";
|
|
5585
|
+
nextState = "PICK_TICKET";
|
|
5586
|
+
} else {
|
|
5587
|
+
nextState = "PICK_TICKET";
|
|
5588
|
+
}
|
|
5589
|
+
const { state: projectState } = await loadProject(root);
|
|
5590
|
+
const nextResult = nextTickets(projectState, 1);
|
|
5591
|
+
if (nextResult.kind !== "found") {
|
|
5592
|
+
nextState = "HANDOVER";
|
|
5593
|
+
}
|
|
5594
|
+
const transitioned = writeSessionSync(dir, {
|
|
5595
|
+
...updated,
|
|
5596
|
+
state: nextState,
|
|
5597
|
+
previousState: "COMPLETE"
|
|
5598
|
+
});
|
|
5599
|
+
if (nextState === "HANDOVER") {
|
|
5600
|
+
return guideResult(transitioned, "HANDOVER", {
|
|
5601
|
+
instruction: [
|
|
5602
|
+
`# Session Complete \u2014 ${ticketsDone} ticket(s) done`,
|
|
5603
|
+
"",
|
|
5604
|
+
"Write a session handover summarizing what was accomplished, decisions made, and what's next.",
|
|
5605
|
+
"",
|
|
5606
|
+
'Call me with completedAction: "handover_written" and include the content in handoverContent.'
|
|
5607
|
+
].join("\n"),
|
|
5608
|
+
reminders: [],
|
|
5609
|
+
transitionedFrom: "COMPLETE",
|
|
5610
|
+
contextAdvice: advice
|
|
5611
|
+
});
|
|
5612
|
+
}
|
|
5613
|
+
const candidates = nextTickets(projectState, 5);
|
|
5614
|
+
let candidatesText = "";
|
|
5615
|
+
if (candidates.kind === "found") {
|
|
5616
|
+
candidatesText = candidates.candidates.map(
|
|
5617
|
+
(c, i) => `${i + 1}. **${c.ticket.id}: ${c.ticket.title}** (${c.ticket.type})`
|
|
5618
|
+
).join("\n");
|
|
5619
|
+
}
|
|
5620
|
+
return guideResult(transitioned, "PICK_TICKET", {
|
|
5621
|
+
instruction: [
|
|
5622
|
+
`# Ticket Complete \u2014 Pick Next (${ticketsDone}/${maxTickets})`,
|
|
5623
|
+
"",
|
|
5624
|
+
candidatesText,
|
|
5625
|
+
"",
|
|
5626
|
+
'Pick a ticket by calling me with completedAction: "ticket_picked" and ticketId.'
|
|
5627
|
+
].join("\n"),
|
|
5628
|
+
reminders: [],
|
|
5629
|
+
transitionedFrom: "COMPLETE",
|
|
5630
|
+
contextAdvice: advice
|
|
5631
|
+
});
|
|
5632
|
+
}
|
|
5633
|
+
async function handleReportHandover(root, dir, state, report) {
|
|
5634
|
+
const content = report.handoverContent;
|
|
5635
|
+
if (!content) {
|
|
5636
|
+
return guideResult(state, "HANDOVER", {
|
|
5637
|
+
instruction: "Missing handoverContent. Write the handover and include it in the report.",
|
|
5638
|
+
reminders: []
|
|
5639
|
+
});
|
|
5640
|
+
}
|
|
5641
|
+
let handoverFailed = false;
|
|
5642
|
+
try {
|
|
5643
|
+
await handleHandoverCreate(content, "auto-session", "md", root);
|
|
5644
|
+
} catch (err) {
|
|
5645
|
+
handoverFailed = true;
|
|
5646
|
+
try {
|
|
5647
|
+
const fallbackPath = join7(dir, "handover-fallback.md");
|
|
5648
|
+
writeFileSync2(fallbackPath, content, "utf-8");
|
|
5649
|
+
} catch {
|
|
5650
|
+
}
|
|
5651
|
+
}
|
|
5652
|
+
const written = writeSessionSync(dir, {
|
|
5653
|
+
...state,
|
|
5654
|
+
state: "SESSION_END",
|
|
5655
|
+
previousState: "HANDOVER",
|
|
5656
|
+
status: "completed"
|
|
5657
|
+
});
|
|
5658
|
+
appendEvent(dir, {
|
|
5659
|
+
rev: written.revision,
|
|
5660
|
+
type: "session_end",
|
|
5661
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5662
|
+
data: { ticketsCompleted: written.completedTickets.length, handoverFailed }
|
|
5663
|
+
});
|
|
5664
|
+
const ticketsDone = written.completedTickets.length;
|
|
5665
|
+
return guideResult(written, "SESSION_END", {
|
|
5666
|
+
instruction: [
|
|
5667
|
+
"# Session Complete",
|
|
5668
|
+
"",
|
|
5669
|
+
`${ticketsDone} ticket(s) completed.${handoverFailed ? " Handover creation failed \u2014 fallback saved to session directory." : " Handover written."} Session ended.`,
|
|
5670
|
+
"",
|
|
5671
|
+
written.completedTickets.map((t) => `- ${t.id}${t.title ? `: ${t.title}` : ""} (${t.commitHash ?? "no commit"})`).join("\n")
|
|
5672
|
+
].join("\n"),
|
|
5673
|
+
reminders: [],
|
|
5674
|
+
transitionedFrom: "HANDOVER"
|
|
5675
|
+
});
|
|
5676
|
+
}
|
|
5677
|
+
async function handleResume(root, args) {
|
|
5678
|
+
if (!args.sessionId) return guideError(new Error("sessionId is required for resume"));
|
|
5679
|
+
const info = findSessionById(root, args.sessionId);
|
|
5680
|
+
if (!info) return guideError(new Error(`Session ${args.sessionId} not found`));
|
|
5681
|
+
if (info.state.state !== "COMPACT") {
|
|
5682
|
+
return guideError(new Error(
|
|
5683
|
+
`Session ${args.sessionId} is not in COMPACT state (current: ${info.state.state}). Use action: "report" to continue.`
|
|
5684
|
+
));
|
|
5685
|
+
}
|
|
5686
|
+
const resumeState = info.state.preCompactState;
|
|
5687
|
+
if (!resumeState || !WORKFLOW_STATES.includes(resumeState)) {
|
|
5688
|
+
return guideError(new Error(
|
|
5689
|
+
`Session ${args.sessionId} has invalid preCompactState: ${resumeState}. Cannot resume safely.`
|
|
5690
|
+
));
|
|
5691
|
+
}
|
|
5692
|
+
const written = writeSessionSync(info.dir, {
|
|
5693
|
+
...refreshLease(info.state),
|
|
5694
|
+
state: resumeState,
|
|
5695
|
+
preCompactState: null,
|
|
5696
|
+
resumeFromRevision: null,
|
|
5697
|
+
contextPressure: { ...info.state.contextPressure, compactionCount: (info.state.contextPressure?.compactionCount ?? 0) + 1 }
|
|
5698
|
+
});
|
|
5699
|
+
return guideResult(written, resumeState, {
|
|
5700
|
+
instruction: [
|
|
5701
|
+
"# Resumed After Compact",
|
|
5702
|
+
"",
|
|
5703
|
+
`Session restored at state: **${resumeState}**.`,
|
|
5704
|
+
written.ticket ? `Working on: **${written.ticket.id}: ${written.ticket.title}**` : "No ticket in progress.",
|
|
5705
|
+
"",
|
|
5706
|
+
"Continue where you left off. Call me when you complete the current step."
|
|
5707
|
+
].join("\n"),
|
|
5708
|
+
reminders: [
|
|
5709
|
+
"Do NOT use plan mode.",
|
|
5710
|
+
"Call autonomous_guide after completing each step."
|
|
5711
|
+
]
|
|
5712
|
+
});
|
|
5713
|
+
}
|
|
5714
|
+
async function handlePreCompact(root, args) {
|
|
5715
|
+
if (!args.sessionId) return guideError(new Error("sessionId is required for pre_compact"));
|
|
5716
|
+
const info = findSessionById(root, args.sessionId);
|
|
5717
|
+
if (!info) return guideError(new Error(`Session ${args.sessionId} not found`));
|
|
5718
|
+
if (info.state.state === "SESSION_END") {
|
|
5719
|
+
return guideError(new Error(`Session ${args.sessionId} is already ended and cannot be compacted.`));
|
|
5720
|
+
}
|
|
5721
|
+
if (info.state.state === "COMPACT") {
|
|
5722
|
+
return guideError(new Error(`Session ${args.sessionId} is already in COMPACT state. Call action: "resume" to continue.`));
|
|
5723
|
+
}
|
|
5724
|
+
const headResult = await gitHead(root);
|
|
5725
|
+
const written = writeSessionSync(info.dir, {
|
|
5726
|
+
...refreshLease(info.state),
|
|
5727
|
+
state: "COMPACT",
|
|
5728
|
+
previousState: info.state.state,
|
|
5729
|
+
preCompactState: info.state.state,
|
|
5730
|
+
resumeFromRevision: info.state.revision,
|
|
5731
|
+
git: {
|
|
5732
|
+
...info.state.git,
|
|
5733
|
+
expectedHead: headResult.ok ? headResult.data.hash : info.state.git.expectedHead
|
|
5734
|
+
}
|
|
5735
|
+
});
|
|
5736
|
+
try {
|
|
5737
|
+
const loadResult = await loadProject(root);
|
|
5738
|
+
const { saveSnapshot: saveSnapshot2 } = await Promise.resolve().then(() => (init_snapshot(), snapshot_exports));
|
|
5739
|
+
await saveSnapshot2(root, loadResult);
|
|
5740
|
+
} catch {
|
|
5741
|
+
}
|
|
5742
|
+
return guideResult(written, "COMPACT", {
|
|
5743
|
+
instruction: [
|
|
5744
|
+
"# Ready for Compact",
|
|
5745
|
+
"",
|
|
5746
|
+
"State flushed. Run `/compact` now.",
|
|
5747
|
+
"",
|
|
5748
|
+
"After compact, call `claudestory_autonomous_guide` with:",
|
|
5749
|
+
"```json",
|
|
5750
|
+
`{ "sessionId": "${written.sessionId}", "action": "resume" }`,
|
|
5751
|
+
"```"
|
|
5752
|
+
].join("\n"),
|
|
5753
|
+
reminders: []
|
|
5754
|
+
});
|
|
5755
|
+
}
|
|
5756
|
+
async function handleCancel(root, args) {
|
|
5757
|
+
if (!args.sessionId) {
|
|
5758
|
+
const active = findActiveSessionFull(root);
|
|
5759
|
+
if (!active) return guideError(new Error("No active session to cancel"));
|
|
5760
|
+
args = { ...args, sessionId: active.state.sessionId };
|
|
5761
|
+
}
|
|
5762
|
+
const info = findSessionById(root, args.sessionId);
|
|
5763
|
+
if (!info) return guideError(new Error(`Session ${args.sessionId} not found`));
|
|
5764
|
+
const written = writeSessionSync(info.dir, {
|
|
5765
|
+
...info.state,
|
|
5766
|
+
state: "SESSION_END",
|
|
5767
|
+
previousState: info.state.state,
|
|
5768
|
+
status: "completed"
|
|
5769
|
+
});
|
|
5770
|
+
appendEvent(info.dir, {
|
|
5771
|
+
rev: written.revision,
|
|
5772
|
+
type: "cancelled",
|
|
5773
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5774
|
+
data: { previousState: info.state.state }
|
|
5775
|
+
});
|
|
5776
|
+
return {
|
|
5777
|
+
content: [{ type: "text", text: `Session ${args.sessionId} cancelled. ${written.completedTickets.length} ticket(s) were completed.` }]
|
|
5778
|
+
};
|
|
5779
|
+
}
|
|
5780
|
+
function guideResult(state, currentState, opts) {
|
|
5781
|
+
const summary = {
|
|
5782
|
+
ticket: state.ticket ? `${state.ticket.id}: ${state.ticket.title}` : "none",
|
|
5783
|
+
risk: state.ticket?.risk ?? "unknown",
|
|
5784
|
+
completed: state.completedTickets.map((t) => t.id),
|
|
5785
|
+
currentStep: currentState,
|
|
5786
|
+
contextPressure: state.contextPressure?.level ?? "low",
|
|
5787
|
+
branch: state.git?.branch ?? null
|
|
5788
|
+
};
|
|
5789
|
+
const output = {
|
|
5790
|
+
sessionId: state.sessionId,
|
|
5791
|
+
state: currentState,
|
|
5792
|
+
transitionedFrom: opts.transitionedFrom,
|
|
5793
|
+
instruction: opts.instruction,
|
|
5794
|
+
reminders: opts.reminders ?? [],
|
|
5795
|
+
contextAdvice: opts.contextAdvice ?? "ok",
|
|
5796
|
+
sessionSummary: summary
|
|
5797
|
+
};
|
|
5798
|
+
const parts = [
|
|
5799
|
+
output.instruction,
|
|
5800
|
+
"",
|
|
5801
|
+
"---",
|
|
5802
|
+
`**Session:** ${output.sessionId}`,
|
|
5803
|
+
`**State:** ${output.state}${output.transitionedFrom ? ` (from ${output.transitionedFrom})` : ""}`,
|
|
5804
|
+
`**Ticket:** ${summary.ticket}`,
|
|
5805
|
+
`**Risk:** ${summary.risk}`,
|
|
5806
|
+
`**Completed:** ${summary.completed.length > 0 ? summary.completed.join(", ") : "none"}`,
|
|
5807
|
+
`**Pressure:** ${summary.contextPressure}`,
|
|
5808
|
+
summary.branch ? `**Branch:** ${summary.branch}` : "",
|
|
5809
|
+
output.contextAdvice !== "ok" ? `**Context:** ${output.contextAdvice}` : "",
|
|
5810
|
+
output.reminders.length > 0 ? `
|
|
5811
|
+
**Reminders:**
|
|
5812
|
+
${output.reminders.map((r) => `- ${r}`).join("\n")}` : ""
|
|
5813
|
+
].filter(Boolean);
|
|
5814
|
+
return { content: [{ type: "text", text: parts.join("\n") }] };
|
|
5815
|
+
}
|
|
5816
|
+
function guideError(err) {
|
|
5817
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
5818
|
+
return {
|
|
5819
|
+
content: [{ type: "text", text: `[autonomous_guide error] ${message}` }],
|
|
5820
|
+
isError: true
|
|
5821
|
+
};
|
|
5822
|
+
}
|
|
5823
|
+
function readFileSafe(path2) {
|
|
5824
|
+
try {
|
|
5825
|
+
return readFileSync2(path2, "utf-8");
|
|
5826
|
+
} catch {
|
|
5827
|
+
return "";
|
|
5828
|
+
}
|
|
5829
|
+
}
|
|
5830
|
+
var workspaceLocks;
|
|
5831
|
+
var init_guide = __esm({
|
|
5832
|
+
"src/autonomous/guide.ts"() {
|
|
5833
|
+
"use strict";
|
|
5834
|
+
init_esm_shims();
|
|
5835
|
+
init_session_types();
|
|
5836
|
+
init_session();
|
|
5837
|
+
init_state_machine();
|
|
5838
|
+
init_context_pressure();
|
|
5839
|
+
init_review_depth();
|
|
5840
|
+
init_git_inspector();
|
|
5841
|
+
init_project_loader();
|
|
5842
|
+
init_snapshot();
|
|
5843
|
+
init_snapshot();
|
|
5844
|
+
init_queries();
|
|
5845
|
+
init_recommend();
|
|
5846
|
+
init_handover();
|
|
5847
|
+
workspaceLocks = /* @__PURE__ */ new Map();
|
|
5848
|
+
}
|
|
5849
|
+
});
|
|
5850
|
+
|
|
5851
|
+
// src/cli/commands/phase.ts
|
|
5852
|
+
import { join as join8, resolve as resolve6 } from "path";
|
|
5853
|
+
function validatePhaseId(id) {
|
|
5854
|
+
if (id.length > PHASE_ID_MAX_LENGTH) {
|
|
5855
|
+
throw new CliValidationError("invalid_input", `Phase ID "${id}" exceeds ${PHASE_ID_MAX_LENGTH} characters`);
|
|
5856
|
+
}
|
|
5857
|
+
if (!PHASE_ID_REGEX.test(id)) {
|
|
5858
|
+
throw new CliValidationError("invalid_input", `Phase ID "${id}" must be lowercase alphanumeric with hyphens (e.g. "my-phase")`);
|
|
5859
|
+
}
|
|
5860
|
+
}
|
|
5861
|
+
function handlePhaseList(ctx) {
|
|
5862
|
+
return { output: formatPhaseList(ctx.state, ctx.format) };
|
|
5863
|
+
}
|
|
5864
|
+
function handlePhaseCurrent(ctx) {
|
|
5865
|
+
const phase = currentPhase(ctx.state);
|
|
5866
|
+
if (phase) {
|
|
5867
|
+
if (ctx.format === "json") {
|
|
5868
|
+
return { output: JSON.stringify(successEnvelope(phase), null, 2) };
|
|
5869
|
+
}
|
|
5870
|
+
const summary = phase.summary ?? phase.description;
|
|
5871
|
+
return { output: `${phase.name} (${phase.id}) \u2014 ${summary}` };
|
|
5872
|
+
}
|
|
5873
|
+
const hasLeavesInAnyPhase = ctx.state.roadmap.phases.some(
|
|
5874
|
+
(p) => ctx.state.phaseTickets(p.id).length > 0
|
|
5875
|
+
);
|
|
5876
|
+
if (!hasLeavesInAnyPhase) {
|
|
5877
|
+
if (ctx.format === "json") {
|
|
5878
|
+
return {
|
|
5879
|
+
output: JSON.stringify(successEnvelope({ current: null, reason: "no_phases" }), null, 2),
|
|
5880
|
+
exitCode: ExitCode.USER_ERROR
|
|
5881
|
+
};
|
|
5882
|
+
}
|
|
5883
|
+
return {
|
|
5884
|
+
output: "No phases with tickets defined.",
|
|
5885
|
+
exitCode: ExitCode.USER_ERROR
|
|
5886
|
+
};
|
|
5887
|
+
}
|
|
5888
|
+
if (ctx.format === "json") {
|
|
5889
|
+
return {
|
|
5890
|
+
output: JSON.stringify(successEnvelope({ current: null, reason: "all_complete" }), null, 2)
|
|
5891
|
+
};
|
|
5892
|
+
}
|
|
5893
|
+
return { output: "All phases complete." };
|
|
5894
|
+
}
|
|
5895
|
+
function handlePhaseTickets(phaseId, ctx) {
|
|
5896
|
+
const phaseExists = ctx.state.roadmap.phases.some((p) => p.id === phaseId);
|
|
5897
|
+
if (!phaseExists) {
|
|
5898
|
+
return {
|
|
5899
|
+
output: formatError("not_found", `Phase "${phaseId}" not found`, ctx.format),
|
|
5900
|
+
exitCode: ExitCode.USER_ERROR,
|
|
5901
|
+
errorCode: "not_found"
|
|
5902
|
+
};
|
|
5903
|
+
}
|
|
5904
|
+
return { output: formatPhaseTickets(phaseId, ctx.state, ctx.format) };
|
|
5905
|
+
}
|
|
5906
|
+
async function handlePhaseCreate(args, format, root) {
|
|
5907
|
+
validatePhaseId(args.id);
|
|
5908
|
+
if (args.atStart && args.after) {
|
|
5909
|
+
throw new CliValidationError("invalid_input", "Cannot use both --after and --at-start");
|
|
5910
|
+
}
|
|
5911
|
+
if (!args.atStart && !args.after) {
|
|
5912
|
+
throw new CliValidationError("invalid_input", "Must specify either --after <phase-id> or --at-start");
|
|
5913
|
+
}
|
|
5914
|
+
let createdPhase;
|
|
5915
|
+
await withProjectLock(root, { strict: true }, async ({ state }) => {
|
|
5916
|
+
if (state.roadmap.phases.some((p) => p.id === args.id)) {
|
|
5917
|
+
throw new CliValidationError("conflict", `Phase "${args.id}" already exists`);
|
|
5918
|
+
}
|
|
5919
|
+
const phase = {
|
|
5920
|
+
id: args.id,
|
|
5921
|
+
label: args.label,
|
|
5922
|
+
name: args.name,
|
|
5923
|
+
description: args.description,
|
|
5924
|
+
...args.summary !== void 0 && { summary: args.summary }
|
|
5925
|
+
};
|
|
5926
|
+
const newPhases = [...state.roadmap.phases];
|
|
5927
|
+
if (args.atStart) {
|
|
5928
|
+
newPhases.unshift(phase);
|
|
5929
|
+
} else {
|
|
5930
|
+
const afterIdx = newPhases.findIndex((p) => p.id === args.after);
|
|
5931
|
+
if (afterIdx < 0) {
|
|
5932
|
+
throw new CliValidationError("not_found", `Phase "${args.after}" not found`);
|
|
5933
|
+
}
|
|
5934
|
+
newPhases.splice(afterIdx + 1, 0, phase);
|
|
5935
|
+
}
|
|
5936
|
+
const newRoadmap = { ...state.roadmap, phases: newPhases };
|
|
5937
|
+
await writeRoadmapUnlocked(newRoadmap, root);
|
|
5938
|
+
createdPhase = phase;
|
|
5939
|
+
});
|
|
5940
|
+
if (!createdPhase) throw new Error("Phase not created");
|
|
5941
|
+
if (format === "json") {
|
|
5942
|
+
return { output: JSON.stringify(successEnvelope(createdPhase), null, 2) };
|
|
5943
|
+
}
|
|
5944
|
+
return { output: `Created phase ${createdPhase.id}: ${createdPhase.name}` };
|
|
5945
|
+
}
|
|
5946
|
+
async function handlePhaseRename(id, updates, format, root) {
|
|
5947
|
+
let updatedPhase;
|
|
5948
|
+
await withProjectLock(root, { strict: true }, async ({ state }) => {
|
|
5949
|
+
const idx = state.roadmap.phases.findIndex((p) => p.id === id);
|
|
5950
|
+
if (idx < 0) {
|
|
5951
|
+
throw new CliValidationError("not_found", `Phase "${id}" not found`);
|
|
5952
|
+
}
|
|
5953
|
+
const existing = state.roadmap.phases[idx];
|
|
5954
|
+
const phase = {
|
|
5955
|
+
...existing,
|
|
5956
|
+
...updates.name !== void 0 && { name: updates.name },
|
|
5957
|
+
...updates.label !== void 0 && { label: updates.label },
|
|
5958
|
+
...updates.description !== void 0 && { description: updates.description },
|
|
5959
|
+
...updates.summary !== void 0 && { summary: updates.summary }
|
|
5960
|
+
};
|
|
4118
5961
|
const newPhases = [...state.roadmap.phases];
|
|
4119
5962
|
newPhases[idx] = phase;
|
|
4120
5963
|
const newRoadmap = { ...state.roadmap, phases: newPhases };
|
|
@@ -4195,21 +6038,21 @@ async function handlePhaseDelete(id, reassign, format, root) {
|
|
|
4195
6038
|
const updated = { ...ticket, phase: reassign, order: maxOrder };
|
|
4196
6039
|
const parsed = TicketSchema.parse(updated);
|
|
4197
6040
|
const content = serializeJSON(parsed);
|
|
4198
|
-
const target =
|
|
6041
|
+
const target = join8(wrapDir, "tickets", `${parsed.id}.json`);
|
|
4199
6042
|
operations.push({ op: "write", target, content });
|
|
4200
6043
|
}
|
|
4201
6044
|
for (const issue of affectedIssues) {
|
|
4202
6045
|
const updated = { ...issue, phase: reassign };
|
|
4203
6046
|
const parsed = IssueSchema.parse(updated);
|
|
4204
6047
|
const content = serializeJSON(parsed);
|
|
4205
|
-
const target =
|
|
6048
|
+
const target = join8(wrapDir, "issues", `${parsed.id}.json`);
|
|
4206
6049
|
operations.push({ op: "write", target, content });
|
|
4207
6050
|
}
|
|
4208
6051
|
const newPhases = state.roadmap.phases.filter((p) => p.id !== id);
|
|
4209
6052
|
const newRoadmap = { ...state.roadmap, phases: newPhases };
|
|
4210
6053
|
const parsedRoadmap = RoadmapSchema.parse(newRoadmap);
|
|
4211
6054
|
const roadmapContent = serializeJSON(parsedRoadmap);
|
|
4212
|
-
const roadmapTarget =
|
|
6055
|
+
const roadmapTarget = join8(wrapDir, "roadmap.json");
|
|
4213
6056
|
operations.push({ op: "write", target: roadmapTarget, content: roadmapContent });
|
|
4214
6057
|
await runTransactionUnlocked(root, operations);
|
|
4215
6058
|
} else {
|
|
@@ -4241,15 +6084,15 @@ var init_phase = __esm({
|
|
|
4241
6084
|
});
|
|
4242
6085
|
|
|
4243
6086
|
// src/mcp/tools.ts
|
|
4244
|
-
import { z as
|
|
4245
|
-
import { join as
|
|
6087
|
+
import { z as z9 } from "zod";
|
|
6088
|
+
import { join as join9 } from "path";
|
|
4246
6089
|
function formatMcpError(code, message) {
|
|
4247
6090
|
return `[${code}] ${message}`;
|
|
4248
6091
|
}
|
|
4249
6092
|
async function runMcpReadTool(pinnedRoot, handler) {
|
|
4250
6093
|
try {
|
|
4251
6094
|
const { state, warnings } = await loadProject(pinnedRoot);
|
|
4252
|
-
const handoversDir =
|
|
6095
|
+
const handoversDir = join9(pinnedRoot, ".story", "handovers");
|
|
4253
6096
|
const ctx = { state, warnings, root: pinnedRoot, handoversDir, format: "md" };
|
|
4254
6097
|
const result = await handler(ctx);
|
|
4255
6098
|
if (result.errorCode && INFRASTRUCTURE_ERROR_CODES.includes(result.errorCode)) {
|
|
@@ -4313,7 +6156,7 @@ function registerAllTools(server, pinnedRoot) {
|
|
|
4313
6156
|
server.registerTool("claudestory_ticket_next", {
|
|
4314
6157
|
description: "Highest-priority unblocked ticket(s) with unblock impact and umbrella progress",
|
|
4315
6158
|
inputSchema: {
|
|
4316
|
-
count:
|
|
6159
|
+
count: z9.number().int().min(1).max(10).optional().describe("Number of candidates to return (default: 1)")
|
|
4317
6160
|
}
|
|
4318
6161
|
}, (args) => runMcpReadTool(
|
|
4319
6162
|
pinnedRoot,
|
|
@@ -4328,7 +6171,7 @@ function registerAllTools(server, pinnedRoot) {
|
|
|
4328
6171
|
server.registerTool("claudestory_handover_latest", {
|
|
4329
6172
|
description: "Content of the most recent handover document(s)",
|
|
4330
6173
|
inputSchema: {
|
|
4331
|
-
count:
|
|
6174
|
+
count: z9.number().int().min(1).max(10).optional().describe("Number of recent handovers to return (default: 1)")
|
|
4332
6175
|
}
|
|
4333
6176
|
}, (args) => runMcpReadTool(
|
|
4334
6177
|
pinnedRoot,
|
|
@@ -4343,7 +6186,7 @@ function registerAllTools(server, pinnedRoot) {
|
|
|
4343
6186
|
server.registerTool("claudestory_phase_tickets", {
|
|
4344
6187
|
description: "Leaf tickets for a specific phase, sorted by order",
|
|
4345
6188
|
inputSchema: {
|
|
4346
|
-
phaseId:
|
|
6189
|
+
phaseId: z9.string().describe("Phase ID (e.g. p5b, dogfood)")
|
|
4347
6190
|
}
|
|
4348
6191
|
}, (args) => runMcpReadTool(pinnedRoot, (ctx) => {
|
|
4349
6192
|
const phaseExists = ctx.state.roadmap.phases.some((p) => p.id === args.phaseId);
|
|
@@ -4359,9 +6202,9 @@ function registerAllTools(server, pinnedRoot) {
|
|
|
4359
6202
|
server.registerTool("claudestory_ticket_list", {
|
|
4360
6203
|
description: "List leaf tickets with optional filters",
|
|
4361
6204
|
inputSchema: {
|
|
4362
|
-
status:
|
|
4363
|
-
phase:
|
|
4364
|
-
type:
|
|
6205
|
+
status: z9.enum(TICKET_STATUSES).optional().describe("Filter by status: open, inprogress, complete"),
|
|
6206
|
+
phase: z9.string().optional().describe("Filter by phase ID"),
|
|
6207
|
+
type: z9.enum(TICKET_TYPES).optional().describe("Filter by type: task, feature, chore")
|
|
4365
6208
|
}
|
|
4366
6209
|
}, (args) => runMcpReadTool(pinnedRoot, (ctx) => {
|
|
4367
6210
|
if (args.phase) {
|
|
@@ -4382,15 +6225,15 @@ function registerAllTools(server, pinnedRoot) {
|
|
|
4382
6225
|
server.registerTool("claudestory_ticket_get", {
|
|
4383
6226
|
description: "Get a ticket by ID (includes umbrella tickets)",
|
|
4384
6227
|
inputSchema: {
|
|
4385
|
-
id:
|
|
6228
|
+
id: z9.string().regex(TICKET_ID_REGEX).describe("Ticket ID (e.g. T-001, T-079b)")
|
|
4386
6229
|
}
|
|
4387
6230
|
}, (args) => runMcpReadTool(pinnedRoot, (ctx) => handleTicketGet(args.id, ctx)));
|
|
4388
6231
|
server.registerTool("claudestory_issue_list", {
|
|
4389
6232
|
description: "List issues with optional filters",
|
|
4390
6233
|
inputSchema: {
|
|
4391
|
-
status:
|
|
4392
|
-
severity:
|
|
4393
|
-
component:
|
|
6234
|
+
status: z9.enum(ISSUE_STATUSES).optional().describe("Filter by status: open, inprogress, resolved"),
|
|
6235
|
+
severity: z9.enum(ISSUE_SEVERITIES).optional().describe("Filter by severity: critical, high, medium, low"),
|
|
6236
|
+
component: z9.string().optional().describe("Filter by component name")
|
|
4394
6237
|
}
|
|
4395
6238
|
}, (args) => runMcpReadTool(
|
|
4396
6239
|
pinnedRoot,
|
|
@@ -4399,13 +6242,13 @@ function registerAllTools(server, pinnedRoot) {
|
|
|
4399
6242
|
server.registerTool("claudestory_issue_get", {
|
|
4400
6243
|
description: "Get an issue by ID",
|
|
4401
6244
|
inputSchema: {
|
|
4402
|
-
id:
|
|
6245
|
+
id: z9.string().regex(ISSUE_ID_REGEX).describe("Issue ID (e.g. ISS-001)")
|
|
4403
6246
|
}
|
|
4404
6247
|
}, (args) => runMcpReadTool(pinnedRoot, (ctx) => handleIssueGet(args.id, ctx)));
|
|
4405
6248
|
server.registerTool("claudestory_handover_get", {
|
|
4406
6249
|
description: "Content of a specific handover document by filename",
|
|
4407
6250
|
inputSchema: {
|
|
4408
|
-
filename:
|
|
6251
|
+
filename: z9.string().describe("Handover filename (e.g. 2026-03-20-session.md)")
|
|
4409
6252
|
}
|
|
4410
6253
|
}, (args) => runMcpReadTool(pinnedRoot, (ctx) => handleHandoverGet(args.filename, ctx)));
|
|
4411
6254
|
server.registerTool("claudestory_recap", {
|
|
@@ -4414,7 +6257,7 @@ function registerAllTools(server, pinnedRoot) {
|
|
|
4414
6257
|
server.registerTool("claudestory_recommend", {
|
|
4415
6258
|
description: "Context-aware ranked work suggestions mixing tickets and issues",
|
|
4416
6259
|
inputSchema: {
|
|
4417
|
-
count:
|
|
6260
|
+
count: z9.number().int().min(1).max(10).optional().describe("Number of recommendations (default: 5)")
|
|
4418
6261
|
}
|
|
4419
6262
|
}, (args) => runMcpReadTool(
|
|
4420
6263
|
pinnedRoot,
|
|
@@ -4426,8 +6269,8 @@ function registerAllTools(server, pinnedRoot) {
|
|
|
4426
6269
|
server.registerTool("claudestory_export", {
|
|
4427
6270
|
description: "Self-contained project document for sharing",
|
|
4428
6271
|
inputSchema: {
|
|
4429
|
-
phase:
|
|
4430
|
-
all:
|
|
6272
|
+
phase: z9.string().optional().describe("Export a single phase by ID"),
|
|
6273
|
+
all: z9.boolean().optional().describe("Export entire project")
|
|
4431
6274
|
}
|
|
4432
6275
|
}, (args) => {
|
|
4433
6276
|
if (!args.phase && !args.all) {
|
|
@@ -4449,8 +6292,8 @@ function registerAllTools(server, pinnedRoot) {
|
|
|
4449
6292
|
server.registerTool("claudestory_handover_create", {
|
|
4450
6293
|
description: "Create a handover document from markdown content",
|
|
4451
6294
|
inputSchema: {
|
|
4452
|
-
content:
|
|
4453
|
-
slug:
|
|
6295
|
+
content: z9.string().describe("Markdown content of the handover"),
|
|
6296
|
+
slug: z9.string().optional().describe("Slug for filename (e.g. phase5b-wrapup). Default: session")
|
|
4454
6297
|
}
|
|
4455
6298
|
}, (args) => {
|
|
4456
6299
|
if (!args.content?.trim()) {
|
|
@@ -4467,12 +6310,12 @@ function registerAllTools(server, pinnedRoot) {
|
|
|
4467
6310
|
server.registerTool("claudestory_ticket_create", {
|
|
4468
6311
|
description: "Create a new ticket",
|
|
4469
6312
|
inputSchema: {
|
|
4470
|
-
title:
|
|
4471
|
-
type:
|
|
4472
|
-
phase:
|
|
4473
|
-
description:
|
|
4474
|
-
blockedBy:
|
|
4475
|
-
parentTicket:
|
|
6313
|
+
title: z9.string().describe("Ticket title"),
|
|
6314
|
+
type: z9.enum(TICKET_TYPES).describe("Ticket type: task, feature, chore"),
|
|
6315
|
+
phase: z9.string().optional().describe("Phase ID"),
|
|
6316
|
+
description: z9.string().optional().describe("Ticket description"),
|
|
6317
|
+
blockedBy: z9.array(z9.string().regex(TICKET_ID_REGEX)).optional().describe("IDs of blocking tickets"),
|
|
6318
|
+
parentTicket: z9.string().regex(TICKET_ID_REGEX).optional().describe("Parent ticket ID (makes this a sub-ticket)")
|
|
4476
6319
|
}
|
|
4477
6320
|
}, (args) => runMcpWriteTool(
|
|
4478
6321
|
pinnedRoot,
|
|
@@ -4492,14 +6335,15 @@ function registerAllTools(server, pinnedRoot) {
|
|
|
4492
6335
|
server.registerTool("claudestory_ticket_update", {
|
|
4493
6336
|
description: "Update an existing ticket",
|
|
4494
6337
|
inputSchema: {
|
|
4495
|
-
id:
|
|
4496
|
-
status:
|
|
4497
|
-
title:
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
|
|
4502
|
-
|
|
6338
|
+
id: z9.string().regex(TICKET_ID_REGEX).describe("Ticket ID (e.g. T-001)"),
|
|
6339
|
+
status: z9.enum(TICKET_STATUSES).optional().describe("New status: open, inprogress, complete"),
|
|
6340
|
+
title: z9.string().optional().describe("New title"),
|
|
6341
|
+
type: z9.enum(TICKET_TYPES).optional().describe("New type: task, feature, chore"),
|
|
6342
|
+
order: z9.number().int().optional().describe("New sort order"),
|
|
6343
|
+
description: z9.string().optional().describe("New description"),
|
|
6344
|
+
phase: z9.string().nullable().optional().describe("New phase ID (null to clear)"),
|
|
6345
|
+
parentTicket: z9.string().regex(TICKET_ID_REGEX).nullable().optional().describe("Parent ticket ID (null to clear)"),
|
|
6346
|
+
blockedBy: z9.array(z9.string().regex(TICKET_ID_REGEX)).optional().describe("IDs of blocking tickets")
|
|
4503
6347
|
}
|
|
4504
6348
|
}, (args) => runMcpWriteTool(
|
|
4505
6349
|
pinnedRoot,
|
|
@@ -4508,6 +6352,7 @@ function registerAllTools(server, pinnedRoot) {
|
|
|
4508
6352
|
{
|
|
4509
6353
|
status: args.status,
|
|
4510
6354
|
title: args.title,
|
|
6355
|
+
type: args.type,
|
|
4511
6356
|
order: args.order,
|
|
4512
6357
|
description: args.description,
|
|
4513
6358
|
phase: args.phase,
|
|
@@ -4521,13 +6366,13 @@ function registerAllTools(server, pinnedRoot) {
|
|
|
4521
6366
|
server.registerTool("claudestory_issue_create", {
|
|
4522
6367
|
description: "Create a new issue",
|
|
4523
6368
|
inputSchema: {
|
|
4524
|
-
title:
|
|
4525
|
-
severity:
|
|
4526
|
-
impact:
|
|
4527
|
-
components:
|
|
4528
|
-
relatedTickets:
|
|
4529
|
-
location:
|
|
4530
|
-
phase:
|
|
6369
|
+
title: z9.string().describe("Issue title"),
|
|
6370
|
+
severity: z9.enum(ISSUE_SEVERITIES).describe("Issue severity: critical, high, medium, low"),
|
|
6371
|
+
impact: z9.string().describe("Impact description"),
|
|
6372
|
+
components: z9.array(z9.string()).optional().describe("Affected components"),
|
|
6373
|
+
relatedTickets: z9.array(z9.string().regex(TICKET_ID_REGEX)).optional().describe("Related ticket IDs"),
|
|
6374
|
+
location: z9.array(z9.string()).optional().describe("File locations"),
|
|
6375
|
+
phase: z9.string().optional().describe("Phase ID")
|
|
4531
6376
|
}
|
|
4532
6377
|
}, (args) => runMcpWriteTool(
|
|
4533
6378
|
pinnedRoot,
|
|
@@ -4548,15 +6393,17 @@ function registerAllTools(server, pinnedRoot) {
|
|
|
4548
6393
|
server.registerTool("claudestory_issue_update", {
|
|
4549
6394
|
description: "Update an existing issue",
|
|
4550
6395
|
inputSchema: {
|
|
4551
|
-
id:
|
|
4552
|
-
status:
|
|
4553
|
-
title:
|
|
4554
|
-
severity:
|
|
4555
|
-
impact:
|
|
4556
|
-
resolution:
|
|
4557
|
-
components:
|
|
4558
|
-
relatedTickets:
|
|
4559
|
-
location:
|
|
6396
|
+
id: z9.string().regex(ISSUE_ID_REGEX).describe("Issue ID (e.g. ISS-001)"),
|
|
6397
|
+
status: z9.enum(ISSUE_STATUSES).optional().describe("New status: open, inprogress, resolved"),
|
|
6398
|
+
title: z9.string().optional().describe("New title"),
|
|
6399
|
+
severity: z9.enum(ISSUE_SEVERITIES).optional().describe("New severity"),
|
|
6400
|
+
impact: z9.string().optional().describe("New impact description"),
|
|
6401
|
+
resolution: z9.string().nullable().optional().describe("Resolution description (null to clear)"),
|
|
6402
|
+
components: z9.array(z9.string()).optional().describe("Affected components"),
|
|
6403
|
+
relatedTickets: z9.array(z9.string().regex(TICKET_ID_REGEX)).optional().describe("Related ticket IDs"),
|
|
6404
|
+
location: z9.array(z9.string()).optional().describe("File locations"),
|
|
6405
|
+
order: z9.number().int().optional().describe("New sort order"),
|
|
6406
|
+
phase: z9.string().nullable().optional().describe("New phase ID (null to clear)")
|
|
4560
6407
|
}
|
|
4561
6408
|
}, (args) => runMcpWriteTool(
|
|
4562
6409
|
pinnedRoot,
|
|
@@ -4570,7 +6417,9 @@ function registerAllTools(server, pinnedRoot) {
|
|
|
4570
6417
|
resolution: args.resolution,
|
|
4571
6418
|
components: args.components,
|
|
4572
6419
|
relatedTickets: args.relatedTickets,
|
|
4573
|
-
location: args.location
|
|
6420
|
+
location: args.location,
|
|
6421
|
+
order: args.order,
|
|
6422
|
+
phase: args.phase
|
|
4574
6423
|
},
|
|
4575
6424
|
format,
|
|
4576
6425
|
root
|
|
@@ -4579,8 +6428,8 @@ function registerAllTools(server, pinnedRoot) {
|
|
|
4579
6428
|
server.registerTool("claudestory_note_list", {
|
|
4580
6429
|
description: "List notes with optional status/tag filters",
|
|
4581
6430
|
inputSchema: {
|
|
4582
|
-
status:
|
|
4583
|
-
tag:
|
|
6431
|
+
status: z9.enum(NOTE_STATUSES).optional().describe("Filter by status: active, archived"),
|
|
6432
|
+
tag: z9.string().optional().describe("Filter by tag")
|
|
4584
6433
|
}
|
|
4585
6434
|
}, (args) => runMcpReadTool(
|
|
4586
6435
|
pinnedRoot,
|
|
@@ -4589,15 +6438,15 @@ function registerAllTools(server, pinnedRoot) {
|
|
|
4589
6438
|
server.registerTool("claudestory_note_get", {
|
|
4590
6439
|
description: "Get a note by ID",
|
|
4591
6440
|
inputSchema: {
|
|
4592
|
-
id:
|
|
6441
|
+
id: z9.string().regex(NOTE_ID_REGEX).describe("Note ID (e.g. N-001)")
|
|
4593
6442
|
}
|
|
4594
6443
|
}, (args) => runMcpReadTool(pinnedRoot, (ctx) => handleNoteGet(args.id, ctx)));
|
|
4595
6444
|
server.registerTool("claudestory_note_create", {
|
|
4596
6445
|
description: "Create a new note",
|
|
4597
6446
|
inputSchema: {
|
|
4598
|
-
content:
|
|
4599
|
-
title:
|
|
4600
|
-
tags:
|
|
6447
|
+
content: z9.string().describe("Note content"),
|
|
6448
|
+
title: z9.string().optional().describe("Note title"),
|
|
6449
|
+
tags: z9.array(z9.string()).optional().describe("Tags for the note")
|
|
4601
6450
|
}
|
|
4602
6451
|
}, (args) => runMcpWriteTool(
|
|
4603
6452
|
pinnedRoot,
|
|
@@ -4614,11 +6463,11 @@ function registerAllTools(server, pinnedRoot) {
|
|
|
4614
6463
|
server.registerTool("claudestory_note_update", {
|
|
4615
6464
|
description: "Update an existing note",
|
|
4616
6465
|
inputSchema: {
|
|
4617
|
-
id:
|
|
4618
|
-
content:
|
|
4619
|
-
title:
|
|
4620
|
-
tags:
|
|
4621
|
-
status:
|
|
6466
|
+
id: z9.string().regex(NOTE_ID_REGEX).describe("Note ID (e.g. N-001)"),
|
|
6467
|
+
content: z9.string().optional().describe("New content"),
|
|
6468
|
+
title: z9.string().nullable().optional().describe("New title (null to clear)"),
|
|
6469
|
+
tags: z9.array(z9.string()).optional().describe("New tags (replaces existing)"),
|
|
6470
|
+
status: z9.enum(NOTE_STATUSES).optional().describe("New status: active, archived")
|
|
4622
6471
|
}
|
|
4623
6472
|
}, (args) => runMcpWriteTool(
|
|
4624
6473
|
pinnedRoot,
|
|
@@ -4633,122 +6482,103 @@ function registerAllTools(server, pinnedRoot) {
|
|
|
4633
6482
|
},
|
|
4634
6483
|
format,
|
|
4635
6484
|
root
|
|
4636
|
-
)
|
|
4637
|
-
));
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
|
|
4656
|
-
|
|
4657
|
-
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
|
|
4661
|
-
|
|
4662
|
-
|
|
4663
|
-
|
|
4664
|
-
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
})
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
|
|
4687
|
-
|
|
4688
|
-
|
|
4689
|
-
|
|
4690
|
-
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
}
|
|
4695
|
-
return null;
|
|
4696
|
-
}
|
|
4697
|
-
try {
|
|
4698
|
-
const root = discoverProjectRoot();
|
|
4699
|
-
return root ? realpathSync(root) : null;
|
|
4700
|
-
} catch {
|
|
4701
|
-
return null;
|
|
4702
|
-
}
|
|
4703
|
-
}
|
|
4704
|
-
async function main() {
|
|
4705
|
-
const root = tryDiscoverRoot();
|
|
4706
|
-
const server = new McpServer(
|
|
4707
|
-
{ name: "claudestory", version },
|
|
4708
|
-
{
|
|
4709
|
-
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 in the current directory. Navigate to a project with a .story/ directory, or set CLAUDESTORY_PROJECT_ROOT."
|
|
4710
|
-
}
|
|
4711
|
-
);
|
|
4712
|
-
if (root) {
|
|
4713
|
-
registerAllTools(server, root);
|
|
4714
|
-
process.stderr.write(`claudestory MCP server running (root: ${root})
|
|
4715
|
-
`);
|
|
4716
|
-
} else {
|
|
4717
|
-
server.registerTool("claudestory_status", {
|
|
4718
|
-
description: "Project summary \u2014 returns error if no .story/ project found"
|
|
4719
|
-
}, () => Promise.resolve({
|
|
4720
|
-
content: [{ type: "text", text: "No .story/ project found. Navigate to a directory containing .story/ or set CLAUDESTORY_PROJECT_ROOT." }],
|
|
4721
|
-
isError: true
|
|
4722
|
-
}));
|
|
4723
|
-
process.stderr.write("claudestory MCP server running (no project found \u2014 tools will report errors)\n");
|
|
4724
|
-
}
|
|
4725
|
-
const transport = new StdioServerTransport();
|
|
4726
|
-
await server.connect(transport);
|
|
6485
|
+
)
|
|
6486
|
+
));
|
|
6487
|
+
server.registerTool("claudestory_phase_create", {
|
|
6488
|
+
description: "Create a new phase in the roadmap. Exactly one of after or atStart is required for positioning.",
|
|
6489
|
+
inputSchema: {
|
|
6490
|
+
id: z9.string().describe("Phase ID \u2014 lowercase alphanumeric with hyphens (e.g. 'my-phase')"),
|
|
6491
|
+
name: z9.string().describe("Phase display name"),
|
|
6492
|
+
label: z9.string().describe("Phase label (e.g. 'PHASE 1')"),
|
|
6493
|
+
description: z9.string().describe("Phase description"),
|
|
6494
|
+
summary: z9.string().optional().describe("One-line summary for compact display"),
|
|
6495
|
+
after: z9.string().optional().describe("Insert after this phase ID"),
|
|
6496
|
+
atStart: z9.boolean().optional().describe("Insert at beginning of roadmap")
|
|
6497
|
+
}
|
|
6498
|
+
}, (args) => runMcpWriteTool(
|
|
6499
|
+
pinnedRoot,
|
|
6500
|
+
(root, format) => handlePhaseCreate(
|
|
6501
|
+
{
|
|
6502
|
+
id: args.id,
|
|
6503
|
+
name: args.name,
|
|
6504
|
+
label: args.label,
|
|
6505
|
+
description: args.description,
|
|
6506
|
+
summary: args.summary,
|
|
6507
|
+
after: args.after,
|
|
6508
|
+
atStart: args.atStart ?? false
|
|
6509
|
+
},
|
|
6510
|
+
format,
|
|
6511
|
+
root
|
|
6512
|
+
)
|
|
6513
|
+
));
|
|
6514
|
+
server.registerTool("claudestory_selftest", {
|
|
6515
|
+
description: "Integration smoke test \u2014 creates, updates, and deletes test entities to verify the full pipeline"
|
|
6516
|
+
}, () => runMcpWriteTool(
|
|
6517
|
+
pinnedRoot,
|
|
6518
|
+
(root, format) => handleSelftest(root, format)
|
|
6519
|
+
));
|
|
6520
|
+
server.registerTool("claudestory_autonomous_guide", {
|
|
6521
|
+
description: "Autonomous session orchestrator. Call at every decision point during autonomous mode.",
|
|
6522
|
+
inputSchema: {
|
|
6523
|
+
sessionId: z9.string().uuid().nullable().describe("Session ID (null for start action)"),
|
|
6524
|
+
action: z9.enum(["start", "report", "resume", "pre_compact", "cancel"]).describe("Action to perform"),
|
|
6525
|
+
report: z9.object({
|
|
6526
|
+
completedAction: z9.string().describe("What was completed"),
|
|
6527
|
+
ticketId: z9.string().optional().describe("Ticket ID (for ticket_picked)"),
|
|
6528
|
+
commitHash: z9.string().optional().describe("Git commit hash (for commit_done)"),
|
|
6529
|
+
handoverContent: z9.string().optional().describe("Handover markdown content"),
|
|
6530
|
+
verdict: z9.string().optional().describe("Review verdict: approve|revise|request_changes|reject"),
|
|
6531
|
+
findings: z9.array(z9.object({
|
|
6532
|
+
id: z9.string(),
|
|
6533
|
+
severity: z9.string(),
|
|
6534
|
+
category: z9.string(),
|
|
6535
|
+
description: z9.string(),
|
|
6536
|
+
disposition: z9.string()
|
|
6537
|
+
})).optional().describe("Review findings"),
|
|
6538
|
+
reviewerSessionId: z9.string().optional().describe("Codex session ID"),
|
|
6539
|
+
notes: z9.string().optional().describe("Free-text notes")
|
|
6540
|
+
}).optional().describe("Report data (required for report action)")
|
|
6541
|
+
}
|
|
6542
|
+
}, (args) => handleAutonomousGuide(pinnedRoot, args));
|
|
4727
6543
|
}
|
|
4728
|
-
var
|
|
4729
|
-
var
|
|
4730
|
-
"src/mcp/
|
|
6544
|
+
var INFRASTRUCTURE_ERROR_CODES;
|
|
6545
|
+
var init_tools = __esm({
|
|
6546
|
+
"src/mcp/tools.ts"() {
|
|
4731
6547
|
"use strict";
|
|
4732
6548
|
init_esm_shims();
|
|
4733
|
-
|
|
4734
|
-
|
|
4735
|
-
|
|
4736
|
-
|
|
4737
|
-
|
|
4738
|
-
|
|
4739
|
-
|
|
4740
|
-
|
|
4741
|
-
|
|
4742
|
-
|
|
6549
|
+
init_project_loader();
|
|
6550
|
+
init_errors();
|
|
6551
|
+
init_helpers();
|
|
6552
|
+
init_types();
|
|
6553
|
+
init_status();
|
|
6554
|
+
init_validate();
|
|
6555
|
+
init_handover();
|
|
6556
|
+
init_blocker();
|
|
6557
|
+
init_ticket2();
|
|
6558
|
+
init_issue2();
|
|
6559
|
+
init_recap();
|
|
6560
|
+
init_note2();
|
|
6561
|
+
init_recommend2();
|
|
6562
|
+
init_snapshot2();
|
|
6563
|
+
init_export();
|
|
6564
|
+
init_selftest();
|
|
6565
|
+
init_handover();
|
|
6566
|
+
init_guide();
|
|
6567
|
+
init_phase();
|
|
6568
|
+
INFRASTRUCTURE_ERROR_CODES = [
|
|
6569
|
+
"io_error",
|
|
6570
|
+
"project_corrupt",
|
|
6571
|
+
"version_mismatch"
|
|
6572
|
+
];
|
|
4743
6573
|
}
|
|
4744
6574
|
});
|
|
4745
6575
|
|
|
4746
6576
|
// src/core/init.ts
|
|
4747
|
-
import { mkdir as mkdir4, stat as stat2 } from "fs/promises";
|
|
4748
|
-
import { join as
|
|
6577
|
+
import { mkdir as mkdir4, stat as stat2, readFile as readFile4, writeFile as writeFile2 } from "fs/promises";
|
|
6578
|
+
import { join as join10, resolve as resolve7 } from "path";
|
|
4749
6579
|
async function initProject(root, options) {
|
|
4750
|
-
const absRoot =
|
|
4751
|
-
const wrapDir =
|
|
6580
|
+
const absRoot = resolve7(root);
|
|
6581
|
+
const wrapDir = join10(absRoot, ".story");
|
|
4752
6582
|
let exists = false;
|
|
4753
6583
|
try {
|
|
4754
6584
|
const s = await stat2(wrapDir);
|
|
@@ -4768,10 +6598,10 @@ async function initProject(root, options) {
|
|
|
4768
6598
|
".story/ already exists. Use --force to overwrite config and roadmap."
|
|
4769
6599
|
);
|
|
4770
6600
|
}
|
|
4771
|
-
await mkdir4(
|
|
4772
|
-
await mkdir4(
|
|
4773
|
-
await mkdir4(
|
|
4774
|
-
await mkdir4(
|
|
6601
|
+
await mkdir4(join10(wrapDir, "tickets"), { recursive: true });
|
|
6602
|
+
await mkdir4(join10(wrapDir, "issues"), { recursive: true });
|
|
6603
|
+
await mkdir4(join10(wrapDir, "handovers"), { recursive: true });
|
|
6604
|
+
await mkdir4(join10(wrapDir, "notes"), { recursive: true });
|
|
4775
6605
|
const created = [
|
|
4776
6606
|
".story/config.json",
|
|
4777
6607
|
".story/roadmap.json",
|
|
@@ -4798,7 +6628,7 @@ async function initProject(root, options) {
|
|
|
4798
6628
|
const roadmap = {
|
|
4799
6629
|
title: options.name,
|
|
4800
6630
|
date: today,
|
|
4801
|
-
phases: [
|
|
6631
|
+
phases: options.phases ?? [
|
|
4802
6632
|
{
|
|
4803
6633
|
id: "p0",
|
|
4804
6634
|
label: "PHASE 0",
|
|
@@ -4810,6 +6640,8 @@ async function initProject(root, options) {
|
|
|
4810
6640
|
};
|
|
4811
6641
|
await writeConfig(config, absRoot);
|
|
4812
6642
|
await writeRoadmap(roadmap, absRoot);
|
|
6643
|
+
const gitignorePath = join10(wrapDir, ".gitignore");
|
|
6644
|
+
await ensureGitignoreEntries(gitignorePath, STORY_GITIGNORE_ENTRIES);
|
|
4813
6645
|
const warnings = [];
|
|
4814
6646
|
if (options.force && exists) {
|
|
4815
6647
|
try {
|
|
@@ -4828,12 +6660,158 @@ async function initProject(root, options) {
|
|
|
4828
6660
|
warnings
|
|
4829
6661
|
};
|
|
4830
6662
|
}
|
|
6663
|
+
async function ensureGitignoreEntries(gitignorePath, entries) {
|
|
6664
|
+
let existing = "";
|
|
6665
|
+
try {
|
|
6666
|
+
existing = await readFile4(gitignorePath, "utf-8");
|
|
6667
|
+
} catch {
|
|
6668
|
+
}
|
|
6669
|
+
const lines = existing.split("\n").map((l) => l.trim());
|
|
6670
|
+
const missing = entries.filter((e) => !lines.includes(e));
|
|
6671
|
+
if (missing.length === 0) return;
|
|
6672
|
+
let content = existing;
|
|
6673
|
+
if (content.length > 0 && !content.endsWith("\n")) content += "\n";
|
|
6674
|
+
content += missing.join("\n") + "\n";
|
|
6675
|
+
await writeFile2(gitignorePath, content, "utf-8");
|
|
6676
|
+
}
|
|
6677
|
+
var STORY_GITIGNORE_ENTRIES;
|
|
4831
6678
|
var init_init = __esm({
|
|
4832
6679
|
"src/core/init.ts"() {
|
|
4833
6680
|
"use strict";
|
|
4834
6681
|
init_esm_shims();
|
|
4835
6682
|
init_project_loader();
|
|
4836
6683
|
init_errors();
|
|
6684
|
+
STORY_GITIGNORE_ENTRIES = ["snapshots/", "status.json", "sessions/"];
|
|
6685
|
+
}
|
|
6686
|
+
});
|
|
6687
|
+
|
|
6688
|
+
// src/mcp/index.ts
|
|
6689
|
+
var mcp_exports = {};
|
|
6690
|
+
import { realpathSync as realpathSync2, existsSync as existsSync8 } from "fs";
|
|
6691
|
+
import { resolve as resolve8, join as join11, isAbsolute } from "path";
|
|
6692
|
+
import { z as z10 } from "zod";
|
|
6693
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
6694
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6695
|
+
function tryDiscoverRoot() {
|
|
6696
|
+
const envRoot = process.env[ENV_VAR2];
|
|
6697
|
+
if (envRoot) {
|
|
6698
|
+
if (!isAbsolute(envRoot)) {
|
|
6699
|
+
process.stderr.write(`Warning: ${ENV_VAR2} must be an absolute path, got: ${envRoot}
|
|
6700
|
+
`);
|
|
6701
|
+
return null;
|
|
6702
|
+
}
|
|
6703
|
+
const resolved = resolve8(envRoot);
|
|
6704
|
+
try {
|
|
6705
|
+
const canonical = realpathSync2(resolved);
|
|
6706
|
+
if (existsSync8(join11(canonical, CONFIG_PATH2))) {
|
|
6707
|
+
return canonical;
|
|
6708
|
+
}
|
|
6709
|
+
process.stderr.write(`Warning: No .story/config.json at ${canonical}
|
|
6710
|
+
`);
|
|
6711
|
+
} catch {
|
|
6712
|
+
process.stderr.write(`Warning: ${ENV_VAR2} path does not exist: ${resolved}
|
|
6713
|
+
`);
|
|
6714
|
+
}
|
|
6715
|
+
return null;
|
|
6716
|
+
}
|
|
6717
|
+
try {
|
|
6718
|
+
const root = discoverProjectRoot();
|
|
6719
|
+
return root ? realpathSync2(root) : null;
|
|
6720
|
+
} catch {
|
|
6721
|
+
return null;
|
|
6722
|
+
}
|
|
6723
|
+
}
|
|
6724
|
+
function registerDegradedTools(server) {
|
|
6725
|
+
const degradedStatus = server.registerTool("claudestory_status", {
|
|
6726
|
+
description: "Project summary \u2014 returns guidance if no .story/ project found"
|
|
6727
|
+
}, () => Promise.resolve({
|
|
6728
|
+
content: [{ type: "text", text: "No .story/ project found. Use claudestory_init to create one, or navigate to a directory with .story/." }],
|
|
6729
|
+
isError: true
|
|
6730
|
+
}));
|
|
6731
|
+
const degradedInit = server.registerTool("claudestory_init", {
|
|
6732
|
+
description: "Initialize a new .story/ project in the current directory",
|
|
6733
|
+
inputSchema: {
|
|
6734
|
+
name: z10.string().describe("Project name"),
|
|
6735
|
+
type: z10.string().optional().describe("Project type (e.g. npm, macapp, cargo, generic)"),
|
|
6736
|
+
language: z10.string().optional().describe("Primary language (e.g. typescript, swift, rust)")
|
|
6737
|
+
}
|
|
6738
|
+
}, async (args) => {
|
|
6739
|
+
let result;
|
|
6740
|
+
try {
|
|
6741
|
+
const projectRoot = realpathSync2(process.cwd());
|
|
6742
|
+
result = await initProject(projectRoot, {
|
|
6743
|
+
name: args.name,
|
|
6744
|
+
type: args.type,
|
|
6745
|
+
language: args.language,
|
|
6746
|
+
phases: []
|
|
6747
|
+
});
|
|
6748
|
+
} catch (err) {
|
|
6749
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
6750
|
+
return { content: [{ type: "text", text: `[init_error] ${msg}` }], isError: true };
|
|
6751
|
+
}
|
|
6752
|
+
try {
|
|
6753
|
+
degradedStatus.remove();
|
|
6754
|
+
degradedInit.remove();
|
|
6755
|
+
registerAllTools(server, result.root);
|
|
6756
|
+
} catch (swapErr) {
|
|
6757
|
+
process.stderr.write(`claudestory: tool-swap failed after init: ${swapErr instanceof Error ? swapErr.message : String(swapErr)}
|
|
6758
|
+
`);
|
|
6759
|
+
try {
|
|
6760
|
+
registerDegradedTools(server);
|
|
6761
|
+
} catch {
|
|
6762
|
+
}
|
|
6763
|
+
return { content: [{ type: "text", text: `Initialized .story/ project "${args.name}" at ${result.root}
|
|
6764
|
+
|
|
6765
|
+
Warning: tool registration failed. Restart the MCP server for full tool access.` }] };
|
|
6766
|
+
}
|
|
6767
|
+
process.stderr.write(`claudestory: initialized at ${result.root}
|
|
6768
|
+
`);
|
|
6769
|
+
const lines = [
|
|
6770
|
+
`Initialized .story/ project "${args.name}" at ${result.root}`,
|
|
6771
|
+
`Created: ${result.created.join(", ")}`
|
|
6772
|
+
];
|
|
6773
|
+
if (result.warnings.length > 0) {
|
|
6774
|
+
lines.push(`Warnings: ${result.warnings.join("; ")}`);
|
|
6775
|
+
}
|
|
6776
|
+
lines.push("", "All claudestory tools are now available. Use claudestory_phase_create to add phases and claudestory_ticket_create to add tickets.");
|
|
6777
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
6778
|
+
});
|
|
6779
|
+
}
|
|
6780
|
+
async function main() {
|
|
6781
|
+
const root = tryDiscoverRoot();
|
|
6782
|
+
const server = new McpServer(
|
|
6783
|
+
{ name: "claudestory", version },
|
|
6784
|
+
{
|
|
6785
|
+
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/."
|
|
6786
|
+
}
|
|
6787
|
+
);
|
|
6788
|
+
if (root) {
|
|
6789
|
+
registerAllTools(server, root);
|
|
6790
|
+
process.stderr.write(`claudestory MCP server running (root: ${root})
|
|
6791
|
+
`);
|
|
6792
|
+
} else {
|
|
6793
|
+
registerDegradedTools(server);
|
|
6794
|
+
process.stderr.write("claudestory MCP server running (no project \u2014 claudestory_init available)\n");
|
|
6795
|
+
}
|
|
6796
|
+
const transport = new StdioServerTransport();
|
|
6797
|
+
await server.connect(transport);
|
|
6798
|
+
}
|
|
6799
|
+
var ENV_VAR2, CONFIG_PATH2, version;
|
|
6800
|
+
var init_mcp = __esm({
|
|
6801
|
+
"src/mcp/index.ts"() {
|
|
6802
|
+
"use strict";
|
|
6803
|
+
init_esm_shims();
|
|
6804
|
+
init_project_root_discovery();
|
|
6805
|
+
init_tools();
|
|
6806
|
+
init_init();
|
|
6807
|
+
ENV_VAR2 = "CLAUDESTORY_PROJECT_ROOT";
|
|
6808
|
+
CONFIG_PATH2 = ".story/config.json";
|
|
6809
|
+
version = "0.1.12";
|
|
6810
|
+
main().catch((err) => {
|
|
6811
|
+
process.stderr.write(`Fatal: ${err instanceof Error ? err.message : String(err)}
|
|
6812
|
+
`);
|
|
6813
|
+
process.exit(1);
|
|
6814
|
+
});
|
|
4837
6815
|
}
|
|
4838
6816
|
});
|
|
4839
6817
|
|
|
@@ -4864,7 +6842,7 @@ __export(run_exports, {
|
|
|
4864
6842
|
runReadCommand: () => runReadCommand,
|
|
4865
6843
|
writeOutput: () => writeOutput
|
|
4866
6844
|
});
|
|
4867
|
-
import { join as
|
|
6845
|
+
import { join as join12 } from "path";
|
|
4868
6846
|
function writeOutput(text) {
|
|
4869
6847
|
try {
|
|
4870
6848
|
process.stdout.write(text + "\n");
|
|
@@ -4892,7 +6870,7 @@ async function runReadCommand(format, handler) {
|
|
|
4892
6870
|
return;
|
|
4893
6871
|
}
|
|
4894
6872
|
const { state, warnings } = await loadProject(root);
|
|
4895
|
-
const handoversDir =
|
|
6873
|
+
const handoversDir = join12(root, ".story", "handovers");
|
|
4896
6874
|
const result = await handler({ state, warnings, root, handoversDir, format });
|
|
4897
6875
|
writeOutput(result.output);
|
|
4898
6876
|
let exitCode = result.exitCode ?? ExitCode.OK;
|
|
@@ -4927,7 +6905,7 @@ async function runDeleteCommand(format, force, handler) {
|
|
|
4927
6905
|
return;
|
|
4928
6906
|
}
|
|
4929
6907
|
const { state, warnings } = await loadProject(root);
|
|
4930
|
-
const handoversDir =
|
|
6908
|
+
const handoversDir = join12(root, ".story", "handovers");
|
|
4931
6909
|
if (!force && hasIntegrityWarnings(warnings)) {
|
|
4932
6910
|
writeOutput(
|
|
4933
6911
|
formatError(
|
|
@@ -5106,8 +7084,8 @@ var init_reference = __esm({
|
|
|
5106
7084
|
{
|
|
5107
7085
|
name: "ticket update",
|
|
5108
7086
|
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"]
|
|
7087
|
+
usage: "claudestory ticket update <id> [--status <s>] [--title <t>] [--type <type>] [--phase <p>] [--order <n>] [--description <d>] [--blocked-by <ids>] [--parent-ticket <id>] [--format json|md]",
|
|
7088
|
+
flags: ["--status", "--title", "--type", "--phase", "--order", "--description", "--blocked-by", "--parent-ticket"]
|
|
5111
7089
|
},
|
|
5112
7090
|
{
|
|
5113
7091
|
name: "ticket delete",
|
|
@@ -5129,14 +7107,14 @@ var init_reference = __esm({
|
|
|
5129
7107
|
{
|
|
5130
7108
|
name: "issue create",
|
|
5131
7109
|
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"]
|
|
7110
|
+
usage: "claudestory issue create --title <t> --severity <s> --impact <i> [--components <c>] [--related-tickets <ids>] [--location <locs>] [--phase <p>] [--format json|md]",
|
|
7111
|
+
flags: ["--title", "--severity", "--impact", "--components", "--related-tickets", "--location", "--phase"]
|
|
5134
7112
|
},
|
|
5135
7113
|
{
|
|
5136
7114
|
name: "issue update",
|
|
5137
7115
|
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"]
|
|
7116
|
+
usage: "claudestory issue update <id> [--status <s>] [--title <t>] [--severity <sev>] [--impact <i>] [--resolution <r>] [--components <c>] [--related-tickets <ids>] [--location <locs>] [--order <n>] [--phase <p>] [--format json|md]",
|
|
7117
|
+
flags: ["--status", "--title", "--severity", "--impact", "--resolution", "--components", "--related-tickets", "--location", "--order", "--phase"]
|
|
5140
7118
|
},
|
|
5141
7119
|
{
|
|
5142
7120
|
name: "issue delete",
|
|
@@ -5281,6 +7259,11 @@ var init_reference = __esm({
|
|
|
5281
7259
|
description: "Print CLI command and MCP tool reference",
|
|
5282
7260
|
usage: "claudestory reference [--format json|md]"
|
|
5283
7261
|
},
|
|
7262
|
+
{
|
|
7263
|
+
name: "selftest",
|
|
7264
|
+
description: "Run integration smoke test \u2014 create/update/delete cycle across all entity types",
|
|
7265
|
+
usage: "claudestory selftest [--format json|md]"
|
|
7266
|
+
},
|
|
5284
7267
|
{
|
|
5285
7268
|
name: "setup-skill",
|
|
5286
7269
|
description: "Install the /story skill globally for Claude Code",
|
|
@@ -5296,7 +7279,7 @@ var init_reference = __esm({
|
|
|
5296
7279
|
{ name: "claudestory_ticket_get", description: "Get a ticket by ID", params: ["id"] },
|
|
5297
7280
|
{ name: "claudestory_ticket_next", description: "Highest-priority unblocked ticket(s)", params: ["count?"] },
|
|
5298
7281
|
{ name: "claudestory_ticket_blocked", description: "All blocked tickets with dependencies" },
|
|
5299
|
-
{ name: "claudestory_issue_list", description: "List issues with optional filters", params: ["status?", "severity?"] },
|
|
7282
|
+
{ name: "claudestory_issue_list", description: "List issues with optional filters", params: ["status?", "severity?", "component?"] },
|
|
5300
7283
|
{ name: "claudestory_issue_get", description: "Get an issue by ID", params: ["id"] },
|
|
5301
7284
|
{ name: "claudestory_handover_list", description: "List handover filenames (newest first)" },
|
|
5302
7285
|
{ name: "claudestory_handover_latest", description: "Content of most recent handover" },
|
|
@@ -5313,9 +7296,11 @@ var init_reference = __esm({
|
|
|
5313
7296
|
{ name: "claudestory_note_create", description: "Create note", params: ["content", "title?", "tags?"] },
|
|
5314
7297
|
{ name: "claudestory_note_update", description: "Update note", params: ["id", "content?", "title?", "tags?", "status?"] },
|
|
5315
7298
|
{ 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?"] },
|
|
7299
|
+
{ name: "claudestory_ticket_update", description: "Update ticket", params: ["id", "status?", "title?", "type?", "order?", "description?", "phase?", "parentTicket?", "blockedBy?"] },
|
|
5317
7300
|
{ 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?"] }
|
|
7301
|
+
{ name: "claudestory_issue_update", description: "Update issue", params: ["id", "status?", "title?", "severity?", "impact?", "resolution?", "components?", "relatedTickets?", "location?", "order?", "phase?"] },
|
|
7302
|
+
{ name: "claudestory_phase_create", description: "Create phase in roadmap", params: ["id", "name", "label", "description", "summary?", "after?", "atStart?"] },
|
|
7303
|
+
{ name: "claudestory_selftest", description: "Integration smoke test \u2014 create/update/delete cycle" }
|
|
5319
7304
|
];
|
|
5320
7305
|
}
|
|
5321
7306
|
});
|
|
@@ -5325,11 +7310,12 @@ var setup_skill_exports = {};
|
|
|
5325
7310
|
__export(setup_skill_exports, {
|
|
5326
7311
|
handleSetupSkill: () => handleSetupSkill,
|
|
5327
7312
|
registerPreCompactHook: () => registerPreCompactHook,
|
|
7313
|
+
registerStopHook: () => registerStopHook,
|
|
5328
7314
|
resolveSkillSourceDir: () => resolveSkillSourceDir
|
|
5329
7315
|
});
|
|
5330
|
-
import { mkdir as mkdir5, writeFile as
|
|
5331
|
-
import { existsSync as
|
|
5332
|
-
import { join as
|
|
7316
|
+
import { mkdir as mkdir5, writeFile as writeFile3, readFile as readFile5, rm, rename as rename2, unlink as unlink3 } from "fs/promises";
|
|
7317
|
+
import { existsSync as existsSync9 } from "fs";
|
|
7318
|
+
import { join as join13, dirname as dirname3 } from "path";
|
|
5333
7319
|
import { homedir } from "os";
|
|
5334
7320
|
import { execFileSync } from "child_process";
|
|
5335
7321
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
@@ -5338,27 +7324,27 @@ function log(msg) {
|
|
|
5338
7324
|
}
|
|
5339
7325
|
function resolveSkillSourceDir() {
|
|
5340
7326
|
const thisDir = dirname3(fileURLToPath2(import.meta.url));
|
|
5341
|
-
const bundledPath =
|
|
5342
|
-
if (
|
|
5343
|
-
const sourcePath =
|
|
5344
|
-
if (
|
|
7327
|
+
const bundledPath = join13(thisDir, "..", "src", "skill");
|
|
7328
|
+
if (existsSync9(join13(bundledPath, "SKILL.md"))) return bundledPath;
|
|
7329
|
+
const sourcePath = join13(thisDir, "..", "..", "skill");
|
|
7330
|
+
if (existsSync9(join13(sourcePath, "SKILL.md"))) return sourcePath;
|
|
5345
7331
|
throw new Error(
|
|
5346
7332
|
`Cannot find bundled skill files. Checked:
|
|
5347
7333
|
${bundledPath}
|
|
5348
7334
|
${sourcePath}`
|
|
5349
7335
|
);
|
|
5350
7336
|
}
|
|
5351
|
-
function
|
|
7337
|
+
function isHookWithCommand(entry, command) {
|
|
5352
7338
|
if (typeof entry !== "object" || entry === null) return false;
|
|
5353
7339
|
const e = entry;
|
|
5354
|
-
return e.type === "command" && typeof e.command === "string" && e.command.trim() ===
|
|
7340
|
+
return e.type === "command" && typeof e.command === "string" && e.command.trim() === command;
|
|
5355
7341
|
}
|
|
5356
|
-
async function
|
|
5357
|
-
const path2 = settingsPath ??
|
|
7342
|
+
async function registerHook(hookType, hookEntry, settingsPath) {
|
|
7343
|
+
const path2 = settingsPath ?? join13(homedir(), ".claude", "settings.json");
|
|
5358
7344
|
let raw = "{}";
|
|
5359
|
-
if (
|
|
7345
|
+
if (existsSync9(path2)) {
|
|
5360
7346
|
try {
|
|
5361
|
-
raw = await
|
|
7347
|
+
raw = await readFile5(path2, "utf-8");
|
|
5362
7348
|
} catch {
|
|
5363
7349
|
process.stderr.write(`Could not read ${path2} \u2014 skipping hook registration.
|
|
5364
7350
|
`);
|
|
@@ -5389,43 +7375,45 @@ async function registerPreCompactHook(settingsPath) {
|
|
|
5389
7375
|
settings.hooks = {};
|
|
5390
7376
|
}
|
|
5391
7377
|
const hooks = settings.hooks;
|
|
5392
|
-
if (
|
|
5393
|
-
if (!Array.isArray(hooks
|
|
5394
|
-
process.stderr.write(`${path2} has unexpected hooks
|
|
7378
|
+
if (hookType in hooks) {
|
|
7379
|
+
if (!Array.isArray(hooks[hookType])) {
|
|
7380
|
+
process.stderr.write(`${path2} has unexpected hooks.${hookType} format \u2014 skipping hook registration.
|
|
5395
7381
|
`);
|
|
5396
7382
|
return "skipped";
|
|
5397
7383
|
}
|
|
5398
7384
|
} else {
|
|
5399
|
-
hooks
|
|
5400
|
-
}
|
|
5401
|
-
const
|
|
5402
|
-
|
|
5403
|
-
|
|
5404
|
-
const
|
|
5405
|
-
|
|
5406
|
-
|
|
5407
|
-
if (
|
|
7385
|
+
hooks[hookType] = [];
|
|
7386
|
+
}
|
|
7387
|
+
const hookArray = hooks[hookType];
|
|
7388
|
+
const hookCommand = hookEntry.command;
|
|
7389
|
+
if (hookCommand) {
|
|
7390
|
+
for (const group of hookArray) {
|
|
7391
|
+
if (typeof group !== "object" || group === null) continue;
|
|
7392
|
+
const g = group;
|
|
7393
|
+
if (!Array.isArray(g.hooks)) continue;
|
|
7394
|
+
for (const entry of g.hooks) {
|
|
7395
|
+
if (isHookWithCommand(entry, hookCommand)) return "exists";
|
|
7396
|
+
}
|
|
5408
7397
|
}
|
|
5409
7398
|
}
|
|
5410
|
-
const ourEntry = { type: "command", command: HOOK_COMMAND };
|
|
5411
7399
|
let appended = false;
|
|
5412
|
-
for (const group of
|
|
7400
|
+
for (const group of hookArray) {
|
|
5413
7401
|
if (typeof group !== "object" || group === null) continue;
|
|
5414
7402
|
const g = group;
|
|
5415
7403
|
if (g.matcher === "" && Array.isArray(g.hooks)) {
|
|
5416
|
-
g.hooks.push(
|
|
7404
|
+
g.hooks.push(hookEntry);
|
|
5417
7405
|
appended = true;
|
|
5418
7406
|
break;
|
|
5419
7407
|
}
|
|
5420
7408
|
}
|
|
5421
7409
|
if (!appended) {
|
|
5422
|
-
|
|
7410
|
+
hookArray.push({ matcher: "", hooks: [hookEntry] });
|
|
5423
7411
|
}
|
|
5424
7412
|
const tmpPath = `${path2}.${process.pid}.tmp`;
|
|
5425
7413
|
try {
|
|
5426
7414
|
const dir = dirname3(path2);
|
|
5427
7415
|
await mkdir5(dir, { recursive: true });
|
|
5428
|
-
await
|
|
7416
|
+
await writeFile3(tmpPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
5429
7417
|
await rename2(tmpPath, path2);
|
|
5430
7418
|
} catch (err) {
|
|
5431
7419
|
try {
|
|
@@ -5439,9 +7427,15 @@ async function registerPreCompactHook(settingsPath) {
|
|
|
5439
7427
|
}
|
|
5440
7428
|
return "registered";
|
|
5441
7429
|
}
|
|
7430
|
+
async function registerPreCompactHook(settingsPath) {
|
|
7431
|
+
return registerHook("PreCompact", { type: "command", command: PRECOMPACT_HOOK_COMMAND }, settingsPath);
|
|
7432
|
+
}
|
|
7433
|
+
async function registerStopHook(settingsPath) {
|
|
7434
|
+
return registerHook("Stop", { type: "command", command: STOP_HOOK_COMMAND, async: true }, settingsPath);
|
|
7435
|
+
}
|
|
5442
7436
|
async function handleSetupSkill(options = {}) {
|
|
5443
7437
|
const { skipHooks = false } = options;
|
|
5444
|
-
const skillDir =
|
|
7438
|
+
const skillDir = join13(homedir(), ".claude", "skills", "story");
|
|
5445
7439
|
await mkdir5(skillDir, { recursive: true });
|
|
5446
7440
|
let srcSkillDir;
|
|
5447
7441
|
try {
|
|
@@ -5454,19 +7448,19 @@ async function handleSetupSkill(options = {}) {
|
|
|
5454
7448
|
process.exitCode = 1;
|
|
5455
7449
|
return;
|
|
5456
7450
|
}
|
|
5457
|
-
const oldPrimeDir =
|
|
5458
|
-
if (
|
|
7451
|
+
const oldPrimeDir = join13(homedir(), ".claude", "skills", "prime");
|
|
7452
|
+
if (existsSync9(oldPrimeDir)) {
|
|
5459
7453
|
await rm(oldPrimeDir, { recursive: true, force: true });
|
|
5460
7454
|
log("Removed old /prime skill (migrated to /story)");
|
|
5461
7455
|
}
|
|
5462
|
-
const existed =
|
|
5463
|
-
const skillContent = await
|
|
5464
|
-
await
|
|
7456
|
+
const existed = existsSync9(join13(skillDir, "SKILL.md"));
|
|
7457
|
+
const skillContent = await readFile5(join13(srcSkillDir, "SKILL.md"), "utf-8");
|
|
7458
|
+
await writeFile3(join13(skillDir, "SKILL.md"), skillContent, "utf-8");
|
|
5465
7459
|
let referenceWritten = false;
|
|
5466
|
-
const refSrcPath =
|
|
5467
|
-
if (
|
|
5468
|
-
const refContent = await
|
|
5469
|
-
await
|
|
7460
|
+
const refSrcPath = join13(srcSkillDir, "reference.md");
|
|
7461
|
+
if (existsSync9(refSrcPath)) {
|
|
7462
|
+
const refContent = await readFile5(refSrcPath, "utf-8");
|
|
7463
|
+
await writeFile3(join13(skillDir, "reference.md"), refContent, "utf-8");
|
|
5470
7464
|
referenceWritten = true;
|
|
5471
7465
|
}
|
|
5472
7466
|
log(`${existed ? "Updated" : "Installed"} /story skill at ${skillDir}/`);
|
|
@@ -5530,6 +7524,21 @@ async function handleSetupSkill(options = {}) {
|
|
|
5530
7524
|
} else if (skipHooks) {
|
|
5531
7525
|
log(" Hook registration skipped (--skip-hooks)");
|
|
5532
7526
|
}
|
|
7527
|
+
if (cliInPath && !skipHooks) {
|
|
7528
|
+
const stopResult = await registerStopHook();
|
|
7529
|
+
switch (stopResult) {
|
|
7530
|
+
case "registered":
|
|
7531
|
+
log(" Stop hook registered \u2014 status.json updated after every Claude response");
|
|
7532
|
+
break;
|
|
7533
|
+
case "exists":
|
|
7534
|
+
log(" Stop hook already configured");
|
|
7535
|
+
break;
|
|
7536
|
+
case "skipped":
|
|
7537
|
+
break;
|
|
7538
|
+
}
|
|
7539
|
+
} else if (!cliInPath) {
|
|
7540
|
+
} else if (skipHooks) {
|
|
7541
|
+
}
|
|
5533
7542
|
log("");
|
|
5534
7543
|
if (mcpRegistered) {
|
|
5535
7544
|
log("Done! Restart Claude Code, then type /story in any project.");
|
|
@@ -5537,12 +7546,142 @@ async function handleSetupSkill(options = {}) {
|
|
|
5537
7546
|
log("Skill installed. After registering MCP, restart Claude Code and type /story.");
|
|
5538
7547
|
}
|
|
5539
7548
|
}
|
|
5540
|
-
var
|
|
7549
|
+
var PRECOMPACT_HOOK_COMMAND, STOP_HOOK_COMMAND;
|
|
5541
7550
|
var init_setup_skill = __esm({
|
|
5542
7551
|
"src/cli/commands/setup-skill.ts"() {
|
|
5543
7552
|
"use strict";
|
|
5544
7553
|
init_esm_shims();
|
|
5545
|
-
|
|
7554
|
+
PRECOMPACT_HOOK_COMMAND = "claudestory snapshot --quiet";
|
|
7555
|
+
STOP_HOOK_COMMAND = "claudestory hook-status";
|
|
7556
|
+
}
|
|
7557
|
+
});
|
|
7558
|
+
|
|
7559
|
+
// src/cli/commands/hook-status.ts
|
|
7560
|
+
var hook_status_exports = {};
|
|
7561
|
+
__export(hook_status_exports, {
|
|
7562
|
+
handleHookStatus: () => handleHookStatus
|
|
7563
|
+
});
|
|
7564
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, renameSync as renameSync2, unlinkSync as unlinkSync2 } from "fs";
|
|
7565
|
+
import { join as join14 } from "path";
|
|
7566
|
+
async function readStdinSilent() {
|
|
7567
|
+
try {
|
|
7568
|
+
const chunks = [];
|
|
7569
|
+
for await (const chunk of process.stdin) {
|
|
7570
|
+
chunks.push(chunk);
|
|
7571
|
+
}
|
|
7572
|
+
return Buffer.concat(
|
|
7573
|
+
chunks.map((c) => Buffer.isBuffer(c) ? c : Buffer.from(c))
|
|
7574
|
+
).toString("utf-8");
|
|
7575
|
+
} catch {
|
|
7576
|
+
return null;
|
|
7577
|
+
}
|
|
7578
|
+
}
|
|
7579
|
+
function atomicWriteSync(targetPath, content) {
|
|
7580
|
+
const tmp = `${targetPath}.${process.pid}.tmp`;
|
|
7581
|
+
try {
|
|
7582
|
+
writeFileSync3(tmp, content, "utf-8");
|
|
7583
|
+
renameSync2(tmp, targetPath);
|
|
7584
|
+
return true;
|
|
7585
|
+
} catch {
|
|
7586
|
+
try {
|
|
7587
|
+
unlinkSync2(tmp);
|
|
7588
|
+
} catch {
|
|
7589
|
+
}
|
|
7590
|
+
return false;
|
|
7591
|
+
}
|
|
7592
|
+
}
|
|
7593
|
+
function inactivePayload() {
|
|
7594
|
+
return { schemaVersion: CURRENT_STATUS_SCHEMA_VERSION, sessionActive: false, source: "hook" };
|
|
7595
|
+
}
|
|
7596
|
+
function activePayload(session) {
|
|
7597
|
+
return {
|
|
7598
|
+
schemaVersion: CURRENT_STATUS_SCHEMA_VERSION,
|
|
7599
|
+
sessionActive: true,
|
|
7600
|
+
sessionId: session.sessionId,
|
|
7601
|
+
state: session.state,
|
|
7602
|
+
ticket: session.ticket?.id ?? null,
|
|
7603
|
+
ticketTitle: session.ticket?.title ?? null,
|
|
7604
|
+
risk: session.ticket?.risk ?? null,
|
|
7605
|
+
claudeStatus: deriveClaudeStatus(session.state, session.waitingForRetry),
|
|
7606
|
+
observedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7607
|
+
lastGuideCall: session.lastGuideCall ?? null,
|
|
7608
|
+
completedThisSession: session.completedTickets?.map((t) => t.id) ?? [],
|
|
7609
|
+
contextPressure: session.contextPressure?.level ?? "unknown",
|
|
7610
|
+
branch: session.git?.branch ?? null,
|
|
7611
|
+
source: "hook"
|
|
7612
|
+
};
|
|
7613
|
+
}
|
|
7614
|
+
function ensureGitignore(root) {
|
|
7615
|
+
const gitignorePath = join14(root, ".story", ".gitignore");
|
|
7616
|
+
let existing = "";
|
|
7617
|
+
try {
|
|
7618
|
+
existing = readFileSync3(gitignorePath, "utf-8");
|
|
7619
|
+
} catch {
|
|
7620
|
+
}
|
|
7621
|
+
const lines = existing.split("\n").map((l) => l.trim());
|
|
7622
|
+
const missing = STORY_GITIGNORE_ENTRIES.filter((e) => !lines.includes(e));
|
|
7623
|
+
if (missing.length === 0) return;
|
|
7624
|
+
let content = existing;
|
|
7625
|
+
if (content.length > 0 && !content.endsWith("\n")) content += "\n";
|
|
7626
|
+
content += missing.join("\n") + "\n";
|
|
7627
|
+
try {
|
|
7628
|
+
writeFileSync3(gitignorePath, content, "utf-8");
|
|
7629
|
+
} catch {
|
|
7630
|
+
}
|
|
7631
|
+
}
|
|
7632
|
+
function writeStatus(root, payload) {
|
|
7633
|
+
ensureGitignore(root);
|
|
7634
|
+
const statusPath = join14(root, ".story", "status.json");
|
|
7635
|
+
const content = JSON.stringify(payload, null, 2) + "\n";
|
|
7636
|
+
atomicWriteSync(statusPath, content);
|
|
7637
|
+
}
|
|
7638
|
+
async function handleHookStatus() {
|
|
7639
|
+
try {
|
|
7640
|
+
if (process.stdin.isTTY) {
|
|
7641
|
+
const root2 = discoverProjectRoot();
|
|
7642
|
+
if (root2) {
|
|
7643
|
+
const session2 = findActiveSessionMinimal(root2);
|
|
7644
|
+
const payload2 = session2 ? activePayload(session2) : inactivePayload();
|
|
7645
|
+
writeStatus(root2, payload2);
|
|
7646
|
+
}
|
|
7647
|
+
process.exit(0);
|
|
7648
|
+
}
|
|
7649
|
+
const raw = await readStdinSilent();
|
|
7650
|
+
if (raw === null || raw === "") {
|
|
7651
|
+
process.exit(0);
|
|
7652
|
+
}
|
|
7653
|
+
let input;
|
|
7654
|
+
try {
|
|
7655
|
+
input = JSON.parse(raw);
|
|
7656
|
+
} catch {
|
|
7657
|
+
process.exit(0);
|
|
7658
|
+
}
|
|
7659
|
+
if (input.stop_hook_active === true) {
|
|
7660
|
+
process.exit(0);
|
|
7661
|
+
}
|
|
7662
|
+
const cwd = input.cwd;
|
|
7663
|
+
if (typeof cwd !== "string" || !cwd) {
|
|
7664
|
+
process.exit(0);
|
|
7665
|
+
}
|
|
7666
|
+
const root = discoverProjectRoot(cwd);
|
|
7667
|
+
if (!root) {
|
|
7668
|
+
process.exit(0);
|
|
7669
|
+
}
|
|
7670
|
+
const session = findActiveSessionMinimal(root);
|
|
7671
|
+
const payload = session ? activePayload(session) : inactivePayload();
|
|
7672
|
+
writeStatus(root, payload);
|
|
7673
|
+
} catch {
|
|
7674
|
+
}
|
|
7675
|
+
process.exit(0);
|
|
7676
|
+
}
|
|
7677
|
+
var init_hook_status = __esm({
|
|
7678
|
+
"src/cli/commands/hook-status.ts"() {
|
|
7679
|
+
"use strict";
|
|
7680
|
+
init_esm_shims();
|
|
7681
|
+
init_project_root_discovery();
|
|
7682
|
+
init_init();
|
|
7683
|
+
init_session_types();
|
|
7684
|
+
init_session();
|
|
5546
7685
|
}
|
|
5547
7686
|
});
|
|
5548
7687
|
|
|
@@ -5552,6 +7691,7 @@ __export(register_exports, {
|
|
|
5552
7691
|
registerBlockerCommand: () => registerBlockerCommand,
|
|
5553
7692
|
registerExportCommand: () => registerExportCommand,
|
|
5554
7693
|
registerHandoverCommand: () => registerHandoverCommand,
|
|
7694
|
+
registerHookStatusCommand: () => registerHookStatusCommand,
|
|
5555
7695
|
registerInitCommand: () => registerInitCommand,
|
|
5556
7696
|
registerIssueCommand: () => registerIssueCommand,
|
|
5557
7697
|
registerNoteCommand: () => registerNoteCommand,
|
|
@@ -5559,6 +7699,7 @@ __export(register_exports, {
|
|
|
5559
7699
|
registerRecapCommand: () => registerRecapCommand,
|
|
5560
7700
|
registerRecommendCommand: () => registerRecommendCommand,
|
|
5561
7701
|
registerReferenceCommand: () => registerReferenceCommand,
|
|
7702
|
+
registerSelftestCommand: () => registerSelftestCommand,
|
|
5562
7703
|
registerSetupSkillCommand: () => registerSetupSkillCommand,
|
|
5563
7704
|
registerSnapshotCommand: () => registerSnapshotCommand,
|
|
5564
7705
|
registerStatusCommand: () => registerStatusCommand,
|
|
@@ -6007,6 +8148,9 @@ function registerTicketCommand(yargs) {
|
|
|
6007
8148
|
}).option("title", {
|
|
6008
8149
|
type: "string",
|
|
6009
8150
|
describe: "New title"
|
|
8151
|
+
}).option("type", {
|
|
8152
|
+
type: "string",
|
|
8153
|
+
describe: "New type"
|
|
6010
8154
|
}).option("phase", {
|
|
6011
8155
|
type: "string",
|
|
6012
8156
|
describe: "New phase ID"
|
|
@@ -6053,6 +8197,7 @@ function registerTicketCommand(yargs) {
|
|
|
6053
8197
|
{
|
|
6054
8198
|
status: argv.status,
|
|
6055
8199
|
title: argv.title,
|
|
8200
|
+
type: argv.type,
|
|
6056
8201
|
phase: argv.phase === "" ? null : argv.phase,
|
|
6057
8202
|
order: argv.order,
|
|
6058
8203
|
description,
|
|
@@ -6234,7 +8379,7 @@ function registerIssueCommand(yargs) {
|
|
|
6234
8379
|
location: normalizeArrayOption(
|
|
6235
8380
|
argv.location
|
|
6236
8381
|
),
|
|
6237
|
-
phase: argv.phase
|
|
8382
|
+
phase: argv.phase === "" ? void 0 : argv.phase
|
|
6238
8383
|
},
|
|
6239
8384
|
format,
|
|
6240
8385
|
root
|
|
@@ -6296,6 +8441,12 @@ function registerIssueCommand(yargs) {
|
|
|
6296
8441
|
type: "string",
|
|
6297
8442
|
array: true,
|
|
6298
8443
|
describe: "File locations"
|
|
8444
|
+
}).option("order", {
|
|
8445
|
+
type: "number",
|
|
8446
|
+
describe: "New sort order"
|
|
8447
|
+
}).option("phase", {
|
|
8448
|
+
type: "string",
|
|
8449
|
+
describe: "New phase ID"
|
|
6299
8450
|
}).conflicts("impact", "stdin")
|
|
6300
8451
|
),
|
|
6301
8452
|
async (argv) => {
|
|
@@ -6328,7 +8479,9 @@ function registerIssueCommand(yargs) {
|
|
|
6328
8479
|
resolution: argv.resolution === "" ? null : argv.resolution,
|
|
6329
8480
|
components: argv.components ? normalizeArrayOption(argv.components) : void 0,
|
|
6330
8481
|
relatedTickets: argv["related-tickets"] ? normalizeArrayOption(argv["related-tickets"]) : void 0,
|
|
6331
|
-
location: argv.location ? normalizeArrayOption(argv.location) : void 0
|
|
8482
|
+
location: argv.location ? normalizeArrayOption(argv.location) : void 0,
|
|
8483
|
+
order: argv.order,
|
|
8484
|
+
phase: argv.phase === "" ? null : argv.phase
|
|
6332
8485
|
},
|
|
6333
8486
|
format,
|
|
6334
8487
|
root
|
|
@@ -6921,7 +9074,6 @@ function registerNoteCommand(yargs) {
|
|
|
6921
9074
|
describe: "New tags (replaces existing)"
|
|
6922
9075
|
}).option("clear-tags", {
|
|
6923
9076
|
type: "boolean",
|
|
6924
|
-
default: false,
|
|
6925
9077
|
describe: "Clear all tags"
|
|
6926
9078
|
}).option("status", {
|
|
6927
9079
|
type: "string",
|
|
@@ -7037,6 +9189,44 @@ function registerRecommendCommand(yargs) {
|
|
|
7037
9189
|
}
|
|
7038
9190
|
);
|
|
7039
9191
|
}
|
|
9192
|
+
function registerSelftestCommand(yargs) {
|
|
9193
|
+
return yargs.command(
|
|
9194
|
+
"selftest",
|
|
9195
|
+
"Run integration smoke test \u2014 create/update/delete cycle across all entity types",
|
|
9196
|
+
(y) => addFormatOption(y),
|
|
9197
|
+
async (argv) => {
|
|
9198
|
+
const format = parseOutputFormat(argv.format);
|
|
9199
|
+
const root = (await Promise.resolve().then(() => (init_project_root_discovery(), project_root_discovery_exports))).discoverProjectRoot();
|
|
9200
|
+
if (!root) {
|
|
9201
|
+
writeOutput(
|
|
9202
|
+
formatError("not_found", "No .story/ project found.", format)
|
|
9203
|
+
);
|
|
9204
|
+
process.exitCode = ExitCode.USER_ERROR;
|
|
9205
|
+
return;
|
|
9206
|
+
}
|
|
9207
|
+
try {
|
|
9208
|
+
const result = await handleSelftest(root, format);
|
|
9209
|
+
writeOutput(result.output);
|
|
9210
|
+
process.exitCode = result.exitCode ?? ExitCode.OK;
|
|
9211
|
+
} catch (err) {
|
|
9212
|
+
if (err instanceof CliValidationError) {
|
|
9213
|
+
writeOutput(formatError(err.code, err.message, format));
|
|
9214
|
+
process.exitCode = ExitCode.USER_ERROR;
|
|
9215
|
+
return;
|
|
9216
|
+
}
|
|
9217
|
+
const { ProjectLoaderError: ProjectLoaderError2 } = await Promise.resolve().then(() => (init_errors(), errors_exports));
|
|
9218
|
+
if (err instanceof ProjectLoaderError2) {
|
|
9219
|
+
writeOutput(formatError(err.code, err.message, format));
|
|
9220
|
+
process.exitCode = ExitCode.USER_ERROR;
|
|
9221
|
+
return;
|
|
9222
|
+
}
|
|
9223
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
9224
|
+
writeOutput(formatError("io_error", message, format));
|
|
9225
|
+
process.exitCode = ExitCode.USER_ERROR;
|
|
9226
|
+
}
|
|
9227
|
+
}
|
|
9228
|
+
);
|
|
9229
|
+
}
|
|
7040
9230
|
function registerSetupSkillCommand(yargs) {
|
|
7041
9231
|
return yargs.command(
|
|
7042
9232
|
"setup-skill",
|
|
@@ -7044,7 +9234,7 @@ function registerSetupSkillCommand(yargs) {
|
|
|
7044
9234
|
(y) => y.option("skip-hooks", {
|
|
7045
9235
|
type: "boolean",
|
|
7046
9236
|
default: false,
|
|
7047
|
-
description: "Skip PreCompact
|
|
9237
|
+
description: "Skip hook registration (PreCompact + Stop)"
|
|
7048
9238
|
}),
|
|
7049
9239
|
async (argv) => {
|
|
7050
9240
|
const { handleSetupSkill: handleSetupSkill2 } = await Promise.resolve().then(() => (init_setup_skill(), setup_skill_exports));
|
|
@@ -7052,6 +9242,18 @@ function registerSetupSkillCommand(yargs) {
|
|
|
7052
9242
|
}
|
|
7053
9243
|
);
|
|
7054
9244
|
}
|
|
9245
|
+
function registerHookStatusCommand(yargs) {
|
|
9246
|
+
return yargs.command(
|
|
9247
|
+
"hook-status",
|
|
9248
|
+
false,
|
|
9249
|
+
// hidden — machine-facing, not shown in --help
|
|
9250
|
+
(y) => y,
|
|
9251
|
+
async () => {
|
|
9252
|
+
const { handleHookStatus: handleHookStatus2 } = await Promise.resolve().then(() => (init_hook_status(), hook_status_exports));
|
|
9253
|
+
await handleHookStatus2();
|
|
9254
|
+
}
|
|
9255
|
+
);
|
|
9256
|
+
}
|
|
7055
9257
|
var init_register = __esm({
|
|
7056
9258
|
"src/cli/register.ts"() {
|
|
7057
9259
|
"use strict";
|
|
@@ -7073,6 +9275,7 @@ var init_register = __esm({
|
|
|
7073
9275
|
init_export();
|
|
7074
9276
|
init_snapshot2();
|
|
7075
9277
|
init_reference();
|
|
9278
|
+
init_selftest();
|
|
7076
9279
|
}
|
|
7077
9280
|
});
|
|
7078
9281
|
|
|
@@ -7103,9 +9306,11 @@ async function runCli() {
|
|
|
7103
9306
|
registerNoteCommand: registerNoteCommand2,
|
|
7104
9307
|
registerRecommendCommand: registerRecommendCommand2,
|
|
7105
9308
|
registerReferenceCommand: registerReferenceCommand2,
|
|
7106
|
-
|
|
9309
|
+
registerSelftestCommand: registerSelftestCommand2,
|
|
9310
|
+
registerSetupSkillCommand: registerSetupSkillCommand2,
|
|
9311
|
+
registerHookStatusCommand: registerHookStatusCommand2
|
|
7107
9312
|
} = await Promise.resolve().then(() => (init_register(), register_exports));
|
|
7108
|
-
const version2 = "0.1.
|
|
9313
|
+
const version2 = "0.1.12";
|
|
7109
9314
|
class HandledError extends Error {
|
|
7110
9315
|
constructor() {
|
|
7111
9316
|
super("HANDLED_ERROR");
|
|
@@ -7141,7 +9346,9 @@ async function runCli() {
|
|
|
7141
9346
|
cli = registerExportCommand2(cli);
|
|
7142
9347
|
cli = registerRecommendCommand2(cli);
|
|
7143
9348
|
cli = registerReferenceCommand2(cli);
|
|
9349
|
+
cli = registerSelftestCommand2(cli);
|
|
7144
9350
|
cli = registerSetupSkillCommand2(cli);
|
|
9351
|
+
cli = registerHookStatusCommand2(cli);
|
|
7145
9352
|
function handleUnexpectedError(err) {
|
|
7146
9353
|
if (err instanceof HandledError) return;
|
|
7147
9354
|
const message = err instanceof Error ? err.message : String(err);
|