@anthropologies/claudestory 0.1.34 → 0.1.36
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 +557 -143
- package/dist/index.js +124 -6
- package/dist/mcp.js +379 -110
- package/package.json +1 -1
- package/dist/cli.d.ts +0 -1
- package/dist/index.d.ts +0 -1842
- package/dist/mcp.d.ts +0 -1
package/dist/cli.js
CHANGED
|
@@ -1761,7 +1761,7 @@ function fencedBlock(content, lang) {
|
|
|
1761
1761
|
${content}
|
|
1762
1762
|
${fence}`;
|
|
1763
1763
|
}
|
|
1764
|
-
function formatStatus(state, format) {
|
|
1764
|
+
function formatStatus(state, format, activeSessions = []) {
|
|
1765
1765
|
const phases = phasesWithStatus(state);
|
|
1766
1766
|
const data = {
|
|
1767
1767
|
project: state.config.project,
|
|
@@ -1781,7 +1781,8 @@ function formatStatus(state, format) {
|
|
|
1781
1781
|
name: p.phase.name,
|
|
1782
1782
|
status: p.status,
|
|
1783
1783
|
leafCount: p.leafCount
|
|
1784
|
-
}))
|
|
1784
|
+
})),
|
|
1785
|
+
...activeSessions.length > 0 ? { activeSessions } : {}
|
|
1785
1786
|
};
|
|
1786
1787
|
if (format === "json") {
|
|
1787
1788
|
return JSON.stringify(successEnvelope(data), null, 2);
|
|
@@ -1803,6 +1804,15 @@ function formatStatus(state, format) {
|
|
|
1803
1804
|
const summary = p.phase.summary ?? truncate(p.phase.description, 80);
|
|
1804
1805
|
lines.push(`${indicator} **${escapeMarkdownInline(p.phase.name)}** (${p.leafCount} tickets) \u2014 ${escapeMarkdownInline(summary)}`);
|
|
1805
1806
|
}
|
|
1807
|
+
if (activeSessions.length > 0) {
|
|
1808
|
+
lines.push("");
|
|
1809
|
+
lines.push("## Active Sessions");
|
|
1810
|
+
lines.push("");
|
|
1811
|
+
for (const s of activeSessions) {
|
|
1812
|
+
const ticket = s.ticketId ? `${s.ticketId}: ${escapeMarkdownInline(s.ticketTitle ?? "")}` : "no ticket";
|
|
1813
|
+
lines.push(`- ${s.sessionId.slice(0, 8)}: ${s.state} -- ${ticket} (${s.mode} mode)`);
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1806
1816
|
if (state.isEmptyScaffold) {
|
|
1807
1817
|
lines.push("");
|
|
1808
1818
|
lines.push(EMPTY_SCAFFOLD_HEADING);
|
|
@@ -2691,15 +2701,71 @@ var init_output_formatter = __esm({
|
|
|
2691
2701
|
}
|
|
2692
2702
|
});
|
|
2693
2703
|
|
|
2704
|
+
// src/core/session-scan.ts
|
|
2705
|
+
import { readdirSync, readFileSync } from "fs";
|
|
2706
|
+
import { join as join4 } from "path";
|
|
2707
|
+
function scanActiveSessions(root) {
|
|
2708
|
+
const sessDir = join4(root, ".story", "sessions");
|
|
2709
|
+
let entries;
|
|
2710
|
+
try {
|
|
2711
|
+
entries = readdirSync(sessDir, { withFileTypes: true });
|
|
2712
|
+
} catch {
|
|
2713
|
+
return [];
|
|
2714
|
+
}
|
|
2715
|
+
const results = [];
|
|
2716
|
+
for (const entry of entries) {
|
|
2717
|
+
if (!entry.isDirectory()) continue;
|
|
2718
|
+
const statePath2 = join4(sessDir, entry.name, "state.json");
|
|
2719
|
+
let raw;
|
|
2720
|
+
try {
|
|
2721
|
+
raw = readFileSync(statePath2, "utf-8");
|
|
2722
|
+
} catch {
|
|
2723
|
+
continue;
|
|
2724
|
+
}
|
|
2725
|
+
let parsed;
|
|
2726
|
+
try {
|
|
2727
|
+
parsed = JSON.parse(raw);
|
|
2728
|
+
} catch {
|
|
2729
|
+
continue;
|
|
2730
|
+
}
|
|
2731
|
+
if (parsed.status !== "active") continue;
|
|
2732
|
+
if (parsed.state === "SESSION_END") continue;
|
|
2733
|
+
const lease = parsed.lease;
|
|
2734
|
+
if (lease?.expiresAt) {
|
|
2735
|
+
const expires = new Date(lease.expiresAt).getTime();
|
|
2736
|
+
if (!Number.isNaN(expires) && expires <= Date.now()) continue;
|
|
2737
|
+
} else {
|
|
2738
|
+
continue;
|
|
2739
|
+
}
|
|
2740
|
+
const ticket = parsed.ticket;
|
|
2741
|
+
results.push({
|
|
2742
|
+
sessionId: parsed.sessionId ?? entry.name,
|
|
2743
|
+
state: parsed.state ?? "unknown",
|
|
2744
|
+
mode: parsed.mode ?? "auto",
|
|
2745
|
+
ticketId: ticket?.id ?? null,
|
|
2746
|
+
ticketTitle: ticket?.title ?? null
|
|
2747
|
+
});
|
|
2748
|
+
}
|
|
2749
|
+
return results;
|
|
2750
|
+
}
|
|
2751
|
+
var init_session_scan = __esm({
|
|
2752
|
+
"src/core/session-scan.ts"() {
|
|
2753
|
+
"use strict";
|
|
2754
|
+
init_esm_shims();
|
|
2755
|
+
}
|
|
2756
|
+
});
|
|
2757
|
+
|
|
2694
2758
|
// src/cli/commands/status.ts
|
|
2695
2759
|
function handleStatus(ctx) {
|
|
2696
|
-
|
|
2760
|
+
const sessions = scanActiveSessions(ctx.root);
|
|
2761
|
+
return { output: formatStatus(ctx.state, ctx.format, sessions) };
|
|
2697
2762
|
}
|
|
2698
2763
|
var init_status = __esm({
|
|
2699
2764
|
"src/cli/commands/status.ts"() {
|
|
2700
2765
|
"use strict";
|
|
2701
2766
|
init_esm_shims();
|
|
2702
2767
|
init_output_formatter();
|
|
2768
|
+
init_session_scan();
|
|
2703
2769
|
}
|
|
2704
2770
|
});
|
|
2705
2771
|
|
|
@@ -2787,6 +2853,7 @@ function validateProject(state) {
|
|
|
2787
2853
|
}
|
|
2788
2854
|
}
|
|
2789
2855
|
}
|
|
2856
|
+
detectSupersedesCycles(state, findings);
|
|
2790
2857
|
const phaseIDCounts = /* @__PURE__ */ new Map();
|
|
2791
2858
|
for (const p of state.roadmap.phases) {
|
|
2792
2859
|
phaseIDCounts.set(p.id, (phaseIDCounts.get(p.id) ?? 0) + 1);
|
|
@@ -2991,6 +3058,33 @@ function dfsBlocked(id, state, visited, inStack, findings) {
|
|
|
2991
3058
|
inStack.delete(id);
|
|
2992
3059
|
visited.add(id);
|
|
2993
3060
|
}
|
|
3061
|
+
function detectSupersedesCycles(state, findings) {
|
|
3062
|
+
const visited = /* @__PURE__ */ new Set();
|
|
3063
|
+
const inStack = /* @__PURE__ */ new Set();
|
|
3064
|
+
for (const l of state.lessons) {
|
|
3065
|
+
if (l.supersedes == null || visited.has(l.id)) continue;
|
|
3066
|
+
dfsSupersedesChain(l.id, state, visited, inStack, findings);
|
|
3067
|
+
}
|
|
3068
|
+
}
|
|
3069
|
+
function dfsSupersedesChain(id, state, visited, inStack, findings) {
|
|
3070
|
+
if (inStack.has(id)) {
|
|
3071
|
+
findings.push({
|
|
3072
|
+
level: "error",
|
|
3073
|
+
code: "supersedes_cycle",
|
|
3074
|
+
message: `Cycle detected in supersedes chain involving ${id}.`,
|
|
3075
|
+
entity: id
|
|
3076
|
+
});
|
|
3077
|
+
return;
|
|
3078
|
+
}
|
|
3079
|
+
if (visited.has(id)) return;
|
|
3080
|
+
inStack.add(id);
|
|
3081
|
+
const lesson = state.lessonByID(id);
|
|
3082
|
+
if (lesson?.supersedes && lesson.supersedes !== id) {
|
|
3083
|
+
dfsSupersedesChain(lesson.supersedes, state, visited, inStack, findings);
|
|
3084
|
+
}
|
|
3085
|
+
inStack.delete(id);
|
|
3086
|
+
visited.add(id);
|
|
3087
|
+
}
|
|
2994
3088
|
var init_validation = __esm({
|
|
2995
3089
|
"src/core/validation.ts"() {
|
|
2996
3090
|
"use strict";
|
|
@@ -3027,7 +3121,7 @@ __export(handover_exports, {
|
|
|
3027
3121
|
});
|
|
3028
3122
|
import { existsSync as existsSync4 } from "fs";
|
|
3029
3123
|
import { mkdir as mkdir2 } from "fs/promises";
|
|
3030
|
-
import { join as
|
|
3124
|
+
import { join as join5, resolve as resolve4 } from "path";
|
|
3031
3125
|
function handleHandoverList(ctx) {
|
|
3032
3126
|
return { output: formatHandoverList(ctx.state.handoverFilenames, ctx.format) };
|
|
3033
3127
|
}
|
|
@@ -3112,15 +3206,15 @@ async function handleHandoverCreate(content, slugRaw, format, root) {
|
|
|
3112
3206
|
let filename;
|
|
3113
3207
|
await withProjectLock(root, { strict: false }, async () => {
|
|
3114
3208
|
const absRoot = resolve4(root);
|
|
3115
|
-
const handoversDir =
|
|
3209
|
+
const handoversDir = join5(absRoot, ".story", "handovers");
|
|
3116
3210
|
await mkdir2(handoversDir, { recursive: true });
|
|
3117
|
-
const wrapDir =
|
|
3211
|
+
const wrapDir = join5(absRoot, ".story");
|
|
3118
3212
|
const datePrefix = `${date}-`;
|
|
3119
3213
|
const seqRegex = new RegExp(`^${date}-(\\d{2})-`);
|
|
3120
3214
|
let maxSeq = 0;
|
|
3121
|
-
const { readdirSync:
|
|
3215
|
+
const { readdirSync: readdirSync5 } = await import("fs");
|
|
3122
3216
|
try {
|
|
3123
|
-
for (const f of
|
|
3217
|
+
for (const f of readdirSync5(handoversDir)) {
|
|
3124
3218
|
const m = f.match(seqRegex);
|
|
3125
3219
|
if (m) {
|
|
3126
3220
|
const n = parseInt(m[1], 10);
|
|
@@ -3137,7 +3231,7 @@ async function handleHandoverCreate(content, slugRaw, format, root) {
|
|
|
3137
3231
|
);
|
|
3138
3232
|
}
|
|
3139
3233
|
let candidate = `${date}-${String(nextSeq).padStart(2, "0")}-${slug}.md`;
|
|
3140
|
-
let candidatePath =
|
|
3234
|
+
let candidatePath = join5(handoversDir, candidate);
|
|
3141
3235
|
while (existsSync4(candidatePath)) {
|
|
3142
3236
|
nextSeq++;
|
|
3143
3237
|
if (nextSeq > 99) {
|
|
@@ -3147,7 +3241,7 @@ async function handleHandoverCreate(content, slugRaw, format, root) {
|
|
|
3147
3241
|
);
|
|
3148
3242
|
}
|
|
3149
3243
|
candidate = `${date}-${String(nextSeq).padStart(2, "0")}-${slug}.md`;
|
|
3150
|
-
candidatePath =
|
|
3244
|
+
candidatePath = join5(handoversDir, candidate);
|
|
3151
3245
|
}
|
|
3152
3246
|
await parseHandoverFilename(candidate, handoversDir);
|
|
3153
3247
|
await guardPath(candidatePath, wrapDir);
|
|
@@ -3382,7 +3476,20 @@ function validateParentTicket(parentId, ticketId, state) {
|
|
|
3382
3476
|
throw new CliValidationError("invalid_input", `Parent ticket ${parentId} not found`);
|
|
3383
3477
|
}
|
|
3384
3478
|
}
|
|
3479
|
+
function buildErrorMultiset(findings) {
|
|
3480
|
+
const counts = /* @__PURE__ */ new Map();
|
|
3481
|
+
const messages = /* @__PURE__ */ new Map();
|
|
3482
|
+
for (const f of findings) {
|
|
3483
|
+
if (f.level !== "error") continue;
|
|
3484
|
+
const key = `${f.code}|${f.entity ?? ""}|${f.message}`;
|
|
3485
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
3486
|
+
messages.set(key, f.message);
|
|
3487
|
+
}
|
|
3488
|
+
return { counts, messages };
|
|
3489
|
+
}
|
|
3385
3490
|
function validatePostWriteState(candidate, state, isCreate) {
|
|
3491
|
+
const preResult = validateProject(state);
|
|
3492
|
+
const { counts: preErrors } = buildErrorMultiset(preResult.findings);
|
|
3386
3493
|
const existingTickets = [...state.tickets];
|
|
3387
3494
|
if (isCreate) {
|
|
3388
3495
|
existingTickets.push(candidate);
|
|
@@ -3399,11 +3506,17 @@ function validatePostWriteState(candidate, state, isCreate) {
|
|
|
3399
3506
|
config: state.config,
|
|
3400
3507
|
handoverFilenames: [...state.handoverFilenames]
|
|
3401
3508
|
});
|
|
3402
|
-
const
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3509
|
+
const postResult = validateProject(postState);
|
|
3510
|
+
const { counts: postErrors, messages: postMessages } = buildErrorMultiset(postResult.findings);
|
|
3511
|
+
const newErrors = [];
|
|
3512
|
+
for (const [key, postCount] of postErrors) {
|
|
3513
|
+
const preCount = preErrors.get(key) ?? 0;
|
|
3514
|
+
if (postCount > preCount) {
|
|
3515
|
+
newErrors.push(postMessages.get(key) ?? key);
|
|
3516
|
+
}
|
|
3517
|
+
}
|
|
3518
|
+
if (newErrors.length > 0) {
|
|
3519
|
+
throw new CliValidationError("validation_failed", `Write would create invalid state: ${newErrors.join("; ")}`);
|
|
3407
3520
|
}
|
|
3408
3521
|
}
|
|
3409
3522
|
async function handleTicketCreate(args, format, root) {
|
|
@@ -3587,7 +3700,20 @@ function validateRelatedTickets(ids, state) {
|
|
|
3587
3700
|
}
|
|
3588
3701
|
}
|
|
3589
3702
|
}
|
|
3703
|
+
function buildErrorMultiset2(findings) {
|
|
3704
|
+
const counts = /* @__PURE__ */ new Map();
|
|
3705
|
+
const messages = /* @__PURE__ */ new Map();
|
|
3706
|
+
for (const f of findings) {
|
|
3707
|
+
if (f.level !== "error") continue;
|
|
3708
|
+
const key = `${f.code}|${f.entity ?? ""}|${f.message}`;
|
|
3709
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
3710
|
+
messages.set(key, f.message);
|
|
3711
|
+
}
|
|
3712
|
+
return { counts, messages };
|
|
3713
|
+
}
|
|
3590
3714
|
function validatePostWriteIssueState(candidate, state, isCreate) {
|
|
3715
|
+
const preResult = validateProject(state);
|
|
3716
|
+
const { counts: preErrors } = buildErrorMultiset2(preResult.findings);
|
|
3591
3717
|
const existingIssues = [...state.issues];
|
|
3592
3718
|
if (isCreate) {
|
|
3593
3719
|
existingIssues.push(candidate);
|
|
@@ -3604,11 +3730,17 @@ function validatePostWriteIssueState(candidate, state, isCreate) {
|
|
|
3604
3730
|
config: state.config,
|
|
3605
3731
|
handoverFilenames: [...state.handoverFilenames]
|
|
3606
3732
|
});
|
|
3607
|
-
const
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3733
|
+
const postResult = validateProject(postState);
|
|
3734
|
+
const { counts: postErrors, messages: postMessages } = buildErrorMultiset2(postResult.findings);
|
|
3735
|
+
const newErrors = [];
|
|
3736
|
+
for (const [key, postCount] of postErrors) {
|
|
3737
|
+
const preCount = preErrors.get(key) ?? 0;
|
|
3738
|
+
if (postCount > preCount) {
|
|
3739
|
+
newErrors.push(postMessages.get(key) ?? key);
|
|
3740
|
+
}
|
|
3741
|
+
}
|
|
3742
|
+
if (newErrors.length > 0) {
|
|
3743
|
+
throw new CliValidationError("validation_failed", `Write would create invalid state: ${newErrors.join("; ")}`);
|
|
3612
3744
|
}
|
|
3613
3745
|
}
|
|
3614
3746
|
async function handleIssueCreate(args, format, root) {
|
|
@@ -3742,11 +3874,11 @@ __export(snapshot_exports, {
|
|
|
3742
3874
|
});
|
|
3743
3875
|
import { readdir as readdir3, readFile as readFile3, mkdir as mkdir3, unlink as unlink2 } from "fs/promises";
|
|
3744
3876
|
import { existsSync as existsSync5 } from "fs";
|
|
3745
|
-
import { join as
|
|
3877
|
+
import { join as join6, resolve as resolve5 } from "path";
|
|
3746
3878
|
import { z as z8 } from "zod";
|
|
3747
3879
|
async function saveSnapshot(root, loadResult) {
|
|
3748
3880
|
const absRoot = resolve5(root);
|
|
3749
|
-
const snapshotsDir =
|
|
3881
|
+
const snapshotsDir = join6(absRoot, ".story", "snapshots");
|
|
3750
3882
|
await mkdir3(snapshotsDir, { recursive: true });
|
|
3751
3883
|
const { state, warnings } = loadResult;
|
|
3752
3884
|
const now = /* @__PURE__ */ new Date();
|
|
@@ -3771,8 +3903,8 @@ async function saveSnapshot(root, loadResult) {
|
|
|
3771
3903
|
} : {}
|
|
3772
3904
|
};
|
|
3773
3905
|
const json = JSON.stringify(snapshot, null, 2) + "\n";
|
|
3774
|
-
const targetPath =
|
|
3775
|
-
const wrapDir =
|
|
3906
|
+
const targetPath = join6(snapshotsDir, filename);
|
|
3907
|
+
const wrapDir = join6(absRoot, ".story");
|
|
3776
3908
|
await guardPath(targetPath, wrapDir);
|
|
3777
3909
|
await atomicWrite(targetPath, json);
|
|
3778
3910
|
const pruned = await pruneSnapshots(snapshotsDir);
|
|
@@ -3780,13 +3912,13 @@ async function saveSnapshot(root, loadResult) {
|
|
|
3780
3912
|
return { filename, retained: entries.length, pruned };
|
|
3781
3913
|
}
|
|
3782
3914
|
async function loadLatestSnapshot(root) {
|
|
3783
|
-
const snapshotsDir =
|
|
3915
|
+
const snapshotsDir = join6(resolve5(root), ".story", "snapshots");
|
|
3784
3916
|
if (!existsSync5(snapshotsDir)) return null;
|
|
3785
3917
|
const files = await listSnapshotFiles(snapshotsDir);
|
|
3786
3918
|
if (files.length === 0) return null;
|
|
3787
3919
|
for (const filename of files) {
|
|
3788
3920
|
try {
|
|
3789
|
-
const content = await readFile3(
|
|
3921
|
+
const content = await readFile3(join6(snapshotsDir, filename), "utf-8");
|
|
3790
3922
|
const parsed = JSON.parse(content);
|
|
3791
3923
|
const snapshot = SnapshotV1Schema.parse(parsed);
|
|
3792
3924
|
return { snapshot, filename };
|
|
@@ -4030,7 +4162,7 @@ async function pruneSnapshots(dir) {
|
|
|
4030
4162
|
const toRemove = files.slice(MAX_SNAPSHOTS);
|
|
4031
4163
|
for (const f of toRemove) {
|
|
4032
4164
|
try {
|
|
4033
|
-
await unlink2(
|
|
4165
|
+
await unlink2(join6(dir, f));
|
|
4034
4166
|
} catch {
|
|
4035
4167
|
}
|
|
4036
4168
|
}
|
|
@@ -4449,7 +4581,7 @@ var init_lesson2 = __esm({
|
|
|
4449
4581
|
});
|
|
4450
4582
|
|
|
4451
4583
|
// src/core/recommend.ts
|
|
4452
|
-
function recommend(state, count) {
|
|
4584
|
+
function recommend(state, count, options) {
|
|
4453
4585
|
const effectiveCount = Math.max(1, Math.min(10, count));
|
|
4454
4586
|
const dedup = /* @__PURE__ */ new Map();
|
|
4455
4587
|
const phaseIndex = buildPhaseIndex(state);
|
|
@@ -4461,7 +4593,8 @@ function recommend(state, count) {
|
|
|
4461
4593
|
() => generateNearCompleteUmbrellas(state, phaseIndex),
|
|
4462
4594
|
() => generatePhaseMomentum(state),
|
|
4463
4595
|
() => generateQuickWins(state, phaseIndex),
|
|
4464
|
-
() => generateOpenIssues(state)
|
|
4596
|
+
() => generateOpenIssues(state),
|
|
4597
|
+
() => generateDebtTrend(state, options)
|
|
4465
4598
|
];
|
|
4466
4599
|
for (const gen of generators) {
|
|
4467
4600
|
for (const rec of gen()) {
|
|
@@ -4471,6 +4604,7 @@ function recommend(state, count) {
|
|
|
4471
4604
|
}
|
|
4472
4605
|
}
|
|
4473
4606
|
}
|
|
4607
|
+
applyHandoverBoost(state, dedup, options);
|
|
4474
4608
|
const curPhase = currentPhase(state);
|
|
4475
4609
|
const curPhaseIdx = curPhase ? phaseIndex.get(curPhase.id) ?? 0 : 0;
|
|
4476
4610
|
for (const [id, rec] of dedup) {
|
|
@@ -4660,7 +4794,76 @@ function sortByPhaseAndOrder(tickets, phaseIndex) {
|
|
|
4660
4794
|
return a.order - b.order;
|
|
4661
4795
|
});
|
|
4662
4796
|
}
|
|
4663
|
-
|
|
4797
|
+
function applyHandoverBoost(state, dedup, options) {
|
|
4798
|
+
if (!options?.latestHandoverContent) return;
|
|
4799
|
+
const content = options.latestHandoverContent;
|
|
4800
|
+
let actionableIds = extractTicketIdsFromActionableSections(content);
|
|
4801
|
+
if (actionableIds.size === 0) {
|
|
4802
|
+
const allIds = new Set(content.match(TICKET_ID_RE) ?? []);
|
|
4803
|
+
for (const id of allIds) {
|
|
4804
|
+
const ticket = state.ticketByID(id);
|
|
4805
|
+
if (ticket && ticket.status !== "complete" && ticket.status !== "inprogress") {
|
|
4806
|
+
actionableIds.add(id);
|
|
4807
|
+
}
|
|
4808
|
+
}
|
|
4809
|
+
}
|
|
4810
|
+
for (const id of actionableIds) {
|
|
4811
|
+
const ticket = state.ticketByID(id);
|
|
4812
|
+
if (!ticket || ticket.status === "complete") continue;
|
|
4813
|
+
const existing = dedup.get(id);
|
|
4814
|
+
if (existing) {
|
|
4815
|
+
dedup.set(id, {
|
|
4816
|
+
...existing,
|
|
4817
|
+
score: existing.score + HANDOVER_BOOST,
|
|
4818
|
+
reason: existing.reason + " (handover context)"
|
|
4819
|
+
});
|
|
4820
|
+
} else {
|
|
4821
|
+
dedup.set(id, {
|
|
4822
|
+
id,
|
|
4823
|
+
kind: "ticket",
|
|
4824
|
+
title: ticket.title,
|
|
4825
|
+
category: "handover_context",
|
|
4826
|
+
reason: "Referenced in latest handover",
|
|
4827
|
+
score: HANDOVER_BASE_SCORE
|
|
4828
|
+
});
|
|
4829
|
+
}
|
|
4830
|
+
}
|
|
4831
|
+
}
|
|
4832
|
+
function extractTicketIdsFromActionableSections(content) {
|
|
4833
|
+
const ids = /* @__PURE__ */ new Set();
|
|
4834
|
+
const lines = content.split("\n");
|
|
4835
|
+
let inActionable = false;
|
|
4836
|
+
for (const line of lines) {
|
|
4837
|
+
if (/^#+\s/.test(line)) {
|
|
4838
|
+
inActionable = ACTIONABLE_HEADING_RE.test(line);
|
|
4839
|
+
}
|
|
4840
|
+
if (inActionable) {
|
|
4841
|
+
const matches = line.match(TICKET_ID_RE);
|
|
4842
|
+
if (matches) for (const m of matches) ids.add(m);
|
|
4843
|
+
}
|
|
4844
|
+
}
|
|
4845
|
+
return ids;
|
|
4846
|
+
}
|
|
4847
|
+
function generateDebtTrend(state, options) {
|
|
4848
|
+
if (options?.previousOpenIssueCount == null) return [];
|
|
4849
|
+
const currentOpen = state.issues.filter((i) => i.status !== "resolved").length;
|
|
4850
|
+
const previous = options.previousOpenIssueCount;
|
|
4851
|
+
if (previous <= 0) return [];
|
|
4852
|
+
const growth = (currentOpen - previous) / previous;
|
|
4853
|
+
const absolute = currentOpen - previous;
|
|
4854
|
+
if (growth > DEBT_GROWTH_THRESHOLD && absolute >= DEBT_ABSOLUTE_MINIMUM) {
|
|
4855
|
+
return [{
|
|
4856
|
+
id: "DEBT_TREND",
|
|
4857
|
+
kind: "action",
|
|
4858
|
+
title: "Issue debt growing",
|
|
4859
|
+
category: "debt_trend",
|
|
4860
|
+
reason: `Open issues grew from ${previous} to ${currentOpen} (+${Math.round(growth * 100)}%). Consider triaging or resolving issues before adding features.`,
|
|
4861
|
+
score: DEBT_TREND_SCORE
|
|
4862
|
+
}];
|
|
4863
|
+
}
|
|
4864
|
+
return [];
|
|
4865
|
+
}
|
|
4866
|
+
var SEVERITY_RANK, PHASE_DISTANCE_PENALTY, MAX_PHASE_PENALTY, CATEGORY_PRIORITY, TICKET_ID_RE, ACTIONABLE_HEADING_RE, HANDOVER_BOOST, HANDOVER_BASE_SCORE, DEBT_TREND_SCORE, DEBT_GROWTH_THRESHOLD, DEBT_ABSOLUTE_MINIMUM;
|
|
4664
4867
|
var init_recommend = __esm({
|
|
4665
4868
|
"src/core/recommend.ts"() {
|
|
4666
4869
|
"use strict";
|
|
@@ -4682,17 +4885,52 @@ var init_recommend = __esm({
|
|
|
4682
4885
|
high_impact_unblock: 4,
|
|
4683
4886
|
near_complete_umbrella: 5,
|
|
4684
4887
|
phase_momentum: 6,
|
|
4685
|
-
|
|
4686
|
-
|
|
4888
|
+
debt_trend: 7,
|
|
4889
|
+
quick_win: 8,
|
|
4890
|
+
handover_context: 9,
|
|
4891
|
+
open_issue: 10
|
|
4687
4892
|
};
|
|
4893
|
+
TICKET_ID_RE = /\bT-\d{3}[a-z]?\b/g;
|
|
4894
|
+
ACTIONABLE_HEADING_RE = /^#+\s.*(next|open|remaining|todo|blocked)/im;
|
|
4895
|
+
HANDOVER_BOOST = 50;
|
|
4896
|
+
HANDOVER_BASE_SCORE = 350;
|
|
4897
|
+
DEBT_TREND_SCORE = 450;
|
|
4898
|
+
DEBT_GROWTH_THRESHOLD = 0.25;
|
|
4899
|
+
DEBT_ABSOLUTE_MINIMUM = 2;
|
|
4688
4900
|
}
|
|
4689
4901
|
});
|
|
4690
4902
|
|
|
4691
4903
|
// src/cli/commands/recommend.ts
|
|
4904
|
+
import { readFileSync as readFileSync2, readdirSync as readdirSync2 } from "fs";
|
|
4905
|
+
import { join as join7 } from "path";
|
|
4692
4906
|
function handleRecommend(ctx, count) {
|
|
4693
|
-
const
|
|
4907
|
+
const options = buildRecommendOptions(ctx);
|
|
4908
|
+
const result = recommend(ctx.state, count, options);
|
|
4694
4909
|
return { output: formatRecommendations(result, ctx.state, ctx.format) };
|
|
4695
4910
|
}
|
|
4911
|
+
function buildRecommendOptions(ctx) {
|
|
4912
|
+
const opts = {};
|
|
4913
|
+
try {
|
|
4914
|
+
const files = readdirSync2(ctx.handoversDir).filter((f) => f.endsWith(".md")).sort();
|
|
4915
|
+
if (files.length > 0) {
|
|
4916
|
+
opts.latestHandoverContent = readFileSync2(join7(ctx.handoversDir, files[files.length - 1]), "utf-8");
|
|
4917
|
+
}
|
|
4918
|
+
} catch {
|
|
4919
|
+
}
|
|
4920
|
+
try {
|
|
4921
|
+
const snapshotsDir = join7(ctx.root, ".story", "snapshots");
|
|
4922
|
+
const snapFiles = readdirSync2(snapshotsDir).filter((f) => f.endsWith(".json")).sort();
|
|
4923
|
+
if (snapFiles.length > 0) {
|
|
4924
|
+
const raw = readFileSync2(join7(snapshotsDir, snapFiles[snapFiles.length - 1]), "utf-8");
|
|
4925
|
+
const snap = JSON.parse(raw);
|
|
4926
|
+
if (snap.issues) {
|
|
4927
|
+
opts.previousOpenIssueCount = snap.issues.filter((i) => i.status !== "resolved").length;
|
|
4928
|
+
}
|
|
4929
|
+
}
|
|
4930
|
+
} catch {
|
|
4931
|
+
}
|
|
4932
|
+
return opts;
|
|
4933
|
+
}
|
|
4696
4934
|
var init_recommend2 = __esm({
|
|
4697
4935
|
"src/cli/commands/recommend.ts"() {
|
|
4698
4936
|
"use strict";
|
|
@@ -5242,27 +5480,27 @@ __export(session_exports, {
|
|
|
5242
5480
|
import { randomUUID } from "crypto";
|
|
5243
5481
|
import {
|
|
5244
5482
|
mkdirSync,
|
|
5245
|
-
readdirSync,
|
|
5246
|
-
readFileSync,
|
|
5483
|
+
readdirSync as readdirSync3,
|
|
5484
|
+
readFileSync as readFileSync3,
|
|
5247
5485
|
writeFileSync,
|
|
5248
5486
|
renameSync,
|
|
5249
5487
|
unlinkSync,
|
|
5250
5488
|
existsSync as existsSync6,
|
|
5251
5489
|
rmSync
|
|
5252
5490
|
} from "fs";
|
|
5253
|
-
import { join as
|
|
5491
|
+
import { join as join8 } from "path";
|
|
5254
5492
|
import lockfile2 from "proper-lockfile";
|
|
5255
5493
|
function sessionsRoot(root) {
|
|
5256
|
-
return
|
|
5494
|
+
return join8(root, ".story", SESSIONS_DIR);
|
|
5257
5495
|
}
|
|
5258
5496
|
function sessionDir(root, sessionId) {
|
|
5259
|
-
return
|
|
5497
|
+
return join8(sessionsRoot(root), sessionId);
|
|
5260
5498
|
}
|
|
5261
5499
|
function statePath(dir) {
|
|
5262
|
-
return
|
|
5500
|
+
return join8(dir, "state.json");
|
|
5263
5501
|
}
|
|
5264
5502
|
function eventsPath(dir) {
|
|
5265
|
-
return
|
|
5503
|
+
return join8(dir, "events.log");
|
|
5266
5504
|
}
|
|
5267
5505
|
function createSession(root, recipe, workspaceId, configOverrides) {
|
|
5268
5506
|
const id = randomUUID();
|
|
@@ -5318,7 +5556,7 @@ function readSession(dir) {
|
|
|
5318
5556
|
const path2 = statePath(dir);
|
|
5319
5557
|
let raw;
|
|
5320
5558
|
try {
|
|
5321
|
-
raw =
|
|
5559
|
+
raw = readFileSync3(path2, "utf-8");
|
|
5322
5560
|
} catch {
|
|
5323
5561
|
return null;
|
|
5324
5562
|
}
|
|
@@ -5361,7 +5599,7 @@ function readEvents(dir) {
|
|
|
5361
5599
|
const path2 = eventsPath(dir);
|
|
5362
5600
|
let raw;
|
|
5363
5601
|
try {
|
|
5364
|
-
raw =
|
|
5602
|
+
raw = readFileSync3(path2, "utf-8");
|
|
5365
5603
|
} catch {
|
|
5366
5604
|
return { events: [], malformedCount: 0 };
|
|
5367
5605
|
}
|
|
@@ -5418,7 +5656,7 @@ function findActiveSessionFull(root) {
|
|
|
5418
5656
|
const sessDir = sessionsRoot(root);
|
|
5419
5657
|
let entries;
|
|
5420
5658
|
try {
|
|
5421
|
-
entries =
|
|
5659
|
+
entries = readdirSync3(sessDir, { withFileTypes: true });
|
|
5422
5660
|
} catch {
|
|
5423
5661
|
return null;
|
|
5424
5662
|
}
|
|
@@ -5432,7 +5670,7 @@ function findActiveSessionFull(root) {
|
|
|
5432
5670
|
let bestGuideCall = 0;
|
|
5433
5671
|
for (const entry of entries) {
|
|
5434
5672
|
if (!entry.isDirectory()) continue;
|
|
5435
|
-
const dir =
|
|
5673
|
+
const dir = join8(sessDir, entry.name);
|
|
5436
5674
|
const session = readSession(dir);
|
|
5437
5675
|
if (!session) continue;
|
|
5438
5676
|
if (session.status !== "active") continue;
|
|
@@ -5455,7 +5693,7 @@ function findStaleSessions(root) {
|
|
|
5455
5693
|
const sessDir = sessionsRoot(root);
|
|
5456
5694
|
let entries;
|
|
5457
5695
|
try {
|
|
5458
|
-
entries =
|
|
5696
|
+
entries = readdirSync3(sessDir, { withFileTypes: true });
|
|
5459
5697
|
} catch {
|
|
5460
5698
|
return [];
|
|
5461
5699
|
}
|
|
@@ -5468,7 +5706,7 @@ function findStaleSessions(root) {
|
|
|
5468
5706
|
const results = [];
|
|
5469
5707
|
for (const entry of entries) {
|
|
5470
5708
|
if (!entry.isDirectory()) continue;
|
|
5471
|
-
const dir =
|
|
5709
|
+
const dir = join8(sessDir, entry.name);
|
|
5472
5710
|
const session = readSession(dir);
|
|
5473
5711
|
if (!session) continue;
|
|
5474
5712
|
if (session.status !== "active") continue;
|
|
@@ -5524,7 +5762,7 @@ function findResumableSession(root) {
|
|
|
5524
5762
|
const sessDir = sessionsRoot(root);
|
|
5525
5763
|
let entries;
|
|
5526
5764
|
try {
|
|
5527
|
-
entries =
|
|
5765
|
+
entries = readdirSync3(sessDir, { withFileTypes: true });
|
|
5528
5766
|
} catch {
|
|
5529
5767
|
return null;
|
|
5530
5768
|
}
|
|
@@ -5539,7 +5777,7 @@ function findResumableSession(root) {
|
|
|
5539
5777
|
let bestPreparedAt = 0;
|
|
5540
5778
|
for (const entry of entries) {
|
|
5541
5779
|
if (!entry.isDirectory()) continue;
|
|
5542
|
-
const dir =
|
|
5780
|
+
const dir = join8(sessDir, entry.name);
|
|
5543
5781
|
const session = readSession(dir);
|
|
5544
5782
|
if (!session) continue;
|
|
5545
5783
|
if (session.status !== "active") continue;
|
|
@@ -5563,7 +5801,7 @@ async function withSessionLock(root, fn) {
|
|
|
5563
5801
|
release = await lockfile2.lock(sessDir, {
|
|
5564
5802
|
retries: { retries: 3, minTimeout: 100, maxTimeout: 1e3 },
|
|
5565
5803
|
stale: 3e4,
|
|
5566
|
-
lockfilePath:
|
|
5804
|
+
lockfilePath: join8(sessDir, ".lock")
|
|
5567
5805
|
});
|
|
5568
5806
|
return await fn();
|
|
5569
5807
|
} finally {
|
|
@@ -5879,16 +6117,16 @@ var init_git_inspector = __esm({
|
|
|
5879
6117
|
});
|
|
5880
6118
|
|
|
5881
6119
|
// src/autonomous/recipes/loader.ts
|
|
5882
|
-
import { readFileSync as
|
|
5883
|
-
import { join as
|
|
6120
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
6121
|
+
import { join as join9, dirname as dirname3 } from "path";
|
|
5884
6122
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
5885
6123
|
function loadRecipe(recipeName) {
|
|
5886
6124
|
if (!/^[A-Za-z0-9_-]+$/.test(recipeName)) {
|
|
5887
6125
|
throw new Error(`Invalid recipe name: ${recipeName}`);
|
|
5888
6126
|
}
|
|
5889
|
-
const recipesDir =
|
|
5890
|
-
const path2 =
|
|
5891
|
-
const raw =
|
|
6127
|
+
const recipesDir = join9(dirname3(fileURLToPath2(import.meta.url)), "..", "recipes");
|
|
6128
|
+
const path2 = join9(recipesDir, `${recipeName}.json`);
|
|
6129
|
+
const raw = readFileSync4(path2, "utf-8");
|
|
5892
6130
|
return JSON.parse(raw);
|
|
5893
6131
|
}
|
|
5894
6132
|
function resolveRecipe(recipeName, projectOverrides) {
|
|
@@ -6164,7 +6402,7 @@ var init_types2 = __esm({
|
|
|
6164
6402
|
|
|
6165
6403
|
// src/autonomous/stages/pick-ticket.ts
|
|
6166
6404
|
import { existsSync as existsSync7, unlinkSync as unlinkSync2 } from "fs";
|
|
6167
|
-
import { join as
|
|
6405
|
+
import { join as join10 } from "path";
|
|
6168
6406
|
var PickTicketStage;
|
|
6169
6407
|
var init_pick_ticket = __esm({
|
|
6170
6408
|
"src/autonomous/stages/pick-ticket.ts"() {
|
|
@@ -6219,7 +6457,7 @@ var init_pick_ticket = __esm({
|
|
|
6219
6457
|
return { action: "retry", instruction: `Ticket ${ticketId} is ${ticket.status} \u2014 pick an open ticket.` };
|
|
6220
6458
|
}
|
|
6221
6459
|
}
|
|
6222
|
-
const planPath =
|
|
6460
|
+
const planPath = join10(ctx.dir, "plan.md");
|
|
6223
6461
|
try {
|
|
6224
6462
|
if (existsSync7(planPath)) unlinkSync2(planPath);
|
|
6225
6463
|
} catch {
|
|
@@ -6259,11 +6497,11 @@ ${ticket.description}` : "",
|
|
|
6259
6497
|
});
|
|
6260
6498
|
|
|
6261
6499
|
// src/autonomous/stages/plan.ts
|
|
6262
|
-
import { existsSync as existsSync8, readFileSync as
|
|
6263
|
-
import { join as
|
|
6500
|
+
import { existsSync as existsSync8, readFileSync as readFileSync5 } from "fs";
|
|
6501
|
+
import { join as join11 } from "path";
|
|
6264
6502
|
function readFileSafe(path2) {
|
|
6265
6503
|
try {
|
|
6266
|
-
return
|
|
6504
|
+
return readFileSync5(path2, "utf-8");
|
|
6267
6505
|
} catch {
|
|
6268
6506
|
return "";
|
|
6269
6507
|
}
|
|
@@ -6304,7 +6542,7 @@ var init_plan = __esm({
|
|
|
6304
6542
|
};
|
|
6305
6543
|
}
|
|
6306
6544
|
async report(ctx, _report) {
|
|
6307
|
-
const planPath =
|
|
6545
|
+
const planPath = join11(ctx.dir, "plan.md");
|
|
6308
6546
|
if (!existsSync8(planPath)) {
|
|
6309
6547
|
return { action: "retry", instruction: `Plan file not found at ${planPath}. Write your plan there and call me again.`, reminders: ["Save plan to .story/sessions/<id>/plan.md"] };
|
|
6310
6548
|
}
|
|
@@ -6550,7 +6788,8 @@ var init_implement = __esm({
|
|
|
6550
6788
|
reminders: [
|
|
6551
6789
|
"Follow the plan exactly. Do NOT deviate without re-planning.",
|
|
6552
6790
|
"Do NOT ask the user for confirmation.",
|
|
6553
|
-
"If you discover pre-existing bugs, failing tests not caused by your changes, or other out-of-scope problems, file them as issues using claudestory_issue_create. Do not fix them inline."
|
|
6791
|
+
"If you discover pre-existing bugs, failing tests not caused by your changes, or other out-of-scope problems, file them as issues using claudestory_issue_create. Do not fix them inline.",
|
|
6792
|
+
"Track which files you create or modify. Only these files should be staged at commit time."
|
|
6554
6793
|
],
|
|
6555
6794
|
transitionedFrom: ctx.state.previousState ?? void 0
|
|
6556
6795
|
};
|
|
@@ -7247,10 +7486,10 @@ var init_finalize = __esm({
|
|
|
7247
7486
|
"Code review passed. Time to commit.",
|
|
7248
7487
|
"",
|
|
7249
7488
|
ctx.state.ticket ? `1. Update ticket ${ctx.state.ticket.id} status to "complete" in .story/` : "",
|
|
7250
|
-
"2. Stage
|
|
7489
|
+
"2. Stage only the files you created or modified for this ticket (code + .story/ changes). Do NOT use `git add -A` or `git add .`",
|
|
7251
7490
|
'3. Call me with completedAction: "files_staged"'
|
|
7252
7491
|
].filter(Boolean).join("\n"),
|
|
7253
|
-
reminders: ["Stage both code changes and .story/ ticket update in the same commit."],
|
|
7492
|
+
reminders: ["Stage both code changes and .story/ ticket update in the same commit. Only stage files related to this ticket."],
|
|
7254
7493
|
transitionedFrom: ctx.state.previousState ?? void 0
|
|
7255
7494
|
};
|
|
7256
7495
|
}
|
|
@@ -7754,7 +7993,7 @@ Impact: ${nextIssue.impact}` : ""}` : `Fix issue ${next}.`,
|
|
|
7754
7993
|
|
|
7755
7994
|
// src/autonomous/stages/handover.ts
|
|
7756
7995
|
import { writeFileSync as writeFileSync2 } from "fs";
|
|
7757
|
-
import { join as
|
|
7996
|
+
import { join as join12 } from "path";
|
|
7758
7997
|
var HandoverStage;
|
|
7759
7998
|
var init_handover2 = __esm({
|
|
7760
7999
|
"src/autonomous/stages/handover.ts"() {
|
|
@@ -7791,7 +8030,7 @@ var init_handover2 = __esm({
|
|
|
7791
8030
|
} catch {
|
|
7792
8031
|
handoverFailed = true;
|
|
7793
8032
|
try {
|
|
7794
|
-
const fallbackPath =
|
|
8033
|
+
const fallbackPath = join12(ctx.dir, "handover-fallback.md");
|
|
7795
8034
|
writeFileSync2(fallbackPath, content, "utf-8");
|
|
7796
8035
|
} catch {
|
|
7797
8036
|
}
|
|
@@ -7875,8 +8114,32 @@ var init_stages = __esm({
|
|
|
7875
8114
|
});
|
|
7876
8115
|
|
|
7877
8116
|
// src/autonomous/guide.ts
|
|
7878
|
-
import { readFileSync as
|
|
7879
|
-
import { join as
|
|
8117
|
+
import { readFileSync as readFileSync6, writeFileSync as writeFileSync3, readdirSync as readdirSync4 } from "fs";
|
|
8118
|
+
import { join as join13 } from "path";
|
|
8119
|
+
function buildGuideRecommendOptions(root) {
|
|
8120
|
+
const opts = {};
|
|
8121
|
+
try {
|
|
8122
|
+
const handoversDir = join13(root, ".story", "handovers");
|
|
8123
|
+
const files = readdirSync4(handoversDir, "utf-8").filter((f) => f.endsWith(".md")).sort();
|
|
8124
|
+
if (files.length > 0) {
|
|
8125
|
+
opts.latestHandoverContent = readFileSync6(join13(handoversDir, files[files.length - 1]), "utf-8");
|
|
8126
|
+
}
|
|
8127
|
+
} catch {
|
|
8128
|
+
}
|
|
8129
|
+
try {
|
|
8130
|
+
const snapshotsDir = join13(root, ".story", "snapshots");
|
|
8131
|
+
const snapFiles = readdirSync4(snapshotsDir, "utf-8").filter((f) => f.endsWith(".json")).sort();
|
|
8132
|
+
if (snapFiles.length > 0) {
|
|
8133
|
+
const raw = readFileSync6(join13(snapshotsDir, snapFiles[snapFiles.length - 1]), "utf-8");
|
|
8134
|
+
const snap = JSON.parse(raw);
|
|
8135
|
+
if (snap.issues) {
|
|
8136
|
+
opts.previousOpenIssueCount = snap.issues.filter((i) => i.status !== "resolved").length;
|
|
8137
|
+
}
|
|
8138
|
+
}
|
|
8139
|
+
} catch {
|
|
8140
|
+
}
|
|
8141
|
+
return opts;
|
|
8142
|
+
}
|
|
7880
8143
|
async function recoverPendingMutation(dir, state, root) {
|
|
7881
8144
|
const mutation = state.pendingProjectMutation;
|
|
7882
8145
|
if (!mutation || typeof mutation !== "object") return state;
|
|
@@ -8248,7 +8511,7 @@ Staged: ${stagedResult.data.join(", ")}`
|
|
|
8248
8511
|
}
|
|
8249
8512
|
}
|
|
8250
8513
|
const { state: projectState, warnings } = await loadProject(root);
|
|
8251
|
-
const handoversDir =
|
|
8514
|
+
const handoversDir = join13(root, ".story", "handovers");
|
|
8252
8515
|
const ctx = { state: projectState, warnings, root, handoversDir, format: "md" };
|
|
8253
8516
|
let handoverText = "";
|
|
8254
8517
|
try {
|
|
@@ -8265,7 +8528,7 @@ Staged: ${stagedResult.data.join(", ")}`
|
|
|
8265
8528
|
}
|
|
8266
8529
|
} catch {
|
|
8267
8530
|
}
|
|
8268
|
-
const rulesText = readFileSafe2(
|
|
8531
|
+
const rulesText = readFileSafe2(join13(root, "RULES.md"));
|
|
8269
8532
|
const lessonDigest = buildLessonDigest(projectState.lessons);
|
|
8270
8533
|
const digestParts = [
|
|
8271
8534
|
handoverText ? `## Recent Handovers
|
|
@@ -8281,7 +8544,7 @@ ${rulesText}` : "",
|
|
|
8281
8544
|
].filter(Boolean);
|
|
8282
8545
|
const digest = digestParts.join("\n\n---\n\n");
|
|
8283
8546
|
try {
|
|
8284
|
-
writeFileSync3(
|
|
8547
|
+
writeFileSync3(join13(dir, "context-digest.md"), digest, "utf-8");
|
|
8285
8548
|
} catch {
|
|
8286
8549
|
}
|
|
8287
8550
|
if (mode !== "auto" && args.ticketId) {
|
|
@@ -8300,6 +8563,18 @@ ${rulesText}` : "",
|
|
|
8300
8563
|
return guideError(new Error(`Ticket ${args.ticketId} is blocked by: ${ticket.blockedBy.join(", ")}.`));
|
|
8301
8564
|
}
|
|
8302
8565
|
}
|
|
8566
|
+
if (mode !== "review") {
|
|
8567
|
+
const claimId = ticket.claimedBySession;
|
|
8568
|
+
if (claimId && typeof claimId === "string" && claimId !== session.sessionId) {
|
|
8569
|
+
const claimingSession = findSessionById(root, claimId);
|
|
8570
|
+
if (claimingSession && claimingSession.state.status === "active" && !isLeaseExpired(claimingSession.state)) {
|
|
8571
|
+
deleteSession(root, session.sessionId);
|
|
8572
|
+
return guideError(new Error(
|
|
8573
|
+
`Ticket ${args.ticketId} is claimed by active session ${claimId}. Wait for it to finish or stop it with "claudestory session stop ${claimId}".`
|
|
8574
|
+
));
|
|
8575
|
+
}
|
|
8576
|
+
}
|
|
8577
|
+
}
|
|
8303
8578
|
let entryState;
|
|
8304
8579
|
if (mode === "review") {
|
|
8305
8580
|
entryState = "CODE_REVIEW";
|
|
@@ -8395,7 +8670,8 @@ ${ticket.description}` : "",
|
|
|
8395
8670
|
} else {
|
|
8396
8671
|
candidatesText = "No tickets found.";
|
|
8397
8672
|
}
|
|
8398
|
-
const
|
|
8673
|
+
const guideRecOptions = buildGuideRecommendOptions(root);
|
|
8674
|
+
const recResult = recommend(projectState, 5, guideRecOptions);
|
|
8399
8675
|
let recsText = "";
|
|
8400
8676
|
if (recResult.recommendations.length > 0) {
|
|
8401
8677
|
const ticketRecs = recResult.recommendations.filter((r) => r.kind === "ticket");
|
|
@@ -8620,26 +8896,7 @@ async function handleResume(root, args) {
|
|
|
8620
8896
|
));
|
|
8621
8897
|
}
|
|
8622
8898
|
if (expectedHead && headResult.data.hash !== expectedHead) {
|
|
8623
|
-
const
|
|
8624
|
-
PICK_TICKET: { state: "PICK_TICKET", resetPlan: false, resetCode: false },
|
|
8625
|
-
COMPLETE: { state: "PICK_TICKET", resetPlan: false, resetCode: false },
|
|
8626
|
-
HANDOVER: { state: "PICK_TICKET", resetPlan: false, resetCode: false },
|
|
8627
|
-
PLAN: { state: "PLAN", resetPlan: true, resetCode: false },
|
|
8628
|
-
IMPLEMENT: { state: "PLAN", resetPlan: true, resetCode: false },
|
|
8629
|
-
WRITE_TESTS: { state: "PLAN", resetPlan: true, resetCode: false },
|
|
8630
|
-
// T-139: baseline stale after HEAD change
|
|
8631
|
-
VERIFY: { state: "IMPLEMENT", resetPlan: false, resetCode: true },
|
|
8632
|
-
// T-131: reviewed code stale after HEAD drift
|
|
8633
|
-
PLAN_REVIEW: { state: "PLAN", resetPlan: true, resetCode: true },
|
|
8634
|
-
TEST: { state: "IMPLEMENT", resetPlan: false, resetCode: true },
|
|
8635
|
-
// T-128: tests invalidated by HEAD change
|
|
8636
|
-
CODE_REVIEW: { state: "PLAN", resetPlan: true, resetCode: true },
|
|
8637
|
-
FINALIZE: { state: "IMPLEMENT", resetPlan: false, resetCode: true },
|
|
8638
|
-
LESSON_CAPTURE: { state: "PICK_TICKET", resetPlan: false, resetCode: false },
|
|
8639
|
-
ISSUE_SWEEP: { state: "PICK_TICKET", resetPlan: false, resetCode: false }
|
|
8640
|
-
// T-128: post-complete, restart sweep
|
|
8641
|
-
};
|
|
8642
|
-
const mapping = recoveryMapping[resumeState] ?? { state: "PICK_TICKET", resetPlan: false, resetCode: false };
|
|
8899
|
+
const mapping = RECOVERY_MAPPING[resumeState] ?? { state: "PICK_TICKET", resetPlan: false, resetCode: false };
|
|
8643
8900
|
const recoveryReviews = {
|
|
8644
8901
|
plan: mapping.resetPlan ? [] : info.state.reviews.plan,
|
|
8645
8902
|
code: mapping.resetCode ? [] : info.state.reviews.code
|
|
@@ -8987,12 +9244,12 @@ function guideError(err) {
|
|
|
8987
9244
|
}
|
|
8988
9245
|
function readFileSafe2(path2) {
|
|
8989
9246
|
try {
|
|
8990
|
-
return
|
|
9247
|
+
return readFileSync6(path2, "utf-8");
|
|
8991
9248
|
} catch {
|
|
8992
9249
|
return "";
|
|
8993
9250
|
}
|
|
8994
9251
|
}
|
|
8995
|
-
var SEVERITY_MAP, workspaceLocks, MAX_AUTO_ADVANCE_DEPTH;
|
|
9252
|
+
var RECOVERY_MAPPING, SEVERITY_MAP, workspaceLocks, MAX_AUTO_ADVANCE_DEPTH;
|
|
8996
9253
|
var init_guide = __esm({
|
|
8997
9254
|
"src/autonomous/guide.ts"() {
|
|
8998
9255
|
"use strict";
|
|
@@ -9014,6 +9271,22 @@ var init_guide = __esm({
|
|
|
9014
9271
|
init_queries();
|
|
9015
9272
|
init_recommend();
|
|
9016
9273
|
init_handover();
|
|
9274
|
+
RECOVERY_MAPPING = {
|
|
9275
|
+
PICK_TICKET: { state: "PICK_TICKET", resetPlan: false, resetCode: false },
|
|
9276
|
+
COMPLETE: { state: "PICK_TICKET", resetPlan: false, resetCode: false },
|
|
9277
|
+
HANDOVER: { state: "PICK_TICKET", resetPlan: false, resetCode: false },
|
|
9278
|
+
PLAN: { state: "PLAN", resetPlan: true, resetCode: false },
|
|
9279
|
+
IMPLEMENT: { state: "PLAN", resetPlan: true, resetCode: false },
|
|
9280
|
+
WRITE_TESTS: { state: "PLAN", resetPlan: true, resetCode: false },
|
|
9281
|
+
BUILD: { state: "IMPLEMENT", resetPlan: false, resetCode: true },
|
|
9282
|
+
VERIFY: { state: "IMPLEMENT", resetPlan: false, resetCode: true },
|
|
9283
|
+
PLAN_REVIEW: { state: "PLAN", resetPlan: true, resetCode: true },
|
|
9284
|
+
TEST: { state: "IMPLEMENT", resetPlan: false, resetCode: true },
|
|
9285
|
+
CODE_REVIEW: { state: "PLAN", resetPlan: true, resetCode: true },
|
|
9286
|
+
FINALIZE: { state: "IMPLEMENT", resetPlan: false, resetCode: true },
|
|
9287
|
+
LESSON_CAPTURE: { state: "PICK_TICKET", resetPlan: false, resetCode: false },
|
|
9288
|
+
ISSUE_SWEEP: { state: "PICK_TICKET", resetPlan: false, resetCode: false }
|
|
9289
|
+
};
|
|
9017
9290
|
SEVERITY_MAP = {
|
|
9018
9291
|
critical: "critical",
|
|
9019
9292
|
major: "high",
|
|
@@ -9222,8 +9495,8 @@ var init_session_report_formatter = __esm({
|
|
|
9222
9495
|
});
|
|
9223
9496
|
|
|
9224
9497
|
// src/cli/commands/session-report.ts
|
|
9225
|
-
import { readFileSync as
|
|
9226
|
-
import { join as
|
|
9498
|
+
import { readFileSync as readFileSync7, existsSync as existsSync10 } from "fs";
|
|
9499
|
+
import { join as join14 } from "path";
|
|
9227
9500
|
async function handleSessionReport(sessionId, root, format = "md") {
|
|
9228
9501
|
if (!UUID_REGEX.test(sessionId)) {
|
|
9229
9502
|
return {
|
|
@@ -9242,7 +9515,7 @@ async function handleSessionReport(sessionId, root, format = "md") {
|
|
|
9242
9515
|
isError: true
|
|
9243
9516
|
};
|
|
9244
9517
|
}
|
|
9245
|
-
const statePath2 =
|
|
9518
|
+
const statePath2 = join14(dir, "state.json");
|
|
9246
9519
|
if (!existsSync10(statePath2)) {
|
|
9247
9520
|
return {
|
|
9248
9521
|
output: `Error: Session ${sessionId} corrupt \u2014 state.json missing.`,
|
|
@@ -9252,7 +9525,7 @@ async function handleSessionReport(sessionId, root, format = "md") {
|
|
|
9252
9525
|
};
|
|
9253
9526
|
}
|
|
9254
9527
|
try {
|
|
9255
|
-
const rawJson = JSON.parse(
|
|
9528
|
+
const rawJson = JSON.parse(readFileSync7(statePath2, "utf-8"));
|
|
9256
9529
|
if (rawJson && typeof rawJson === "object" && "schemaVersion" in rawJson && rawJson.schemaVersion !== CURRENT_SESSION_SCHEMA_VERSION) {
|
|
9257
9530
|
return {
|
|
9258
9531
|
output: `Error: Session ${sessionId} \u2014 unsupported session schema version ${rawJson.schemaVersion}.`,
|
|
@@ -9281,7 +9554,7 @@ async function handleSessionReport(sessionId, root, format = "md") {
|
|
|
9281
9554
|
const events = readEvents(dir);
|
|
9282
9555
|
let planContent = null;
|
|
9283
9556
|
try {
|
|
9284
|
-
planContent =
|
|
9557
|
+
planContent = readFileSync7(join14(dir, "plan.md"), "utf-8");
|
|
9285
9558
|
} catch {
|
|
9286
9559
|
}
|
|
9287
9560
|
let gitLog = null;
|
|
@@ -9310,7 +9583,7 @@ var init_session_report = __esm({
|
|
|
9310
9583
|
});
|
|
9311
9584
|
|
|
9312
9585
|
// src/cli/commands/phase.ts
|
|
9313
|
-
import { join as
|
|
9586
|
+
import { join as join15, resolve as resolve6 } from "path";
|
|
9314
9587
|
function validatePhaseId(id) {
|
|
9315
9588
|
if (id.length > PHASE_ID_MAX_LENGTH) {
|
|
9316
9589
|
throw new CliValidationError("invalid_input", `Phase ID "${id}" exceeds ${PHASE_ID_MAX_LENGTH} characters`);
|
|
@@ -9499,21 +9772,21 @@ async function handlePhaseDelete(id, reassign, format, root) {
|
|
|
9499
9772
|
const updated = { ...ticket, phase: reassign, order: maxOrder };
|
|
9500
9773
|
const parsed = TicketSchema.parse(updated);
|
|
9501
9774
|
const content = serializeJSON(parsed);
|
|
9502
|
-
const target =
|
|
9775
|
+
const target = join15(wrapDir, "tickets", `${parsed.id}.json`);
|
|
9503
9776
|
operations.push({ op: "write", target, content });
|
|
9504
9777
|
}
|
|
9505
9778
|
for (const issue of affectedIssues) {
|
|
9506
9779
|
const updated = { ...issue, phase: reassign };
|
|
9507
9780
|
const parsed = IssueSchema.parse(updated);
|
|
9508
9781
|
const content = serializeJSON(parsed);
|
|
9509
|
-
const target =
|
|
9782
|
+
const target = join15(wrapDir, "issues", `${parsed.id}.json`);
|
|
9510
9783
|
operations.push({ op: "write", target, content });
|
|
9511
9784
|
}
|
|
9512
9785
|
const newPhases = state.roadmap.phases.filter((p) => p.id !== id);
|
|
9513
9786
|
const newRoadmap = { ...state.roadmap, phases: newPhases };
|
|
9514
9787
|
const parsedRoadmap = RoadmapSchema.parse(newRoadmap);
|
|
9515
9788
|
const roadmapContent = serializeJSON(parsedRoadmap);
|
|
9516
|
-
const roadmapTarget =
|
|
9789
|
+
const roadmapTarget = join15(wrapDir, "roadmap.json");
|
|
9517
9790
|
operations.push({ op: "write", target: roadmapTarget, content: roadmapContent });
|
|
9518
9791
|
await runTransactionUnlocked(root, operations);
|
|
9519
9792
|
} else {
|
|
@@ -9546,14 +9819,14 @@ var init_phase = __esm({
|
|
|
9546
9819
|
|
|
9547
9820
|
// src/mcp/tools.ts
|
|
9548
9821
|
import { z as z10 } from "zod";
|
|
9549
|
-
import { join as
|
|
9822
|
+
import { join as join16 } from "path";
|
|
9550
9823
|
function formatMcpError(code, message) {
|
|
9551
9824
|
return `[${code}] ${message}`;
|
|
9552
9825
|
}
|
|
9553
9826
|
async function runMcpReadTool(pinnedRoot, handler) {
|
|
9554
9827
|
try {
|
|
9555
9828
|
const { state, warnings } = await loadProject(pinnedRoot);
|
|
9556
|
-
const handoversDir =
|
|
9829
|
+
const handoversDir = join16(pinnedRoot, ".story", "handovers");
|
|
9557
9830
|
const ctx = { state, warnings, root: pinnedRoot, handoversDir, format: "md" };
|
|
9558
9831
|
const result = await handler(ctx);
|
|
9559
9832
|
if (result.errorCode && INFRASTRUCTURE_ERROR_CODES.includes(result.errorCode)) {
|
|
@@ -10140,10 +10413,10 @@ var init_tools = __esm({
|
|
|
10140
10413
|
|
|
10141
10414
|
// src/core/init.ts
|
|
10142
10415
|
import { mkdir as mkdir4, stat as stat2, readFile as readFile4, writeFile as writeFile2 } from "fs/promises";
|
|
10143
|
-
import { join as
|
|
10416
|
+
import { join as join17, resolve as resolve7 } from "path";
|
|
10144
10417
|
async function initProject(root, options) {
|
|
10145
10418
|
const absRoot = resolve7(root);
|
|
10146
|
-
const wrapDir =
|
|
10419
|
+
const wrapDir = join17(absRoot, ".story");
|
|
10147
10420
|
let exists = false;
|
|
10148
10421
|
try {
|
|
10149
10422
|
const s = await stat2(wrapDir);
|
|
@@ -10163,11 +10436,11 @@ async function initProject(root, options) {
|
|
|
10163
10436
|
".story/ already exists. Use --force to overwrite config and roadmap."
|
|
10164
10437
|
);
|
|
10165
10438
|
}
|
|
10166
|
-
await mkdir4(
|
|
10167
|
-
await mkdir4(
|
|
10168
|
-
await mkdir4(
|
|
10169
|
-
await mkdir4(
|
|
10170
|
-
await mkdir4(
|
|
10439
|
+
await mkdir4(join17(wrapDir, "tickets"), { recursive: true });
|
|
10440
|
+
await mkdir4(join17(wrapDir, "issues"), { recursive: true });
|
|
10441
|
+
await mkdir4(join17(wrapDir, "handovers"), { recursive: true });
|
|
10442
|
+
await mkdir4(join17(wrapDir, "notes"), { recursive: true });
|
|
10443
|
+
await mkdir4(join17(wrapDir, "lessons"), { recursive: true });
|
|
10171
10444
|
const created = [
|
|
10172
10445
|
".story/config.json",
|
|
10173
10446
|
".story/roadmap.json",
|
|
@@ -10207,7 +10480,7 @@ async function initProject(root, options) {
|
|
|
10207
10480
|
};
|
|
10208
10481
|
await writeConfig(config, absRoot);
|
|
10209
10482
|
await writeRoadmap(roadmap, absRoot);
|
|
10210
|
-
const gitignorePath =
|
|
10483
|
+
const gitignorePath = join17(wrapDir, ".gitignore");
|
|
10211
10484
|
await ensureGitignoreEntries(gitignorePath, STORY_GITIGNORE_ENTRIES);
|
|
10212
10485
|
const warnings = [];
|
|
10213
10486
|
if (options.force && exists) {
|
|
@@ -10255,7 +10528,7 @@ var init_init = __esm({
|
|
|
10255
10528
|
// src/mcp/index.ts
|
|
10256
10529
|
var mcp_exports = {};
|
|
10257
10530
|
import { realpathSync as realpathSync2, existsSync as existsSync11 } from "fs";
|
|
10258
|
-
import { resolve as resolve8, join as
|
|
10531
|
+
import { resolve as resolve8, join as join18, isAbsolute } from "path";
|
|
10259
10532
|
import { z as z11 } from "zod";
|
|
10260
10533
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
10261
10534
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -10270,7 +10543,7 @@ function tryDiscoverRoot() {
|
|
|
10270
10543
|
const resolved = resolve8(envRoot);
|
|
10271
10544
|
try {
|
|
10272
10545
|
const canonical = realpathSync2(resolved);
|
|
10273
|
-
if (existsSync11(
|
|
10546
|
+
if (existsSync11(join18(canonical, CONFIG_PATH2))) {
|
|
10274
10547
|
return canonical;
|
|
10275
10548
|
}
|
|
10276
10549
|
process.stderr.write(`Warning: No .story/config.json at ${canonical}
|
|
@@ -10373,7 +10646,7 @@ var init_mcp = __esm({
|
|
|
10373
10646
|
init_init();
|
|
10374
10647
|
ENV_VAR2 = "CLAUDESTORY_PROJECT_ROOT";
|
|
10375
10648
|
CONFIG_PATH2 = ".story/config.json";
|
|
10376
|
-
version = "0.1.
|
|
10649
|
+
version = "0.1.36";
|
|
10377
10650
|
main().catch((err) => {
|
|
10378
10651
|
process.stderr.write(`Fatal: ${err instanceof Error ? err.message : String(err)}
|
|
10379
10652
|
`);
|
|
@@ -10409,7 +10682,7 @@ __export(run_exports, {
|
|
|
10409
10682
|
runReadCommand: () => runReadCommand,
|
|
10410
10683
|
writeOutput: () => writeOutput
|
|
10411
10684
|
});
|
|
10412
|
-
import { join as
|
|
10685
|
+
import { join as join19 } from "path";
|
|
10413
10686
|
function writeOutput(text) {
|
|
10414
10687
|
try {
|
|
10415
10688
|
process.stdout.write(text + "\n");
|
|
@@ -10437,7 +10710,7 @@ async function runReadCommand(format, handler) {
|
|
|
10437
10710
|
return;
|
|
10438
10711
|
}
|
|
10439
10712
|
const { state, warnings } = await loadProject(root);
|
|
10440
|
-
const handoversDir =
|
|
10713
|
+
const handoversDir = join19(root, ".story", "handovers");
|
|
10441
10714
|
const result = await handler({ state, warnings, root, handoversDir, format });
|
|
10442
10715
|
writeOutput(result.output);
|
|
10443
10716
|
let exitCode = result.exitCode ?? ExitCode.OK;
|
|
@@ -10472,7 +10745,7 @@ async function runDeleteCommand(format, force, handler) {
|
|
|
10472
10745
|
return;
|
|
10473
10746
|
}
|
|
10474
10747
|
const { state, warnings } = await loadProject(root);
|
|
10475
|
-
const handoversDir =
|
|
10748
|
+
const handoversDir = join19(root, ".story", "handovers");
|
|
10476
10749
|
if (!force && hasIntegrityWarnings(warnings)) {
|
|
10477
10750
|
writeOutput(
|
|
10478
10751
|
formatError(
|
|
@@ -10521,6 +10794,102 @@ var init_run = __esm({
|
|
|
10521
10794
|
}
|
|
10522
10795
|
});
|
|
10523
10796
|
|
|
10797
|
+
// src/cli/commands/repair.ts
|
|
10798
|
+
function computeRepairs(state, warnings) {
|
|
10799
|
+
const integrityWarning = warnings.find(
|
|
10800
|
+
(w) => INTEGRITY_WARNING_TYPES.includes(w.type)
|
|
10801
|
+
);
|
|
10802
|
+
if (integrityWarning) {
|
|
10803
|
+
return {
|
|
10804
|
+
fixes: [],
|
|
10805
|
+
error: `Cannot repair: data integrity issue in ${integrityWarning.file}: ${integrityWarning.message}. Fix the corrupt file first, then retry.`,
|
|
10806
|
+
tickets: [],
|
|
10807
|
+
issues: []
|
|
10808
|
+
};
|
|
10809
|
+
}
|
|
10810
|
+
const fixes = [];
|
|
10811
|
+
const modifiedTickets = [];
|
|
10812
|
+
const modifiedIssues = [];
|
|
10813
|
+
const ticketIDs = new Set(state.tickets.map((t) => t.id));
|
|
10814
|
+
const phaseIDs = new Set(state.roadmap.phases.map((p) => {
|
|
10815
|
+
const id = p.id;
|
|
10816
|
+
return typeof id === "object" && id !== null ? id.rawValue ?? String(id) : String(id);
|
|
10817
|
+
}));
|
|
10818
|
+
for (const ticket of state.tickets) {
|
|
10819
|
+
let modified = false;
|
|
10820
|
+
let blockedBy = [...ticket.blockedBy];
|
|
10821
|
+
let parentTicket = ticket.parentTicket;
|
|
10822
|
+
let phase = ticket.phase;
|
|
10823
|
+
const validBlockedBy = blockedBy.filter((ref) => ticketIDs.has(ref));
|
|
10824
|
+
if (validBlockedBy.length < blockedBy.length) {
|
|
10825
|
+
const removed = blockedBy.filter((ref) => !ticketIDs.has(ref));
|
|
10826
|
+
blockedBy = validBlockedBy;
|
|
10827
|
+
modified = true;
|
|
10828
|
+
fixes.push({ entity: ticket.id, field: "blockedBy", description: `Removed stale refs: ${removed.join(", ")}` });
|
|
10829
|
+
}
|
|
10830
|
+
if (parentTicket && !ticketIDs.has(parentTicket)) {
|
|
10831
|
+
fixes.push({ entity: ticket.id, field: "parentTicket", description: `Cleared stale ref: ${parentTicket}` });
|
|
10832
|
+
parentTicket = null;
|
|
10833
|
+
modified = true;
|
|
10834
|
+
}
|
|
10835
|
+
const phaseRaw = typeof phase === "object" && phase !== null ? phase.rawValue ?? String(phase) : phase != null ? String(phase) : null;
|
|
10836
|
+
if (phaseRaw && !phaseIDs.has(phaseRaw)) {
|
|
10837
|
+
fixes.push({ entity: ticket.id, field: "phase", description: `Cleared stale phase: ${phaseRaw}` });
|
|
10838
|
+
phase = null;
|
|
10839
|
+
modified = true;
|
|
10840
|
+
}
|
|
10841
|
+
if (modified) {
|
|
10842
|
+
modifiedTickets.push({ ...ticket, blockedBy, parentTicket, phase });
|
|
10843
|
+
}
|
|
10844
|
+
}
|
|
10845
|
+
for (const issue of state.issues) {
|
|
10846
|
+
let modified = false;
|
|
10847
|
+
let relatedTickets = [...issue.relatedTickets];
|
|
10848
|
+
let phase = issue.phase;
|
|
10849
|
+
const validRelated = relatedTickets.filter((ref) => ticketIDs.has(ref));
|
|
10850
|
+
if (validRelated.length < relatedTickets.length) {
|
|
10851
|
+
const removed = relatedTickets.filter((ref) => !ticketIDs.has(ref));
|
|
10852
|
+
relatedTickets = validRelated;
|
|
10853
|
+
modified = true;
|
|
10854
|
+
fixes.push({ entity: issue.id, field: "relatedTickets", description: `Removed stale refs: ${removed.join(", ")}` });
|
|
10855
|
+
}
|
|
10856
|
+
const issuePhaseRaw = typeof phase === "object" && phase !== null ? phase.rawValue ?? String(phase) : phase != null ? String(phase) : null;
|
|
10857
|
+
if (issuePhaseRaw && !phaseIDs.has(issuePhaseRaw)) {
|
|
10858
|
+
fixes.push({ entity: issue.id, field: "phase", description: `Cleared stale phase: ${issuePhaseRaw}` });
|
|
10859
|
+
phase = null;
|
|
10860
|
+
modified = true;
|
|
10861
|
+
}
|
|
10862
|
+
if (modified) {
|
|
10863
|
+
modifiedIssues.push({ ...issue, relatedTickets, phase });
|
|
10864
|
+
}
|
|
10865
|
+
}
|
|
10866
|
+
return { fixes, tickets: modifiedTickets, issues: modifiedIssues };
|
|
10867
|
+
}
|
|
10868
|
+
function handleRepair(ctx, dryRun) {
|
|
10869
|
+
const { fixes, error } = computeRepairs(ctx.state, ctx.warnings);
|
|
10870
|
+
if (error) {
|
|
10871
|
+
return { output: error, errorCode: "project_corrupt" };
|
|
10872
|
+
}
|
|
10873
|
+
if (fixes.length === 0) {
|
|
10874
|
+
return { output: "No stale references found. Project is clean." };
|
|
10875
|
+
}
|
|
10876
|
+
const lines = [`Found ${fixes.length} stale reference(s)${dryRun ? " (dry run)" : ""}:`, ""];
|
|
10877
|
+
for (const fix of fixes) {
|
|
10878
|
+
lines.push(`- ${fix.entity}.${fix.field}: ${fix.description}`);
|
|
10879
|
+
}
|
|
10880
|
+
if (dryRun) {
|
|
10881
|
+
lines.push("", "Run without --dry-run to apply fixes.");
|
|
10882
|
+
}
|
|
10883
|
+
return { output: lines.join("\n") };
|
|
10884
|
+
}
|
|
10885
|
+
var init_repair = __esm({
|
|
10886
|
+
"src/cli/commands/repair.ts"() {
|
|
10887
|
+
"use strict";
|
|
10888
|
+
init_esm_shims();
|
|
10889
|
+
init_errors();
|
|
10890
|
+
}
|
|
10891
|
+
});
|
|
10892
|
+
|
|
10524
10893
|
// src/cli/commands/init.ts
|
|
10525
10894
|
import { basename as basename2 } from "path";
|
|
10526
10895
|
function registerInitCommand(yargs) {
|
|
@@ -10890,7 +11259,7 @@ __export(setup_skill_exports, {
|
|
|
10890
11259
|
});
|
|
10891
11260
|
import { mkdir as mkdir5, writeFile as writeFile3, readFile as readFile5, rm, rename as rename2, unlink as unlink3 } from "fs/promises";
|
|
10892
11261
|
import { existsSync as existsSync12 } from "fs";
|
|
10893
|
-
import { join as
|
|
11262
|
+
import { join as join20, dirname as dirname4 } from "path";
|
|
10894
11263
|
import { homedir } from "os";
|
|
10895
11264
|
import { execFileSync } from "child_process";
|
|
10896
11265
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
@@ -10899,10 +11268,10 @@ function log(msg) {
|
|
|
10899
11268
|
}
|
|
10900
11269
|
function resolveSkillSourceDir() {
|
|
10901
11270
|
const thisDir = dirname4(fileURLToPath3(import.meta.url));
|
|
10902
|
-
const bundledPath =
|
|
10903
|
-
if (existsSync12(
|
|
10904
|
-
const sourcePath =
|
|
10905
|
-
if (existsSync12(
|
|
11271
|
+
const bundledPath = join20(thisDir, "..", "src", "skill");
|
|
11272
|
+
if (existsSync12(join20(bundledPath, "SKILL.md"))) return bundledPath;
|
|
11273
|
+
const sourcePath = join20(thisDir, "..", "..", "skill");
|
|
11274
|
+
if (existsSync12(join20(sourcePath, "SKILL.md"))) return sourcePath;
|
|
10906
11275
|
throw new Error(
|
|
10907
11276
|
`Cannot find bundled skill files. Checked:
|
|
10908
11277
|
${bundledPath}
|
|
@@ -10915,7 +11284,7 @@ function isHookWithCommand(entry, command) {
|
|
|
10915
11284
|
return e.type === "command" && typeof e.command === "string" && e.command.trim() === command;
|
|
10916
11285
|
}
|
|
10917
11286
|
async function registerHook(hookType, hookEntry, settingsPath, matcher) {
|
|
10918
|
-
const path2 = settingsPath ??
|
|
11287
|
+
const path2 = settingsPath ?? join20(homedir(), ".claude", "settings.json");
|
|
10919
11288
|
let raw = "{}";
|
|
10920
11289
|
if (existsSync12(path2)) {
|
|
10921
11290
|
try {
|
|
@@ -11013,7 +11382,7 @@ async function registerStopHook(settingsPath) {
|
|
|
11013
11382
|
return registerHook("Stop", { type: "command", command: STOP_HOOK_COMMAND, async: true }, settingsPath);
|
|
11014
11383
|
}
|
|
11015
11384
|
async function removeHook(hookType, command, settingsPath) {
|
|
11016
|
-
const path2 = settingsPath ??
|
|
11385
|
+
const path2 = settingsPath ?? join20(homedir(), ".claude", "settings.json");
|
|
11017
11386
|
let raw = "{}";
|
|
11018
11387
|
if (existsSync12(path2)) {
|
|
11019
11388
|
try {
|
|
@@ -11060,7 +11429,7 @@ async function removeHook(hookType, command, settingsPath) {
|
|
|
11060
11429
|
}
|
|
11061
11430
|
async function handleSetupSkill(options = {}) {
|
|
11062
11431
|
const { skipHooks = false } = options;
|
|
11063
|
-
const skillDir =
|
|
11432
|
+
const skillDir = join20(homedir(), ".claude", "skills", "story");
|
|
11064
11433
|
await mkdir5(skillDir, { recursive: true });
|
|
11065
11434
|
let srcSkillDir;
|
|
11066
11435
|
try {
|
|
@@ -11073,19 +11442,19 @@ async function handleSetupSkill(options = {}) {
|
|
|
11073
11442
|
process.exitCode = 1;
|
|
11074
11443
|
return;
|
|
11075
11444
|
}
|
|
11076
|
-
const oldPrimeDir =
|
|
11445
|
+
const oldPrimeDir = join20(homedir(), ".claude", "skills", "prime");
|
|
11077
11446
|
if (existsSync12(oldPrimeDir)) {
|
|
11078
11447
|
await rm(oldPrimeDir, { recursive: true, force: true });
|
|
11079
11448
|
log("Removed old /prime skill (migrated to /story)");
|
|
11080
11449
|
}
|
|
11081
|
-
const existed = existsSync12(
|
|
11082
|
-
const skillContent = await readFile5(
|
|
11083
|
-
await writeFile3(
|
|
11450
|
+
const existed = existsSync12(join20(skillDir, "SKILL.md"));
|
|
11451
|
+
const skillContent = await readFile5(join20(srcSkillDir, "SKILL.md"), "utf-8");
|
|
11452
|
+
await writeFile3(join20(skillDir, "SKILL.md"), skillContent, "utf-8");
|
|
11084
11453
|
let referenceWritten = false;
|
|
11085
|
-
const refSrcPath =
|
|
11454
|
+
const refSrcPath = join20(srcSkillDir, "reference.md");
|
|
11086
11455
|
if (existsSync12(refSrcPath)) {
|
|
11087
11456
|
const refContent = await readFile5(refSrcPath, "utf-8");
|
|
11088
|
-
await writeFile3(
|
|
11457
|
+
await writeFile3(join20(skillDir, "reference.md"), refContent, "utf-8");
|
|
11089
11458
|
referenceWritten = true;
|
|
11090
11459
|
}
|
|
11091
11460
|
log(`${existed ? "Updated" : "Installed"} /story skill at ${skillDir}/`);
|
|
@@ -11199,8 +11568,8 @@ var hook_status_exports = {};
|
|
|
11199
11568
|
__export(hook_status_exports, {
|
|
11200
11569
|
handleHookStatus: () => handleHookStatus
|
|
11201
11570
|
});
|
|
11202
|
-
import { readFileSync as
|
|
11203
|
-
import { join as
|
|
11571
|
+
import { readFileSync as readFileSync8, writeFileSync as writeFileSync4, renameSync as renameSync2, unlinkSync as unlinkSync4 } from "fs";
|
|
11572
|
+
import { join as join21 } from "path";
|
|
11204
11573
|
async function readStdinSilent() {
|
|
11205
11574
|
try {
|
|
11206
11575
|
const chunks = [];
|
|
@@ -11250,10 +11619,10 @@ function activePayload(session) {
|
|
|
11250
11619
|
};
|
|
11251
11620
|
}
|
|
11252
11621
|
function ensureGitignore(root) {
|
|
11253
|
-
const gitignorePath =
|
|
11622
|
+
const gitignorePath = join21(root, ".story", ".gitignore");
|
|
11254
11623
|
let existing = "";
|
|
11255
11624
|
try {
|
|
11256
|
-
existing =
|
|
11625
|
+
existing = readFileSync8(gitignorePath, "utf-8");
|
|
11257
11626
|
} catch {
|
|
11258
11627
|
}
|
|
11259
11628
|
const lines = existing.split("\n").map((l) => l.trim());
|
|
@@ -11269,7 +11638,7 @@ function ensureGitignore(root) {
|
|
|
11269
11638
|
}
|
|
11270
11639
|
function writeStatus(root, payload) {
|
|
11271
11640
|
ensureGitignore(root);
|
|
11272
|
-
const statusPath =
|
|
11641
|
+
const statusPath = join21(root, ".story", "status.json");
|
|
11273
11642
|
const content = JSON.stringify(payload, null, 2) + "\n";
|
|
11274
11643
|
atomicWriteSync(statusPath, content);
|
|
11275
11644
|
}
|
|
@@ -11328,8 +11697,8 @@ var config_update_exports = {};
|
|
|
11328
11697
|
__export(config_update_exports, {
|
|
11329
11698
|
handleConfigSetOverrides: () => handleConfigSetOverrides
|
|
11330
11699
|
});
|
|
11331
|
-
import { readFileSync as
|
|
11332
|
-
import { join as
|
|
11700
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
11701
|
+
import { join as join22 } from "path";
|
|
11333
11702
|
async function handleConfigSetOverrides(root, format, options) {
|
|
11334
11703
|
const { json: jsonArg, clear } = options;
|
|
11335
11704
|
if (!clear && !jsonArg) {
|
|
@@ -11357,8 +11726,8 @@ async function handleConfigSetOverrides(root, format, options) {
|
|
|
11357
11726
|
}
|
|
11358
11727
|
let resultOverrides = null;
|
|
11359
11728
|
await withProjectLock(root, { strict: false }, async () => {
|
|
11360
|
-
const configPath =
|
|
11361
|
-
const rawContent =
|
|
11729
|
+
const configPath = join22(root, ".story", "config.json");
|
|
11730
|
+
const rawContent = readFileSync9(configPath, "utf-8");
|
|
11362
11731
|
const raw = JSON.parse(rawContent);
|
|
11363
11732
|
if (clear) {
|
|
11364
11733
|
delete raw.recipeOverrides;
|
|
@@ -11605,6 +11974,7 @@ __export(register_exports, {
|
|
|
11605
11974
|
registerRecapCommand: () => registerRecapCommand,
|
|
11606
11975
|
registerRecommendCommand: () => registerRecommendCommand,
|
|
11607
11976
|
registerReferenceCommand: () => registerReferenceCommand,
|
|
11977
|
+
registerRepairCommand: () => registerRepairCommand,
|
|
11608
11978
|
registerSelftestCommand: () => registerSelftestCommand,
|
|
11609
11979
|
registerSessionCommand: () => registerSessionCommand,
|
|
11610
11980
|
registerSetupSkillCommand: () => registerSetupSkillCommand,
|
|
@@ -11635,6 +12005,47 @@ function registerValidateCommand(yargs) {
|
|
|
11635
12005
|
}
|
|
11636
12006
|
);
|
|
11637
12007
|
}
|
|
12008
|
+
function registerRepairCommand(yargs) {
|
|
12009
|
+
return yargs.command(
|
|
12010
|
+
"repair",
|
|
12011
|
+
"Fix stale references in .story/ data",
|
|
12012
|
+
(y) => y.option("dry-run", { type: "boolean", default: false, describe: "Show what would be fixed without writing" }),
|
|
12013
|
+
async (argv) => {
|
|
12014
|
+
const dryRun = argv["dry-run"];
|
|
12015
|
+
if (dryRun) {
|
|
12016
|
+
await runReadCommand("md", (ctx) => handleRepair(ctx, true));
|
|
12017
|
+
} else {
|
|
12018
|
+
const { withProjectLock: withProjectLock2, writeTicketUnlocked: writeTicketUnlocked2, writeIssueUnlocked: writeIssueUnlocked2, runTransactionUnlocked: runTransactionUnlocked2 } = await Promise.resolve().then(() => (init_project_loader(), project_loader_exports));
|
|
12019
|
+
const root = (await Promise.resolve().then(() => (init_project_root_discovery(), project_root_discovery_exports))).discoverProjectRoot();
|
|
12020
|
+
await withProjectLock2(root, { strict: false }, async ({ state, warnings }) => {
|
|
12021
|
+
const result = computeRepairs(state, warnings);
|
|
12022
|
+
if (result.error) {
|
|
12023
|
+
writeOutput(result.error);
|
|
12024
|
+
process.exitCode = ExitCode.USER_ERROR;
|
|
12025
|
+
return;
|
|
12026
|
+
}
|
|
12027
|
+
if (result.fixes.length === 0) {
|
|
12028
|
+
writeOutput("No stale references found. Project is clean.");
|
|
12029
|
+
return;
|
|
12030
|
+
}
|
|
12031
|
+
await runTransactionUnlocked2(root, async () => {
|
|
12032
|
+
for (const ticket of result.tickets) {
|
|
12033
|
+
await writeTicketUnlocked2(ticket, root);
|
|
12034
|
+
}
|
|
12035
|
+
for (const issue of result.issues) {
|
|
12036
|
+
await writeIssueUnlocked2(issue, root);
|
|
12037
|
+
}
|
|
12038
|
+
});
|
|
12039
|
+
const lines = [`Fixed ${result.fixes.length} stale reference(s):`, ""];
|
|
12040
|
+
for (const fix of result.fixes) {
|
|
12041
|
+
lines.push(`- ${fix.entity}.${fix.field}: ${fix.description}`);
|
|
12042
|
+
}
|
|
12043
|
+
writeOutput(lines.join("\n"));
|
|
12044
|
+
});
|
|
12045
|
+
}
|
|
12046
|
+
}
|
|
12047
|
+
);
|
|
12048
|
+
}
|
|
11638
12049
|
function registerHandoverCommand(yargs) {
|
|
11639
12050
|
return yargs.command(
|
|
11640
12051
|
"handover",
|
|
@@ -13606,6 +14017,7 @@ var init_register = __esm({
|
|
|
13606
14017
|
init_output_formatter();
|
|
13607
14018
|
init_status();
|
|
13608
14019
|
init_validate();
|
|
14020
|
+
init_repair();
|
|
13609
14021
|
init_handover();
|
|
13610
14022
|
init_blocker();
|
|
13611
14023
|
init_ticket2();
|
|
@@ -13655,9 +14067,10 @@ async function runCli() {
|
|
|
13655
14067
|
registerSetupSkillCommand: registerSetupSkillCommand2,
|
|
13656
14068
|
registerHookStatusCommand: registerHookStatusCommand2,
|
|
13657
14069
|
registerConfigCommand: registerConfigCommand2,
|
|
13658
|
-
registerSessionCommand: registerSessionCommand2
|
|
14070
|
+
registerSessionCommand: registerSessionCommand2,
|
|
14071
|
+
registerRepairCommand: registerRepairCommand2
|
|
13659
14072
|
} = await Promise.resolve().then(() => (init_register(), register_exports));
|
|
13660
|
-
const version2 = "0.1.
|
|
14073
|
+
const version2 = "0.1.36";
|
|
13661
14074
|
class HandledError extends Error {
|
|
13662
14075
|
constructor() {
|
|
13663
14076
|
super("HANDLED_ERROR");
|
|
@@ -13689,6 +14102,7 @@ async function runCli() {
|
|
|
13689
14102
|
cli = registerHandoverCommand2(cli);
|
|
13690
14103
|
cli = registerBlockerCommand2(cli);
|
|
13691
14104
|
cli = registerValidateCommand2(cli);
|
|
14105
|
+
cli = registerRepairCommand2(cli);
|
|
13692
14106
|
cli = registerSnapshotCommand2(cli);
|
|
13693
14107
|
cli = registerRecapCommand2(cli);
|
|
13694
14108
|
cli = registerExportCommand2(cli);
|