@anthropologies/claudestory 0.1.35 → 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/mcp.js CHANGED
@@ -1560,7 +1560,7 @@ function fencedBlock(content, lang) {
1560
1560
  ${content}
1561
1561
  ${fence}`;
1562
1562
  }
1563
- function formatStatus(state, format) {
1563
+ function formatStatus(state, format, activeSessions = []) {
1564
1564
  const phases = phasesWithStatus(state);
1565
1565
  const data = {
1566
1566
  project: state.config.project,
@@ -1580,7 +1580,8 @@ function formatStatus(state, format) {
1580
1580
  name: p.phase.name,
1581
1581
  status: p.status,
1582
1582
  leafCount: p.leafCount
1583
- }))
1583
+ })),
1584
+ ...activeSessions.length > 0 ? { activeSessions } : {}
1584
1585
  };
1585
1586
  if (format === "json") {
1586
1587
  return JSON.stringify(successEnvelope(data), null, 2);
@@ -1602,6 +1603,15 @@ function formatStatus(state, format) {
1602
1603
  const summary = p.phase.summary ?? truncate(p.phase.description, 80);
1603
1604
  lines.push(`${indicator} **${escapeMarkdownInline(p.phase.name)}** (${p.leafCount} tickets) \u2014 ${escapeMarkdownInline(summary)}`);
1604
1605
  }
1606
+ if (activeSessions.length > 0) {
1607
+ lines.push("");
1608
+ lines.push("## Active Sessions");
1609
+ lines.push("");
1610
+ for (const s of activeSessions) {
1611
+ const ticket = s.ticketId ? `${s.ticketId}: ${escapeMarkdownInline(s.ticketTitle ?? "")}` : "no ticket";
1612
+ lines.push(`- ${s.sessionId.slice(0, 8)}: ${s.state} -- ${ticket} (${s.mode} mode)`);
1613
+ }
1614
+ }
1605
1615
  if (state.isEmptyScaffold) {
1606
1616
  lines.push("");
1607
1617
  lines.push(EMPTY_SCAFFOLD_HEADING);
@@ -2501,6 +2511,7 @@ function validateProject(state) {
2501
2511
  }
2502
2512
  }
2503
2513
  }
2514
+ detectSupersedesCycles(state, findings);
2504
2515
  const phaseIDCounts = /* @__PURE__ */ new Map();
2505
2516
  for (const p of state.roadmap.phases) {
2506
2517
  phaseIDCounts.set(p.id, (phaseIDCounts.get(p.id) ?? 0) + 1);
@@ -2705,6 +2716,33 @@ function dfsBlocked(id, state, visited, inStack, findings) {
2705
2716
  inStack.delete(id);
2706
2717
  visited.add(id);
2707
2718
  }
2719
+ function detectSupersedesCycles(state, findings) {
2720
+ const visited = /* @__PURE__ */ new Set();
2721
+ const inStack = /* @__PURE__ */ new Set();
2722
+ for (const l of state.lessons) {
2723
+ if (l.supersedes == null || visited.has(l.id)) continue;
2724
+ dfsSupersedesChain(l.id, state, visited, inStack, findings);
2725
+ }
2726
+ }
2727
+ function dfsSupersedesChain(id, state, visited, inStack, findings) {
2728
+ if (inStack.has(id)) {
2729
+ findings.push({
2730
+ level: "error",
2731
+ code: "supersedes_cycle",
2732
+ message: `Cycle detected in supersedes chain involving ${id}.`,
2733
+ entity: id
2734
+ });
2735
+ return;
2736
+ }
2737
+ if (visited.has(id)) return;
2738
+ inStack.add(id);
2739
+ const lesson = state.lessonByID(id);
2740
+ if (lesson?.supersedes && lesson.supersedes !== id) {
2741
+ dfsSupersedesChain(lesson.supersedes, state, visited, inStack, findings);
2742
+ }
2743
+ inStack.delete(id);
2744
+ visited.add(id);
2745
+ }
2708
2746
  var init_validation = __esm({
2709
2747
  "src/core/validation.ts"() {
2710
2748
  "use strict";
@@ -2723,7 +2761,7 @@ __export(handover_exports, {
2723
2761
  });
2724
2762
  import { existsSync as existsSync4 } from "fs";
2725
2763
  import { mkdir as mkdir2 } from "fs/promises";
2726
- import { join as join4, resolve as resolve4 } from "path";
2764
+ import { join as join5, resolve as resolve4 } from "path";
2727
2765
  function handleHandoverList(ctx) {
2728
2766
  return { output: formatHandoverList(ctx.state.handoverFilenames, ctx.format) };
2729
2767
  }
@@ -2808,15 +2846,15 @@ async function handleHandoverCreate(content, slugRaw, format, root) {
2808
2846
  let filename;
2809
2847
  await withProjectLock(root, { strict: false }, async () => {
2810
2848
  const absRoot = resolve4(root);
2811
- const handoversDir = join4(absRoot, ".story", "handovers");
2849
+ const handoversDir = join5(absRoot, ".story", "handovers");
2812
2850
  await mkdir2(handoversDir, { recursive: true });
2813
- const wrapDir = join4(absRoot, ".story");
2851
+ const wrapDir = join5(absRoot, ".story");
2814
2852
  const datePrefix = `${date}-`;
2815
2853
  const seqRegex = new RegExp(`^${date}-(\\d{2})-`);
2816
2854
  let maxSeq = 0;
2817
- const { readdirSync: readdirSync2 } = await import("fs");
2855
+ const { readdirSync: readdirSync5 } = await import("fs");
2818
2856
  try {
2819
- for (const f of readdirSync2(handoversDir)) {
2857
+ for (const f of readdirSync5(handoversDir)) {
2820
2858
  const m = f.match(seqRegex);
2821
2859
  if (m) {
2822
2860
  const n = parseInt(m[1], 10);
@@ -2833,7 +2871,7 @@ async function handleHandoverCreate(content, slugRaw, format, root) {
2833
2871
  );
2834
2872
  }
2835
2873
  let candidate = `${date}-${String(nextSeq).padStart(2, "0")}-${slug}.md`;
2836
- let candidatePath = join4(handoversDir, candidate);
2874
+ let candidatePath = join5(handoversDir, candidate);
2837
2875
  while (existsSync4(candidatePath)) {
2838
2876
  nextSeq++;
2839
2877
  if (nextSeq > 99) {
@@ -2843,7 +2881,7 @@ async function handleHandoverCreate(content, slugRaw, format, root) {
2843
2881
  );
2844
2882
  }
2845
2883
  candidate = `${date}-${String(nextSeq).padStart(2, "0")}-${slug}.md`;
2846
- candidatePath = join4(handoversDir, candidate);
2884
+ candidatePath = join5(handoversDir, candidate);
2847
2885
  }
2848
2886
  await parseHandoverFilename(candidate, handoversDir);
2849
2887
  await guardPath(candidatePath, wrapDir);
@@ -3158,11 +3196,11 @@ __export(snapshot_exports, {
3158
3196
  });
3159
3197
  import { readdir as readdir3, readFile as readFile3, mkdir as mkdir3, unlink as unlink2 } from "fs/promises";
3160
3198
  import { existsSync as existsSync5 } from "fs";
3161
- import { join as join5, resolve as resolve5 } from "path";
3199
+ import { join as join6, resolve as resolve5 } from "path";
3162
3200
  import { z as z8 } from "zod";
3163
3201
  async function saveSnapshot(root, loadResult) {
3164
3202
  const absRoot = resolve5(root);
3165
- const snapshotsDir = join5(absRoot, ".story", "snapshots");
3203
+ const snapshotsDir = join6(absRoot, ".story", "snapshots");
3166
3204
  await mkdir3(snapshotsDir, { recursive: true });
3167
3205
  const { state, warnings } = loadResult;
3168
3206
  const now = /* @__PURE__ */ new Date();
@@ -3187,8 +3225,8 @@ async function saveSnapshot(root, loadResult) {
3187
3225
  } : {}
3188
3226
  };
3189
3227
  const json = JSON.stringify(snapshot, null, 2) + "\n";
3190
- const targetPath = join5(snapshotsDir, filename);
3191
- const wrapDir = join5(absRoot, ".story");
3228
+ const targetPath = join6(snapshotsDir, filename);
3229
+ const wrapDir = join6(absRoot, ".story");
3192
3230
  await guardPath(targetPath, wrapDir);
3193
3231
  await atomicWrite(targetPath, json);
3194
3232
  const pruned = await pruneSnapshots(snapshotsDir);
@@ -3196,13 +3234,13 @@ async function saveSnapshot(root, loadResult) {
3196
3234
  return { filename, retained: entries.length, pruned };
3197
3235
  }
3198
3236
  async function loadLatestSnapshot(root) {
3199
- const snapshotsDir = join5(resolve5(root), ".story", "snapshots");
3237
+ const snapshotsDir = join6(resolve5(root), ".story", "snapshots");
3200
3238
  if (!existsSync5(snapshotsDir)) return null;
3201
3239
  const files = await listSnapshotFiles(snapshotsDir);
3202
3240
  if (files.length === 0) return null;
3203
3241
  for (const filename of files) {
3204
3242
  try {
3205
- const content = await readFile3(join5(snapshotsDir, filename), "utf-8");
3243
+ const content = await readFile3(join6(snapshotsDir, filename), "utf-8");
3206
3244
  const parsed = JSON.parse(content);
3207
3245
  const snapshot = SnapshotV1Schema.parse(parsed);
3208
3246
  return { snapshot, filename };
@@ -3446,7 +3484,7 @@ async function pruneSnapshots(dir) {
3446
3484
  const toRemove = files.slice(MAX_SNAPSHOTS);
3447
3485
  for (const f of toRemove) {
3448
3486
  try {
3449
- await unlink2(join5(dir, f));
3487
+ await unlink2(join6(dir, f));
3450
3488
  } catch {
3451
3489
  }
3452
3490
  }
@@ -3702,27 +3740,27 @@ __export(session_exports, {
3702
3740
  import { randomUUID } from "crypto";
3703
3741
  import {
3704
3742
  mkdirSync,
3705
- readdirSync,
3706
- readFileSync,
3743
+ readdirSync as readdirSync3,
3744
+ readFileSync as readFileSync3,
3707
3745
  writeFileSync,
3708
3746
  renameSync,
3709
3747
  unlinkSync,
3710
3748
  existsSync as existsSync6,
3711
3749
  rmSync
3712
3750
  } from "fs";
3713
- import { join as join6 } from "path";
3751
+ import { join as join8 } from "path";
3714
3752
  import lockfile2 from "proper-lockfile";
3715
3753
  function sessionsRoot(root) {
3716
- return join6(root, ".story", SESSIONS_DIR);
3754
+ return join8(root, ".story", SESSIONS_DIR);
3717
3755
  }
3718
3756
  function sessionDir(root, sessionId) {
3719
- return join6(sessionsRoot(root), sessionId);
3757
+ return join8(sessionsRoot(root), sessionId);
3720
3758
  }
3721
3759
  function statePath(dir) {
3722
- return join6(dir, "state.json");
3760
+ return join8(dir, "state.json");
3723
3761
  }
3724
3762
  function eventsPath(dir) {
3725
- return join6(dir, "events.log");
3763
+ return join8(dir, "events.log");
3726
3764
  }
3727
3765
  function createSession(root, recipe, workspaceId, configOverrides) {
3728
3766
  const id = randomUUID();
@@ -3778,7 +3816,7 @@ function readSession(dir) {
3778
3816
  const path2 = statePath(dir);
3779
3817
  let raw;
3780
3818
  try {
3781
- raw = readFileSync(path2, "utf-8");
3819
+ raw = readFileSync3(path2, "utf-8");
3782
3820
  } catch {
3783
3821
  return null;
3784
3822
  }
@@ -3821,7 +3859,7 @@ function readEvents(dir) {
3821
3859
  const path2 = eventsPath(dir);
3822
3860
  let raw;
3823
3861
  try {
3824
- raw = readFileSync(path2, "utf-8");
3862
+ raw = readFileSync3(path2, "utf-8");
3825
3863
  } catch {
3826
3864
  return { events: [], malformedCount: 0 };
3827
3865
  }
@@ -3878,7 +3916,7 @@ function findActiveSessionFull(root) {
3878
3916
  const sessDir = sessionsRoot(root);
3879
3917
  let entries;
3880
3918
  try {
3881
- entries = readdirSync(sessDir, { withFileTypes: true });
3919
+ entries = readdirSync3(sessDir, { withFileTypes: true });
3882
3920
  } catch {
3883
3921
  return null;
3884
3922
  }
@@ -3892,7 +3930,7 @@ function findActiveSessionFull(root) {
3892
3930
  let bestGuideCall = 0;
3893
3931
  for (const entry of entries) {
3894
3932
  if (!entry.isDirectory()) continue;
3895
- const dir = join6(sessDir, entry.name);
3933
+ const dir = join8(sessDir, entry.name);
3896
3934
  const session = readSession(dir);
3897
3935
  if (!session) continue;
3898
3936
  if (session.status !== "active") continue;
@@ -3915,7 +3953,7 @@ function findStaleSessions(root) {
3915
3953
  const sessDir = sessionsRoot(root);
3916
3954
  let entries;
3917
3955
  try {
3918
- entries = readdirSync(sessDir, { withFileTypes: true });
3956
+ entries = readdirSync3(sessDir, { withFileTypes: true });
3919
3957
  } catch {
3920
3958
  return [];
3921
3959
  }
@@ -3928,7 +3966,7 @@ function findStaleSessions(root) {
3928
3966
  const results = [];
3929
3967
  for (const entry of entries) {
3930
3968
  if (!entry.isDirectory()) continue;
3931
- const dir = join6(sessDir, entry.name);
3969
+ const dir = join8(sessDir, entry.name);
3932
3970
  const session = readSession(dir);
3933
3971
  if (!session) continue;
3934
3972
  if (session.status !== "active") continue;
@@ -3984,7 +4022,7 @@ function findResumableSession(root) {
3984
4022
  const sessDir = sessionsRoot(root);
3985
4023
  let entries;
3986
4024
  try {
3987
- entries = readdirSync(sessDir, { withFileTypes: true });
4025
+ entries = readdirSync3(sessDir, { withFileTypes: true });
3988
4026
  } catch {
3989
4027
  return null;
3990
4028
  }
@@ -3999,7 +4037,7 @@ function findResumableSession(root) {
3999
4037
  let bestPreparedAt = 0;
4000
4038
  for (const entry of entries) {
4001
4039
  if (!entry.isDirectory()) continue;
4002
- const dir = join6(sessDir, entry.name);
4040
+ const dir = join8(sessDir, entry.name);
4003
4041
  const session = readSession(dir);
4004
4042
  if (!session) continue;
4005
4043
  if (session.status !== "active") continue;
@@ -4023,7 +4061,7 @@ async function withSessionLock(root, fn) {
4023
4061
  release = await lockfile2.lock(sessDir, {
4024
4062
  retries: { retries: 3, minTimeout: 100, maxTimeout: 1e3 },
4025
4063
  stale: 3e4,
4026
- lockfilePath: join6(sessDir, ".lock")
4064
+ lockfilePath: join8(sessDir, ".lock")
4027
4065
  });
4028
4066
  return await fn();
4029
4067
  } finally {
@@ -4049,7 +4087,7 @@ var init_session = __esm({
4049
4087
  // src/mcp/index.ts
4050
4088
  init_esm_shims();
4051
4089
  import { realpathSync as realpathSync2, existsSync as existsSync11 } from "fs";
4052
- import { resolve as resolve8, join as join16, isAbsolute } from "path";
4090
+ import { resolve as resolve8, join as join18, isAbsolute } from "path";
4053
4091
  import { z as z11 } from "zod";
4054
4092
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4055
4093
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -4102,13 +4140,65 @@ init_errors();
4102
4140
  init_helpers();
4103
4141
  init_types();
4104
4142
  import { z as z10 } from "zod";
4105
- import { join as join14 } from "path";
4143
+ import { join as join16 } from "path";
4106
4144
 
4107
4145
  // src/cli/commands/status.ts
4108
4146
  init_esm_shims();
4109
4147
  init_output_formatter();
4148
+
4149
+ // src/core/session-scan.ts
4150
+ init_esm_shims();
4151
+ import { readdirSync, readFileSync } from "fs";
4152
+ import { join as join4 } from "path";
4153
+ function scanActiveSessions(root) {
4154
+ const sessDir = join4(root, ".story", "sessions");
4155
+ let entries;
4156
+ try {
4157
+ entries = readdirSync(sessDir, { withFileTypes: true });
4158
+ } catch {
4159
+ return [];
4160
+ }
4161
+ const results = [];
4162
+ for (const entry of entries) {
4163
+ if (!entry.isDirectory()) continue;
4164
+ const statePath2 = join4(sessDir, entry.name, "state.json");
4165
+ let raw;
4166
+ try {
4167
+ raw = readFileSync(statePath2, "utf-8");
4168
+ } catch {
4169
+ continue;
4170
+ }
4171
+ let parsed;
4172
+ try {
4173
+ parsed = JSON.parse(raw);
4174
+ } catch {
4175
+ continue;
4176
+ }
4177
+ if (parsed.status !== "active") continue;
4178
+ if (parsed.state === "SESSION_END") continue;
4179
+ const lease = parsed.lease;
4180
+ if (lease?.expiresAt) {
4181
+ const expires = new Date(lease.expiresAt).getTime();
4182
+ if (!Number.isNaN(expires) && expires <= Date.now()) continue;
4183
+ } else {
4184
+ continue;
4185
+ }
4186
+ const ticket = parsed.ticket;
4187
+ results.push({
4188
+ sessionId: parsed.sessionId ?? entry.name,
4189
+ state: parsed.state ?? "unknown",
4190
+ mode: parsed.mode ?? "auto",
4191
+ ticketId: ticket?.id ?? null,
4192
+ ticketTitle: ticket?.title ?? null
4193
+ });
4194
+ }
4195
+ return results;
4196
+ }
4197
+
4198
+ // src/cli/commands/status.ts
4110
4199
  function handleStatus(ctx) {
4111
- return { output: formatStatus(ctx.state, ctx.format) };
4200
+ const sessions = scanActiveSessions(ctx.root);
4201
+ return { output: formatStatus(ctx.state, ctx.format, sessions) };
4112
4202
  }
4113
4203
 
4114
4204
  // src/cli/commands/validate.ts
@@ -4708,6 +4798,8 @@ async function handleLessonReinforce(id, format, root) {
4708
4798
 
4709
4799
  // src/cli/commands/recommend.ts
4710
4800
  init_esm_shims();
4801
+ import { readFileSync as readFileSync2, readdirSync as readdirSync2 } from "fs";
4802
+ import { join as join7 } from "path";
4711
4803
 
4712
4804
  // src/core/recommend.ts
4713
4805
  init_esm_shims();
@@ -4728,10 +4820,12 @@ var CATEGORY_PRIORITY = {
4728
4820
  high_impact_unblock: 4,
4729
4821
  near_complete_umbrella: 5,
4730
4822
  phase_momentum: 6,
4731
- quick_win: 7,
4732
- open_issue: 8
4823
+ debt_trend: 7,
4824
+ quick_win: 8,
4825
+ handover_context: 9,
4826
+ open_issue: 10
4733
4827
  };
4734
- function recommend(state, count) {
4828
+ function recommend(state, count, options) {
4735
4829
  const effectiveCount = Math.max(1, Math.min(10, count));
4736
4830
  const dedup = /* @__PURE__ */ new Map();
4737
4831
  const phaseIndex = buildPhaseIndex(state);
@@ -4743,7 +4837,8 @@ function recommend(state, count) {
4743
4837
  () => generateNearCompleteUmbrellas(state, phaseIndex),
4744
4838
  () => generatePhaseMomentum(state),
4745
4839
  () => generateQuickWins(state, phaseIndex),
4746
- () => generateOpenIssues(state)
4840
+ () => generateOpenIssues(state),
4841
+ () => generateDebtTrend(state, options)
4747
4842
  ];
4748
4843
  for (const gen of generators) {
4749
4844
  for (const rec of gen()) {
@@ -4753,6 +4848,7 @@ function recommend(state, count) {
4753
4848
  }
4754
4849
  }
4755
4850
  }
4851
+ applyHandoverBoost(state, dedup, options);
4756
4852
  const curPhase = currentPhase(state);
4757
4853
  const curPhaseIdx = curPhase ? phaseIndex.get(curPhase.id) ?? 0 : 0;
4758
4854
  for (const [id, rec] of dedup) {
@@ -4942,13 +5038,113 @@ function sortByPhaseAndOrder(tickets, phaseIndex) {
4942
5038
  return a.order - b.order;
4943
5039
  });
4944
5040
  }
5041
+ var TICKET_ID_RE = /\bT-\d{3}[a-z]?\b/g;
5042
+ var ACTIONABLE_HEADING_RE = /^#+\s.*(next|open|remaining|todo|blocked)/im;
5043
+ var HANDOVER_BOOST = 50;
5044
+ var HANDOVER_BASE_SCORE = 350;
5045
+ function applyHandoverBoost(state, dedup, options) {
5046
+ if (!options?.latestHandoverContent) return;
5047
+ const content = options.latestHandoverContent;
5048
+ let actionableIds = extractTicketIdsFromActionableSections(content);
5049
+ if (actionableIds.size === 0) {
5050
+ const allIds = new Set(content.match(TICKET_ID_RE) ?? []);
5051
+ for (const id of allIds) {
5052
+ const ticket = state.ticketByID(id);
5053
+ if (ticket && ticket.status !== "complete" && ticket.status !== "inprogress") {
5054
+ actionableIds.add(id);
5055
+ }
5056
+ }
5057
+ }
5058
+ for (const id of actionableIds) {
5059
+ const ticket = state.ticketByID(id);
5060
+ if (!ticket || ticket.status === "complete") continue;
5061
+ const existing = dedup.get(id);
5062
+ if (existing) {
5063
+ dedup.set(id, {
5064
+ ...existing,
5065
+ score: existing.score + HANDOVER_BOOST,
5066
+ reason: existing.reason + " (handover context)"
5067
+ });
5068
+ } else {
5069
+ dedup.set(id, {
5070
+ id,
5071
+ kind: "ticket",
5072
+ title: ticket.title,
5073
+ category: "handover_context",
5074
+ reason: "Referenced in latest handover",
5075
+ score: HANDOVER_BASE_SCORE
5076
+ });
5077
+ }
5078
+ }
5079
+ }
5080
+ function extractTicketIdsFromActionableSections(content) {
5081
+ const ids = /* @__PURE__ */ new Set();
5082
+ const lines = content.split("\n");
5083
+ let inActionable = false;
5084
+ for (const line of lines) {
5085
+ if (/^#+\s/.test(line)) {
5086
+ inActionable = ACTIONABLE_HEADING_RE.test(line);
5087
+ }
5088
+ if (inActionable) {
5089
+ const matches = line.match(TICKET_ID_RE);
5090
+ if (matches) for (const m of matches) ids.add(m);
5091
+ }
5092
+ }
5093
+ return ids;
5094
+ }
5095
+ var DEBT_TREND_SCORE = 450;
5096
+ var DEBT_GROWTH_THRESHOLD = 0.25;
5097
+ var DEBT_ABSOLUTE_MINIMUM = 2;
5098
+ function generateDebtTrend(state, options) {
5099
+ if (options?.previousOpenIssueCount == null) return [];
5100
+ const currentOpen = state.issues.filter((i) => i.status !== "resolved").length;
5101
+ const previous = options.previousOpenIssueCount;
5102
+ if (previous <= 0) return [];
5103
+ const growth = (currentOpen - previous) / previous;
5104
+ const absolute = currentOpen - previous;
5105
+ if (growth > DEBT_GROWTH_THRESHOLD && absolute >= DEBT_ABSOLUTE_MINIMUM) {
5106
+ return [{
5107
+ id: "DEBT_TREND",
5108
+ kind: "action",
5109
+ title: "Issue debt growing",
5110
+ category: "debt_trend",
5111
+ reason: `Open issues grew from ${previous} to ${currentOpen} (+${Math.round(growth * 100)}%). Consider triaging or resolving issues before adding features.`,
5112
+ score: DEBT_TREND_SCORE
5113
+ }];
5114
+ }
5115
+ return [];
5116
+ }
4945
5117
 
4946
5118
  // src/cli/commands/recommend.ts
4947
5119
  init_output_formatter();
4948
5120
  function handleRecommend(ctx, count) {
4949
- const result = recommend(ctx.state, count);
5121
+ const options = buildRecommendOptions(ctx);
5122
+ const result = recommend(ctx.state, count, options);
4950
5123
  return { output: formatRecommendations(result, ctx.state, ctx.format) };
4951
5124
  }
5125
+ function buildRecommendOptions(ctx) {
5126
+ const opts = {};
5127
+ try {
5128
+ const files = readdirSync2(ctx.handoversDir).filter((f) => f.endsWith(".md")).sort();
5129
+ if (files.length > 0) {
5130
+ opts.latestHandoverContent = readFileSync2(join7(ctx.handoversDir, files[files.length - 1]), "utf-8");
5131
+ }
5132
+ } catch {
5133
+ }
5134
+ try {
5135
+ const snapshotsDir = join7(ctx.root, ".story", "snapshots");
5136
+ const snapFiles = readdirSync2(snapshotsDir).filter((f) => f.endsWith(".json")).sort();
5137
+ if (snapFiles.length > 0) {
5138
+ const raw = readFileSync2(join7(snapshotsDir, snapFiles[snapFiles.length - 1]), "utf-8");
5139
+ const snap = JSON.parse(raw);
5140
+ if (snap.issues) {
5141
+ opts.previousOpenIssueCount = snap.issues.filter((i) => i.status !== "resolved").length;
5142
+ }
5143
+ }
5144
+ } catch {
5145
+ }
5146
+ return opts;
5147
+ }
4952
5148
 
4953
5149
  // src/cli/commands/snapshot.ts
4954
5150
  init_esm_shims();
@@ -5238,8 +5434,8 @@ init_handover();
5238
5434
  init_esm_shims();
5239
5435
  init_session_types();
5240
5436
  init_session();
5241
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
5242
- import { join as join11 } from "path";
5437
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync3, readdirSync as readdirSync4 } from "fs";
5438
+ import { join as join13 } from "path";
5243
5439
 
5244
5440
  // src/autonomous/state-machine.ts
5245
5441
  init_esm_shims();
@@ -5511,8 +5707,8 @@ function parseDiffNumstat(out) {
5511
5707
 
5512
5708
  // src/autonomous/recipes/loader.ts
5513
5709
  init_esm_shims();
5514
- import { readFileSync as readFileSync2 } from "fs";
5515
- import { join as join7, dirname as dirname3 } from "path";
5710
+ import { readFileSync as readFileSync4 } from "fs";
5711
+ import { join as join9, dirname as dirname3 } from "path";
5516
5712
  import { fileURLToPath as fileURLToPath2 } from "url";
5517
5713
  var DEFAULT_PIPELINE = [
5518
5714
  "PICK_TICKET",
@@ -5532,9 +5728,9 @@ function loadRecipe(recipeName) {
5532
5728
  if (!/^[A-Za-z0-9_-]+$/.test(recipeName)) {
5533
5729
  throw new Error(`Invalid recipe name: ${recipeName}`);
5534
5730
  }
5535
- const recipesDir = join7(dirname3(fileURLToPath2(import.meta.url)), "..", "recipes");
5536
- const path2 = join7(recipesDir, `${recipeName}.json`);
5537
- const raw = readFileSync2(path2, "utf-8");
5731
+ const recipesDir = join9(dirname3(fileURLToPath2(import.meta.url)), "..", "recipes");
5732
+ const path2 = join9(recipesDir, `${recipeName}.json`);
5733
+ const raw = readFileSync4(path2, "utf-8");
5538
5734
  return JSON.parse(raw);
5539
5735
  }
5540
5736
  function resolveRecipe(recipeName, projectOverrides) {
@@ -5781,7 +5977,7 @@ init_esm_shims();
5781
5977
  // src/autonomous/stages/pick-ticket.ts
5782
5978
  init_esm_shims();
5783
5979
  import { existsSync as existsSync7, unlinkSync as unlinkSync2 } from "fs";
5784
- import { join as join8 } from "path";
5980
+ import { join as join10 } from "path";
5785
5981
  var PickTicketStage = class {
5786
5982
  id = "PICK_TICKET";
5787
5983
  async enter(ctx) {
@@ -5831,7 +6027,7 @@ var PickTicketStage = class {
5831
6027
  return { action: "retry", instruction: `Ticket ${ticketId} is ${ticket.status} \u2014 pick an open ticket.` };
5832
6028
  }
5833
6029
  }
5834
- const planPath = join8(ctx.dir, "plan.md");
6030
+ const planPath = join10(ctx.dir, "plan.md");
5835
6031
  try {
5836
6032
  if (existsSync7(planPath)) unlinkSync2(planPath);
5837
6033
  } catch {
@@ -5870,11 +6066,11 @@ ${ticket.description}` : "",
5870
6066
 
5871
6067
  // src/autonomous/stages/plan.ts
5872
6068
  init_esm_shims();
5873
- import { existsSync as existsSync8, readFileSync as readFileSync3 } from "fs";
5874
- import { join as join9 } from "path";
6069
+ import { existsSync as existsSync8, readFileSync as readFileSync5 } from "fs";
6070
+ import { join as join11 } from "path";
5875
6071
  function readFileSafe(path2) {
5876
6072
  try {
5877
- return readFileSync3(path2, "utf-8");
6073
+ return readFileSync5(path2, "utf-8");
5878
6074
  } catch {
5879
6075
  return "";
5880
6076
  }
@@ -5909,7 +6105,7 @@ var PlanStage = class {
5909
6105
  };
5910
6106
  }
5911
6107
  async report(ctx, _report) {
5912
- const planPath = join9(ctx.dir, "plan.md");
6108
+ const planPath = join11(ctx.dir, "plan.md");
5913
6109
  if (!existsSync8(planPath)) {
5914
6110
  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"] };
5915
6111
  }
@@ -6140,7 +6336,8 @@ var ImplementStage = class {
6140
6336
  reminders: [
6141
6337
  "Follow the plan exactly. Do NOT deviate without re-planning.",
6142
6338
  "Do NOT ask the user for confirmation.",
6143
- "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."
6339
+ "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.",
6340
+ "Track which files you create or modify. Only these files should be staged at commit time."
6144
6341
  ],
6145
6342
  transitionedFrom: ctx.state.previousState ?? void 0
6146
6343
  };
@@ -6798,10 +6995,10 @@ var FinalizeStage = class {
6798
6995
  "Code review passed. Time to commit.",
6799
6996
  "",
6800
6997
  ctx.state.ticket ? `1. Update ticket ${ctx.state.ticket.id} status to "complete" in .story/` : "",
6801
- "2. Stage all changed files (code + .story/ changes)",
6998
+ "2. Stage only the files you created or modified for this ticket (code + .story/ changes). Do NOT use `git add -A` or `git add .`",
6802
6999
  '3. Call me with completedAction: "files_staged"'
6803
7000
  ].filter(Boolean).join("\n"),
6804
- reminders: ["Stage both code changes and .story/ ticket update in the same commit."],
7001
+ reminders: ["Stage both code changes and .story/ ticket update in the same commit. Only stage files related to this ticket."],
6805
7002
  transitionedFrom: ctx.state.previousState ?? void 0
6806
7003
  };
6807
7004
  }
@@ -7285,7 +7482,7 @@ Impact: ${nextIssue.impact}` : ""}` : `Fix issue ${next}.`,
7285
7482
  init_esm_shims();
7286
7483
  init_handover();
7287
7484
  import { writeFileSync as writeFileSync2 } from "fs";
7288
- import { join as join10 } from "path";
7485
+ import { join as join12 } from "path";
7289
7486
  var HandoverStage = class {
7290
7487
  id = "HANDOVER";
7291
7488
  async enter(ctx) {
@@ -7315,7 +7512,7 @@ var HandoverStage = class {
7315
7512
  } catch {
7316
7513
  handoverFailed = true;
7317
7514
  try {
7318
- const fallbackPath = join10(ctx.dir, "handover-fallback.md");
7515
+ const fallbackPath = join12(ctx.dir, "handover-fallback.md");
7319
7516
  writeFileSync2(fallbackPath, content, "utf-8");
7320
7517
  } catch {
7321
7518
  }
@@ -7381,6 +7578,46 @@ init_snapshot();
7381
7578
  init_snapshot();
7382
7579
  init_queries();
7383
7580
  init_handover();
7581
+ var RECOVERY_MAPPING = {
7582
+ PICK_TICKET: { state: "PICK_TICKET", resetPlan: false, resetCode: false },
7583
+ COMPLETE: { state: "PICK_TICKET", resetPlan: false, resetCode: false },
7584
+ HANDOVER: { state: "PICK_TICKET", resetPlan: false, resetCode: false },
7585
+ PLAN: { state: "PLAN", resetPlan: true, resetCode: false },
7586
+ IMPLEMENT: { state: "PLAN", resetPlan: true, resetCode: false },
7587
+ WRITE_TESTS: { state: "PLAN", resetPlan: true, resetCode: false },
7588
+ BUILD: { state: "IMPLEMENT", resetPlan: false, resetCode: true },
7589
+ VERIFY: { state: "IMPLEMENT", resetPlan: false, resetCode: true },
7590
+ PLAN_REVIEW: { state: "PLAN", resetPlan: true, resetCode: true },
7591
+ TEST: { state: "IMPLEMENT", resetPlan: false, resetCode: true },
7592
+ CODE_REVIEW: { state: "PLAN", resetPlan: true, resetCode: true },
7593
+ FINALIZE: { state: "IMPLEMENT", resetPlan: false, resetCode: true },
7594
+ LESSON_CAPTURE: { state: "PICK_TICKET", resetPlan: false, resetCode: false },
7595
+ ISSUE_SWEEP: { state: "PICK_TICKET", resetPlan: false, resetCode: false }
7596
+ };
7597
+ function buildGuideRecommendOptions(root) {
7598
+ const opts = {};
7599
+ try {
7600
+ const handoversDir = join13(root, ".story", "handovers");
7601
+ const files = readdirSync4(handoversDir, "utf-8").filter((f) => f.endsWith(".md")).sort();
7602
+ if (files.length > 0) {
7603
+ opts.latestHandoverContent = readFileSync6(join13(handoversDir, files[files.length - 1]), "utf-8");
7604
+ }
7605
+ } catch {
7606
+ }
7607
+ try {
7608
+ const snapshotsDir = join13(root, ".story", "snapshots");
7609
+ const snapFiles = readdirSync4(snapshotsDir, "utf-8").filter((f) => f.endsWith(".json")).sort();
7610
+ if (snapFiles.length > 0) {
7611
+ const raw = readFileSync6(join13(snapshotsDir, snapFiles[snapFiles.length - 1]), "utf-8");
7612
+ const snap = JSON.parse(raw);
7613
+ if (snap.issues) {
7614
+ opts.previousOpenIssueCount = snap.issues.filter((i) => i.status !== "resolved").length;
7615
+ }
7616
+ }
7617
+ } catch {
7618
+ }
7619
+ return opts;
7620
+ }
7384
7621
  async function recoverPendingMutation(dir, state, root) {
7385
7622
  const mutation = state.pendingProjectMutation;
7386
7623
  if (!mutation || typeof mutation !== "object") return state;
@@ -7758,7 +7995,7 @@ Staged: ${stagedResult.data.join(", ")}`
7758
7995
  }
7759
7996
  }
7760
7997
  const { state: projectState, warnings } = await loadProject(root);
7761
- const handoversDir = join11(root, ".story", "handovers");
7998
+ const handoversDir = join13(root, ".story", "handovers");
7762
7999
  const ctx = { state: projectState, warnings, root, handoversDir, format: "md" };
7763
8000
  let handoverText = "";
7764
8001
  try {
@@ -7775,7 +8012,7 @@ Staged: ${stagedResult.data.join(", ")}`
7775
8012
  }
7776
8013
  } catch {
7777
8014
  }
7778
- const rulesText = readFileSafe2(join11(root, "RULES.md"));
8015
+ const rulesText = readFileSafe2(join13(root, "RULES.md"));
7779
8016
  const lessonDigest = buildLessonDigest(projectState.lessons);
7780
8017
  const digestParts = [
7781
8018
  handoverText ? `## Recent Handovers
@@ -7791,7 +8028,7 @@ ${rulesText}` : "",
7791
8028
  ].filter(Boolean);
7792
8029
  const digest = digestParts.join("\n\n---\n\n");
7793
8030
  try {
7794
- writeFileSync3(join11(dir, "context-digest.md"), digest, "utf-8");
8031
+ writeFileSync3(join13(dir, "context-digest.md"), digest, "utf-8");
7795
8032
  } catch {
7796
8033
  }
7797
8034
  if (mode !== "auto" && args.ticketId) {
@@ -7810,6 +8047,18 @@ ${rulesText}` : "",
7810
8047
  return guideError(new Error(`Ticket ${args.ticketId} is blocked by: ${ticket.blockedBy.join(", ")}.`));
7811
8048
  }
7812
8049
  }
8050
+ if (mode !== "review") {
8051
+ const claimId = ticket.claimedBySession;
8052
+ if (claimId && typeof claimId === "string" && claimId !== session.sessionId) {
8053
+ const claimingSession = findSessionById(root, claimId);
8054
+ if (claimingSession && claimingSession.state.status === "active" && !isLeaseExpired(claimingSession.state)) {
8055
+ deleteSession(root, session.sessionId);
8056
+ return guideError(new Error(
8057
+ `Ticket ${args.ticketId} is claimed by active session ${claimId}. Wait for it to finish or stop it with "claudestory session stop ${claimId}".`
8058
+ ));
8059
+ }
8060
+ }
8061
+ }
7813
8062
  let entryState;
7814
8063
  if (mode === "review") {
7815
8064
  entryState = "CODE_REVIEW";
@@ -7905,7 +8154,8 @@ ${ticket.description}` : "",
7905
8154
  } else {
7906
8155
  candidatesText = "No tickets found.";
7907
8156
  }
7908
- const recResult = recommend(projectState, 5);
8157
+ const guideRecOptions = buildGuideRecommendOptions(root);
8158
+ const recResult = recommend(projectState, 5, guideRecOptions);
7909
8159
  let recsText = "";
7910
8160
  if (recResult.recommendations.length > 0) {
7911
8161
  const ticketRecs = recResult.recommendations.filter((r) => r.kind === "ticket");
@@ -8131,26 +8381,7 @@ async function handleResume(root, args) {
8131
8381
  ));
8132
8382
  }
8133
8383
  if (expectedHead && headResult.data.hash !== expectedHead) {
8134
- const recoveryMapping = {
8135
- PICK_TICKET: { state: "PICK_TICKET", resetPlan: false, resetCode: false },
8136
- COMPLETE: { state: "PICK_TICKET", resetPlan: false, resetCode: false },
8137
- HANDOVER: { state: "PICK_TICKET", resetPlan: false, resetCode: false },
8138
- PLAN: { state: "PLAN", resetPlan: true, resetCode: false },
8139
- IMPLEMENT: { state: "PLAN", resetPlan: true, resetCode: false },
8140
- WRITE_TESTS: { state: "PLAN", resetPlan: true, resetCode: false },
8141
- // T-139: baseline stale after HEAD change
8142
- VERIFY: { state: "IMPLEMENT", resetPlan: false, resetCode: true },
8143
- // T-131: reviewed code stale after HEAD drift
8144
- PLAN_REVIEW: { state: "PLAN", resetPlan: true, resetCode: true },
8145
- TEST: { state: "IMPLEMENT", resetPlan: false, resetCode: true },
8146
- // T-128: tests invalidated by HEAD change
8147
- CODE_REVIEW: { state: "PLAN", resetPlan: true, resetCode: true },
8148
- FINALIZE: { state: "IMPLEMENT", resetPlan: false, resetCode: true },
8149
- LESSON_CAPTURE: { state: "PICK_TICKET", resetPlan: false, resetCode: false },
8150
- ISSUE_SWEEP: { state: "PICK_TICKET", resetPlan: false, resetCode: false }
8151
- // T-128: post-complete, restart sweep
8152
- };
8153
- const mapping = recoveryMapping[resumeState] ?? { state: "PICK_TICKET", resetPlan: false, resetCode: false };
8384
+ const mapping = RECOVERY_MAPPING[resumeState] ?? { state: "PICK_TICKET", resetPlan: false, resetCode: false };
8154
8385
  const recoveryReviews = {
8155
8386
  plan: mapping.resetPlan ? [] : info.state.reviews.plan,
8156
8387
  code: mapping.resetCode ? [] : info.state.reviews.code
@@ -8498,7 +8729,7 @@ function guideError(err) {
8498
8729
  }
8499
8730
  function readFileSafe2(path2) {
8500
8731
  try {
8501
- return readFileSync4(path2, "utf-8");
8732
+ return readFileSync6(path2, "utf-8");
8502
8733
  } catch {
8503
8734
  return "";
8504
8735
  }
@@ -8508,8 +8739,8 @@ function readFileSafe2(path2) {
8508
8739
  init_esm_shims();
8509
8740
  init_session();
8510
8741
  init_session_types();
8511
- import { readFileSync as readFileSync5, existsSync as existsSync10 } from "fs";
8512
- import { join as join12 } from "path";
8742
+ import { readFileSync as readFileSync7, existsSync as existsSync10 } from "fs";
8743
+ import { join as join14 } from "path";
8513
8744
 
8514
8745
  // src/core/session-report-formatter.ts
8515
8746
  init_esm_shims();
@@ -8723,7 +8954,7 @@ async function handleSessionReport(sessionId, root, format = "md") {
8723
8954
  isError: true
8724
8955
  };
8725
8956
  }
8726
- const statePath2 = join12(dir, "state.json");
8957
+ const statePath2 = join14(dir, "state.json");
8727
8958
  if (!existsSync10(statePath2)) {
8728
8959
  return {
8729
8960
  output: `Error: Session ${sessionId} corrupt \u2014 state.json missing.`,
@@ -8733,7 +8964,7 @@ async function handleSessionReport(sessionId, root, format = "md") {
8733
8964
  };
8734
8965
  }
8735
8966
  try {
8736
- const rawJson = JSON.parse(readFileSync5(statePath2, "utf-8"));
8967
+ const rawJson = JSON.parse(readFileSync7(statePath2, "utf-8"));
8737
8968
  if (rawJson && typeof rawJson === "object" && "schemaVersion" in rawJson && rawJson.schemaVersion !== CURRENT_SESSION_SCHEMA_VERSION) {
8738
8969
  return {
8739
8970
  output: `Error: Session ${sessionId} \u2014 unsupported session schema version ${rawJson.schemaVersion}.`,
@@ -8762,7 +8993,7 @@ async function handleSessionReport(sessionId, root, format = "md") {
8762
8993
  const events = readEvents(dir);
8763
8994
  let planContent = null;
8764
8995
  try {
8765
- planContent = readFileSync5(join12(dir, "plan.md"), "utf-8");
8996
+ planContent = readFileSync7(join14(dir, "plan.md"), "utf-8");
8766
8997
  } catch {
8767
8998
  }
8768
8999
  let gitLog = null;
@@ -8787,7 +9018,7 @@ init_issue();
8787
9018
  init_roadmap();
8788
9019
  init_output_formatter();
8789
9020
  init_helpers();
8790
- import { join as join13, resolve as resolve6 } from "path";
9021
+ import { join as join15, resolve as resolve6 } from "path";
8791
9022
  var PHASE_ID_REGEX = /^[a-z0-9]+(-[a-z0-9]+)*$/;
8792
9023
  var PHASE_ID_MAX_LENGTH = 40;
8793
9024
  function validatePhaseId(id) {
@@ -8896,7 +9127,7 @@ function formatMcpError(code, message) {
8896
9127
  async function runMcpReadTool(pinnedRoot, handler) {
8897
9128
  try {
8898
9129
  const { state, warnings } = await loadProject(pinnedRoot);
8899
- const handoversDir = join14(pinnedRoot, ".story", "handovers");
9130
+ const handoversDir = join16(pinnedRoot, ".story", "handovers");
8900
9131
  const ctx = { state, warnings, root: pinnedRoot, handoversDir, format: "md" };
8901
9132
  const result = await handler(ctx);
8902
9133
  if (result.errorCode && INFRASTRUCTURE_ERROR_CODES.includes(result.errorCode)) {
@@ -9453,10 +9684,10 @@ init_esm_shims();
9453
9684
  init_project_loader();
9454
9685
  init_errors();
9455
9686
  import { mkdir as mkdir4, stat as stat2, readFile as readFile4, writeFile as writeFile2 } from "fs/promises";
9456
- import { join as join15, resolve as resolve7 } from "path";
9687
+ import { join as join17, resolve as resolve7 } from "path";
9457
9688
  async function initProject(root, options) {
9458
9689
  const absRoot = resolve7(root);
9459
- const wrapDir = join15(absRoot, ".story");
9690
+ const wrapDir = join17(absRoot, ".story");
9460
9691
  let exists = false;
9461
9692
  try {
9462
9693
  const s = await stat2(wrapDir);
@@ -9476,11 +9707,11 @@ async function initProject(root, options) {
9476
9707
  ".story/ already exists. Use --force to overwrite config and roadmap."
9477
9708
  );
9478
9709
  }
9479
- await mkdir4(join15(wrapDir, "tickets"), { recursive: true });
9480
- await mkdir4(join15(wrapDir, "issues"), { recursive: true });
9481
- await mkdir4(join15(wrapDir, "handovers"), { recursive: true });
9482
- await mkdir4(join15(wrapDir, "notes"), { recursive: true });
9483
- await mkdir4(join15(wrapDir, "lessons"), { recursive: true });
9710
+ await mkdir4(join17(wrapDir, "tickets"), { recursive: true });
9711
+ await mkdir4(join17(wrapDir, "issues"), { recursive: true });
9712
+ await mkdir4(join17(wrapDir, "handovers"), { recursive: true });
9713
+ await mkdir4(join17(wrapDir, "notes"), { recursive: true });
9714
+ await mkdir4(join17(wrapDir, "lessons"), { recursive: true });
9484
9715
  const created = [
9485
9716
  ".story/config.json",
9486
9717
  ".story/roadmap.json",
@@ -9520,7 +9751,7 @@ async function initProject(root, options) {
9520
9751
  };
9521
9752
  await writeConfig(config, absRoot);
9522
9753
  await writeRoadmap(roadmap, absRoot);
9523
- const gitignorePath = join15(wrapDir, ".gitignore");
9754
+ const gitignorePath = join17(wrapDir, ".gitignore");
9524
9755
  await ensureGitignoreEntries(gitignorePath, STORY_GITIGNORE_ENTRIES);
9525
9756
  const warnings = [];
9526
9757
  if (options.force && exists) {
@@ -9559,7 +9790,7 @@ async function ensureGitignoreEntries(gitignorePath, entries) {
9559
9790
  // src/mcp/index.ts
9560
9791
  var ENV_VAR2 = "CLAUDESTORY_PROJECT_ROOT";
9561
9792
  var CONFIG_PATH2 = ".story/config.json";
9562
- var version = "0.1.35";
9793
+ var version = "0.1.36";
9563
9794
  function tryDiscoverRoot() {
9564
9795
  const envRoot = process.env[ENV_VAR2];
9565
9796
  if (envRoot) {
@@ -9571,7 +9802,7 @@ function tryDiscoverRoot() {
9571
9802
  const resolved = resolve8(envRoot);
9572
9803
  try {
9573
9804
  const canonical = realpathSync2(resolved);
9574
- if (existsSync11(join16(canonical, CONFIG_PATH2))) {
9805
+ if (existsSync11(join18(canonical, CONFIG_PATH2))) {
9575
9806
  return canonical;
9576
9807
  }
9577
9808
  process.stderr.write(`Warning: No .story/config.json at ${canonical}