@anthropologies/claudestory 0.1.35 → 0.1.37

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
  }
@@ -3516,6 +3554,7 @@ var init_session_types = __esm({
3516
3554
  "HANDOVER",
3517
3555
  "COMPLETE",
3518
3556
  "LESSON_CAPTURE",
3557
+ "ISSUE_FIX",
3519
3558
  "ISSUE_SWEEP",
3520
3559
  "SESSION_END"
3521
3560
  ];
@@ -3564,6 +3603,14 @@ var init_session_types = __esm({
3564
3603
  timestamp: z9.string()
3565
3604
  })).default([])
3566
3605
  }).default({ plan: [], code: [] }),
3606
+ // T-153: Current issue being fixed (null when working on a ticket)
3607
+ currentIssue: z9.object({
3608
+ id: z9.string(),
3609
+ title: z9.string(),
3610
+ severity: z9.string()
3611
+ }).nullable().default(null),
3612
+ // T-153: Issues resolved this session
3613
+ resolvedIssues: z9.array(z9.string()).default([]),
3567
3614
  // Completed tickets this session
3568
3615
  completedTickets: z9.array(z9.object({
3569
3616
  id: z9.string(),
@@ -3702,27 +3749,27 @@ __export(session_exports, {
3702
3749
  import { randomUUID } from "crypto";
3703
3750
  import {
3704
3751
  mkdirSync,
3705
- readdirSync,
3706
- readFileSync,
3752
+ readdirSync as readdirSync3,
3753
+ readFileSync as readFileSync3,
3707
3754
  writeFileSync,
3708
3755
  renameSync,
3709
3756
  unlinkSync,
3710
3757
  existsSync as existsSync6,
3711
3758
  rmSync
3712
3759
  } from "fs";
3713
- import { join as join6 } from "path";
3760
+ import { join as join8 } from "path";
3714
3761
  import lockfile2 from "proper-lockfile";
3715
3762
  function sessionsRoot(root) {
3716
- return join6(root, ".story", SESSIONS_DIR);
3763
+ return join8(root, ".story", SESSIONS_DIR);
3717
3764
  }
3718
3765
  function sessionDir(root, sessionId) {
3719
- return join6(sessionsRoot(root), sessionId);
3766
+ return join8(sessionsRoot(root), sessionId);
3720
3767
  }
3721
3768
  function statePath(dir) {
3722
- return join6(dir, "state.json");
3769
+ return join8(dir, "state.json");
3723
3770
  }
3724
3771
  function eventsPath(dir) {
3725
- return join6(dir, "events.log");
3772
+ return join8(dir, "events.log");
3726
3773
  }
3727
3774
  function createSession(root, recipe, workspaceId, configOverrides) {
3728
3775
  const id = randomUUID();
@@ -3778,7 +3825,7 @@ function readSession(dir) {
3778
3825
  const path2 = statePath(dir);
3779
3826
  let raw;
3780
3827
  try {
3781
- raw = readFileSync(path2, "utf-8");
3828
+ raw = readFileSync3(path2, "utf-8");
3782
3829
  } catch {
3783
3830
  return null;
3784
3831
  }
@@ -3821,7 +3868,7 @@ function readEvents(dir) {
3821
3868
  const path2 = eventsPath(dir);
3822
3869
  let raw;
3823
3870
  try {
3824
- raw = readFileSync(path2, "utf-8");
3871
+ raw = readFileSync3(path2, "utf-8");
3825
3872
  } catch {
3826
3873
  return { events: [], malformedCount: 0 };
3827
3874
  }
@@ -3878,7 +3925,7 @@ function findActiveSessionFull(root) {
3878
3925
  const sessDir = sessionsRoot(root);
3879
3926
  let entries;
3880
3927
  try {
3881
- entries = readdirSync(sessDir, { withFileTypes: true });
3928
+ entries = readdirSync3(sessDir, { withFileTypes: true });
3882
3929
  } catch {
3883
3930
  return null;
3884
3931
  }
@@ -3892,7 +3939,7 @@ function findActiveSessionFull(root) {
3892
3939
  let bestGuideCall = 0;
3893
3940
  for (const entry of entries) {
3894
3941
  if (!entry.isDirectory()) continue;
3895
- const dir = join6(sessDir, entry.name);
3942
+ const dir = join8(sessDir, entry.name);
3896
3943
  const session = readSession(dir);
3897
3944
  if (!session) continue;
3898
3945
  if (session.status !== "active") continue;
@@ -3915,7 +3962,7 @@ function findStaleSessions(root) {
3915
3962
  const sessDir = sessionsRoot(root);
3916
3963
  let entries;
3917
3964
  try {
3918
- entries = readdirSync(sessDir, { withFileTypes: true });
3965
+ entries = readdirSync3(sessDir, { withFileTypes: true });
3919
3966
  } catch {
3920
3967
  return [];
3921
3968
  }
@@ -3928,7 +3975,7 @@ function findStaleSessions(root) {
3928
3975
  const results = [];
3929
3976
  for (const entry of entries) {
3930
3977
  if (!entry.isDirectory()) continue;
3931
- const dir = join6(sessDir, entry.name);
3978
+ const dir = join8(sessDir, entry.name);
3932
3979
  const session = readSession(dir);
3933
3980
  if (!session) continue;
3934
3981
  if (session.status !== "active") continue;
@@ -3984,7 +4031,7 @@ function findResumableSession(root) {
3984
4031
  const sessDir = sessionsRoot(root);
3985
4032
  let entries;
3986
4033
  try {
3987
- entries = readdirSync(sessDir, { withFileTypes: true });
4034
+ entries = readdirSync3(sessDir, { withFileTypes: true });
3988
4035
  } catch {
3989
4036
  return null;
3990
4037
  }
@@ -3999,7 +4046,7 @@ function findResumableSession(root) {
3999
4046
  let bestPreparedAt = 0;
4000
4047
  for (const entry of entries) {
4001
4048
  if (!entry.isDirectory()) continue;
4002
- const dir = join6(sessDir, entry.name);
4049
+ const dir = join8(sessDir, entry.name);
4003
4050
  const session = readSession(dir);
4004
4051
  if (!session) continue;
4005
4052
  if (session.status !== "active") continue;
@@ -4023,7 +4070,7 @@ async function withSessionLock(root, fn) {
4023
4070
  release = await lockfile2.lock(sessDir, {
4024
4071
  retries: { retries: 3, minTimeout: 100, maxTimeout: 1e3 },
4025
4072
  stale: 3e4,
4026
- lockfilePath: join6(sessDir, ".lock")
4073
+ lockfilePath: join8(sessDir, ".lock")
4027
4074
  });
4028
4075
  return await fn();
4029
4076
  } finally {
@@ -4049,7 +4096,7 @@ var init_session = __esm({
4049
4096
  // src/mcp/index.ts
4050
4097
  init_esm_shims();
4051
4098
  import { realpathSync as realpathSync2, existsSync as existsSync11 } from "fs";
4052
- import { resolve as resolve8, join as join16, isAbsolute } from "path";
4099
+ import { resolve as resolve8, join as join18, isAbsolute } from "path";
4053
4100
  import { z as z11 } from "zod";
4054
4101
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4055
4102
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -4102,13 +4149,65 @@ init_errors();
4102
4149
  init_helpers();
4103
4150
  init_types();
4104
4151
  import { z as z10 } from "zod";
4105
- import { join as join14 } from "path";
4152
+ import { join as join16 } from "path";
4106
4153
 
4107
4154
  // src/cli/commands/status.ts
4108
4155
  init_esm_shims();
4109
4156
  init_output_formatter();
4157
+
4158
+ // src/core/session-scan.ts
4159
+ init_esm_shims();
4160
+ import { readdirSync, readFileSync } from "fs";
4161
+ import { join as join4 } from "path";
4162
+ function scanActiveSessions(root) {
4163
+ const sessDir = join4(root, ".story", "sessions");
4164
+ let entries;
4165
+ try {
4166
+ entries = readdirSync(sessDir, { withFileTypes: true });
4167
+ } catch {
4168
+ return [];
4169
+ }
4170
+ const results = [];
4171
+ for (const entry of entries) {
4172
+ if (!entry.isDirectory()) continue;
4173
+ const statePath2 = join4(sessDir, entry.name, "state.json");
4174
+ let raw;
4175
+ try {
4176
+ raw = readFileSync(statePath2, "utf-8");
4177
+ } catch {
4178
+ continue;
4179
+ }
4180
+ let parsed;
4181
+ try {
4182
+ parsed = JSON.parse(raw);
4183
+ } catch {
4184
+ continue;
4185
+ }
4186
+ if (parsed.status !== "active") continue;
4187
+ if (parsed.state === "SESSION_END") continue;
4188
+ const lease = parsed.lease;
4189
+ if (lease?.expiresAt) {
4190
+ const expires = new Date(lease.expiresAt).getTime();
4191
+ if (!Number.isNaN(expires) && expires <= Date.now()) continue;
4192
+ } else {
4193
+ continue;
4194
+ }
4195
+ const ticket = parsed.ticket;
4196
+ results.push({
4197
+ sessionId: parsed.sessionId ?? entry.name,
4198
+ state: parsed.state ?? "unknown",
4199
+ mode: parsed.mode ?? "auto",
4200
+ ticketId: ticket?.id ?? null,
4201
+ ticketTitle: ticket?.title ?? null
4202
+ });
4203
+ }
4204
+ return results;
4205
+ }
4206
+
4207
+ // src/cli/commands/status.ts
4110
4208
  function handleStatus(ctx) {
4111
- return { output: formatStatus(ctx.state, ctx.format) };
4209
+ const sessions = scanActiveSessions(ctx.root);
4210
+ return { output: formatStatus(ctx.state, ctx.format, sessions) };
4112
4211
  }
4113
4212
 
4114
4213
  // src/cli/commands/validate.ts
@@ -4708,6 +4807,8 @@ async function handleLessonReinforce(id, format, root) {
4708
4807
 
4709
4808
  // src/cli/commands/recommend.ts
4710
4809
  init_esm_shims();
4810
+ import { readFileSync as readFileSync2, readdirSync as readdirSync2 } from "fs";
4811
+ import { join as join7 } from "path";
4711
4812
 
4712
4813
  // src/core/recommend.ts
4713
4814
  init_esm_shims();
@@ -4728,10 +4829,12 @@ var CATEGORY_PRIORITY = {
4728
4829
  high_impact_unblock: 4,
4729
4830
  near_complete_umbrella: 5,
4730
4831
  phase_momentum: 6,
4731
- quick_win: 7,
4732
- open_issue: 8
4832
+ debt_trend: 7,
4833
+ quick_win: 8,
4834
+ handover_context: 9,
4835
+ open_issue: 10
4733
4836
  };
4734
- function recommend(state, count) {
4837
+ function recommend(state, count, options) {
4735
4838
  const effectiveCount = Math.max(1, Math.min(10, count));
4736
4839
  const dedup = /* @__PURE__ */ new Map();
4737
4840
  const phaseIndex = buildPhaseIndex(state);
@@ -4743,7 +4846,8 @@ function recommend(state, count) {
4743
4846
  () => generateNearCompleteUmbrellas(state, phaseIndex),
4744
4847
  () => generatePhaseMomentum(state),
4745
4848
  () => generateQuickWins(state, phaseIndex),
4746
- () => generateOpenIssues(state)
4849
+ () => generateOpenIssues(state),
4850
+ () => generateDebtTrend(state, options)
4747
4851
  ];
4748
4852
  for (const gen of generators) {
4749
4853
  for (const rec of gen()) {
@@ -4753,6 +4857,7 @@ function recommend(state, count) {
4753
4857
  }
4754
4858
  }
4755
4859
  }
4860
+ applyHandoverBoost(state, dedup, options);
4756
4861
  const curPhase = currentPhase(state);
4757
4862
  const curPhaseIdx = curPhase ? phaseIndex.get(curPhase.id) ?? 0 : 0;
4758
4863
  for (const [id, rec] of dedup) {
@@ -4942,13 +5047,113 @@ function sortByPhaseAndOrder(tickets, phaseIndex) {
4942
5047
  return a.order - b.order;
4943
5048
  });
4944
5049
  }
5050
+ var TICKET_ID_RE = /\bT-\d{3}[a-z]?\b/g;
5051
+ var ACTIONABLE_HEADING_RE = /^#+\s.*(next|open|remaining|todo|blocked)/im;
5052
+ var HANDOVER_BOOST = 50;
5053
+ var HANDOVER_BASE_SCORE = 350;
5054
+ function applyHandoverBoost(state, dedup, options) {
5055
+ if (!options?.latestHandoverContent) return;
5056
+ const content = options.latestHandoverContent;
5057
+ let actionableIds = extractTicketIdsFromActionableSections(content);
5058
+ if (actionableIds.size === 0) {
5059
+ const allIds = new Set(content.match(TICKET_ID_RE) ?? []);
5060
+ for (const id of allIds) {
5061
+ const ticket = state.ticketByID(id);
5062
+ if (ticket && ticket.status !== "complete" && ticket.status !== "inprogress") {
5063
+ actionableIds.add(id);
5064
+ }
5065
+ }
5066
+ }
5067
+ for (const id of actionableIds) {
5068
+ const ticket = state.ticketByID(id);
5069
+ if (!ticket || ticket.status === "complete") continue;
5070
+ const existing = dedup.get(id);
5071
+ if (existing) {
5072
+ dedup.set(id, {
5073
+ ...existing,
5074
+ score: existing.score + HANDOVER_BOOST,
5075
+ reason: existing.reason + " (handover context)"
5076
+ });
5077
+ } else {
5078
+ dedup.set(id, {
5079
+ id,
5080
+ kind: "ticket",
5081
+ title: ticket.title,
5082
+ category: "handover_context",
5083
+ reason: "Referenced in latest handover",
5084
+ score: HANDOVER_BASE_SCORE
5085
+ });
5086
+ }
5087
+ }
5088
+ }
5089
+ function extractTicketIdsFromActionableSections(content) {
5090
+ const ids = /* @__PURE__ */ new Set();
5091
+ const lines = content.split("\n");
5092
+ let inActionable = false;
5093
+ for (const line of lines) {
5094
+ if (/^#+\s/.test(line)) {
5095
+ inActionable = ACTIONABLE_HEADING_RE.test(line);
5096
+ }
5097
+ if (inActionable) {
5098
+ const matches = line.match(TICKET_ID_RE);
5099
+ if (matches) for (const m of matches) ids.add(m);
5100
+ }
5101
+ }
5102
+ return ids;
5103
+ }
5104
+ var DEBT_TREND_SCORE = 450;
5105
+ var DEBT_GROWTH_THRESHOLD = 0.25;
5106
+ var DEBT_ABSOLUTE_MINIMUM = 2;
5107
+ function generateDebtTrend(state, options) {
5108
+ if (options?.previousOpenIssueCount == null) return [];
5109
+ const currentOpen = state.issues.filter((i) => i.status !== "resolved").length;
5110
+ const previous = options.previousOpenIssueCount;
5111
+ if (previous <= 0) return [];
5112
+ const growth = (currentOpen - previous) / previous;
5113
+ const absolute = currentOpen - previous;
5114
+ if (growth > DEBT_GROWTH_THRESHOLD && absolute >= DEBT_ABSOLUTE_MINIMUM) {
5115
+ return [{
5116
+ id: "DEBT_TREND",
5117
+ kind: "action",
5118
+ title: "Issue debt growing",
5119
+ category: "debt_trend",
5120
+ reason: `Open issues grew from ${previous} to ${currentOpen} (+${Math.round(growth * 100)}%). Consider triaging or resolving issues before adding features.`,
5121
+ score: DEBT_TREND_SCORE
5122
+ }];
5123
+ }
5124
+ return [];
5125
+ }
4945
5126
 
4946
5127
  // src/cli/commands/recommend.ts
4947
5128
  init_output_formatter();
4948
5129
  function handleRecommend(ctx, count) {
4949
- const result = recommend(ctx.state, count);
5130
+ const options = buildRecommendOptions(ctx);
5131
+ const result = recommend(ctx.state, count, options);
4950
5132
  return { output: formatRecommendations(result, ctx.state, ctx.format) };
4951
5133
  }
5134
+ function buildRecommendOptions(ctx) {
5135
+ const opts = {};
5136
+ try {
5137
+ const files = readdirSync2(ctx.handoversDir).filter((f) => f.endsWith(".md")).sort();
5138
+ if (files.length > 0) {
5139
+ opts.latestHandoverContent = readFileSync2(join7(ctx.handoversDir, files[files.length - 1]), "utf-8");
5140
+ }
5141
+ } catch {
5142
+ }
5143
+ try {
5144
+ const snapshotsDir = join7(ctx.root, ".story", "snapshots");
5145
+ const snapFiles = readdirSync2(snapshotsDir).filter((f) => f.endsWith(".json")).sort();
5146
+ if (snapFiles.length > 0) {
5147
+ const raw = readFileSync2(join7(snapshotsDir, snapFiles[snapFiles.length - 1]), "utf-8");
5148
+ const snap = JSON.parse(raw);
5149
+ if (snap.issues) {
5150
+ opts.previousOpenIssueCount = snap.issues.filter((i) => i.status !== "resolved").length;
5151
+ }
5152
+ }
5153
+ } catch {
5154
+ }
5155
+ return opts;
5156
+ }
4952
5157
 
4953
5158
  // src/cli/commands/snapshot.ts
4954
5159
  init_esm_shims();
@@ -5238,8 +5443,8 @@ init_handover();
5238
5443
  init_esm_shims();
5239
5444
  init_session_types();
5240
5445
  init_session();
5241
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
5242
- import { join as join11 } from "path";
5446
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync3, readdirSync as readdirSync4 } from "fs";
5447
+ import { join as join13 } from "path";
5243
5448
 
5244
5449
  // src/autonomous/state-machine.ts
5245
5450
  init_esm_shims();
@@ -5248,7 +5453,7 @@ var TRANSITIONS = {
5248
5453
  // start does INIT + LOAD_CONTEXT internally
5249
5454
  LOAD_CONTEXT: ["PICK_TICKET"],
5250
5455
  // internal (never seen by Claude)
5251
- PICK_TICKET: ["PLAN", "SESSION_END"],
5456
+ PICK_TICKET: ["PLAN", "ISSUE_FIX", "SESSION_END"],
5252
5457
  PLAN: ["PLAN_REVIEW"],
5253
5458
  PLAN_REVIEW: ["IMPLEMENT", "WRITE_TESTS", "PLAN", "PLAN_REVIEW", "SESSION_END"],
5254
5459
  // approve → IMPLEMENT/WRITE_TESTS, reject → PLAN, stay for next round; SESSION_END for tiered exit
@@ -5262,8 +5467,11 @@ var TRANSITIONS = {
5262
5467
  // approve → VERIFY/FINALIZE, reject → IMPLEMENT/PLAN, stay for next round; SESSION_END for tiered exit
5263
5468
  VERIFY: ["FINALIZE", "IMPLEMENT", "VERIFY"],
5264
5469
  // pass → FINALIZE, fail → IMPLEMENT, retry
5265
- FINALIZE: ["COMPLETE"],
5470
+ FINALIZE: ["COMPLETE", "PICK_TICKET"],
5471
+ // PICK_TICKET for issue-fix flow (bypass COMPLETE)
5266
5472
  COMPLETE: ["PICK_TICKET", "HANDOVER", "ISSUE_SWEEP", "SESSION_END"],
5473
+ ISSUE_FIX: ["FINALIZE", "PICK_TICKET", "ISSUE_FIX"],
5474
+ // T-153: fix done → FINALIZE, cancel → PICK_TICKET, retry self
5267
5475
  LESSON_CAPTURE: ["ISSUE_SWEEP", "HANDOVER", "LESSON_CAPTURE"],
5268
5476
  // advance → ISSUE_SWEEP, retry self, done → HANDOVER
5269
5477
  ISSUE_SWEEP: ["ISSUE_SWEEP", "HANDOVER", "PICK_TICKET"],
@@ -5511,8 +5719,8 @@ function parseDiffNumstat(out) {
5511
5719
 
5512
5720
  // src/autonomous/recipes/loader.ts
5513
5721
  init_esm_shims();
5514
- import { readFileSync as readFileSync2 } from "fs";
5515
- import { join as join7, dirname as dirname3 } from "path";
5722
+ import { readFileSync as readFileSync4 } from "fs";
5723
+ import { join as join9, dirname as dirname3 } from "path";
5516
5724
  import { fileURLToPath as fileURLToPath2 } from "url";
5517
5725
  var DEFAULT_PIPELINE = [
5518
5726
  "PICK_TICKET",
@@ -5532,9 +5740,9 @@ function loadRecipe(recipeName) {
5532
5740
  if (!/^[A-Za-z0-9_-]+$/.test(recipeName)) {
5533
5741
  throw new Error(`Invalid recipe name: ${recipeName}`);
5534
5742
  }
5535
- const recipesDir = join7(dirname3(fileURLToPath2(import.meta.url)), "..", "recipes");
5536
- const path2 = join7(recipesDir, `${recipeName}.json`);
5537
- const raw = readFileSync2(path2, "utf-8");
5743
+ const recipesDir = join9(dirname3(fileURLToPath2(import.meta.url)), "..", "recipes");
5744
+ const path2 = join9(recipesDir, `${recipeName}.json`);
5745
+ const raw = readFileSync4(path2, "utf-8");
5538
5746
  return JSON.parse(raw);
5539
5747
  }
5540
5748
  function resolveRecipe(recipeName, projectOverrides) {
@@ -5781,7 +5989,7 @@ init_esm_shims();
5781
5989
  // src/autonomous/stages/pick-ticket.ts
5782
5990
  init_esm_shims();
5783
5991
  import { existsSync as existsSync7, unlinkSync as unlinkSync2 } from "fs";
5784
- import { join as join8 } from "path";
5992
+ import { join as join10 } from "path";
5785
5993
  var PickTicketStage = class {
5786
5994
  id = "PICK_TICKET";
5787
5995
  async enter(ctx) {
@@ -5794,28 +6002,52 @@ var PickTicketStage = class {
5794
6002
  (c, i) => `${i + 1}. **${c.ticket.id}: ${c.ticket.title}** (${c.ticket.type})`
5795
6003
  ).join("\n");
5796
6004
  }
6005
+ const highIssues = projectState.issues.filter(
6006
+ (i) => i.status === "open" && (i.severity === "critical" || i.severity === "high")
6007
+ );
6008
+ let issuesText = "";
6009
+ if (highIssues.length > 0) {
6010
+ issuesText = "\n\n## Open Issues (high+ severity)\n\n" + highIssues.map(
6011
+ (i, idx) => `${idx + 1}. **${i.id}: ${i.title}** (${i.severity})`
6012
+ ).join("\n");
6013
+ }
5797
6014
  const topCandidate = candidates.kind === "found" ? candidates.candidates[0] : null;
6015
+ const hasIssues = highIssues.length > 0;
5798
6016
  return {
5799
6017
  instruction: [
5800
- "# Pick a Ticket",
6018
+ "# Pick a Ticket or Issue",
6019
+ "",
6020
+ "## Ticket Candidates",
5801
6021
  "",
5802
6022
  candidatesText || "No ticket candidates found.",
6023
+ issuesText,
5803
6024
  "",
5804
- topCandidate ? `Pick **${topCandidate.ticket.id}** (highest priority) by calling \`claudestory_autonomous_guide\` now:` : "Pick a ticket by calling `claudestory_autonomous_guide` now:",
6025
+ topCandidate ? `Pick **${topCandidate.ticket.id}** (highest priority) or an open issue by calling \`claudestory_autonomous_guide\` now:` : hasIssues ? `Pick an issue to fix by calling \`claudestory_autonomous_guide\` now:` : "Pick a ticket by calling `claudestory_autonomous_guide` now:",
5805
6026
  "```json",
5806
6027
  topCandidate ? `{ "sessionId": "${ctx.state.sessionId}", "action": "report", "report": { "completedAction": "ticket_picked", "ticketId": "${topCandidate.ticket.id}" } }` : `{ "sessionId": "${ctx.state.sessionId}", "action": "report", "report": { "completedAction": "ticket_picked", "ticketId": "T-XXX" } }`,
5807
- "```"
6028
+ "```",
6029
+ ...hasIssues ? [
6030
+ "",
6031
+ "Or to fix an issue:",
6032
+ "```json",
6033
+ `{ "sessionId": "${ctx.state.sessionId}", "action": "report", "report": { "completedAction": "issue_picked", "issueId": "${highIssues[0].id}" } }`,
6034
+ "```"
6035
+ ] : []
5808
6036
  ].join("\n"),
5809
6037
  reminders: [
5810
- "Do NOT stop or summarize. Call autonomous_guide IMMEDIATELY to pick a ticket.",
6038
+ "Do NOT stop or summarize. Call autonomous_guide IMMEDIATELY to pick a ticket or issue.",
5811
6039
  "Do NOT ask the user for confirmation."
5812
6040
  ]
5813
6041
  };
5814
6042
  }
5815
6043
  async report(ctx, report) {
6044
+ const issueId = report.issueId;
6045
+ if (issueId) {
6046
+ return this.handleIssuePick(ctx, issueId);
6047
+ }
5816
6048
  const ticketId = report.ticketId;
5817
6049
  if (!ticketId) {
5818
- return { action: "retry", instruction: "report.ticketId is required when picking a ticket." };
6050
+ return { action: "retry", instruction: "report.ticketId or report.issueId is required." };
5819
6051
  }
5820
6052
  const { state: projectState } = await ctx.loadProject();
5821
6053
  const ticket = projectState.ticketByID(ticketId);
@@ -5831,7 +6063,7 @@ var PickTicketStage = class {
5831
6063
  return { action: "retry", instruction: `Ticket ${ticketId} is ${ticket.status} \u2014 pick an open ticket.` };
5832
6064
  }
5833
6065
  }
5834
- const planPath = join8(ctx.dir, "plan.md");
6066
+ const planPath = join10(ctx.dir, "plan.md");
5835
6067
  try {
5836
6068
  if (existsSync7(planPath)) unlinkSync2(planPath);
5837
6069
  } catch {
@@ -5866,15 +6098,33 @@ ${ticket.description}` : "",
5866
6098
  }
5867
6099
  };
5868
6100
  }
6101
+ // T-153: Handle issue pick -- validate and route to ISSUE_FIX
6102
+ async handleIssuePick(ctx, issueId) {
6103
+ const { state: projectState } = await ctx.loadProject();
6104
+ const issue = projectState.issues.find((i) => i.id === issueId);
6105
+ if (!issue) {
6106
+ return { action: "retry", instruction: `Issue ${issueId} not found. Pick a valid issue or ticket.` };
6107
+ }
6108
+ if (issue.status !== "open") {
6109
+ return { action: "retry", instruction: `Issue ${issueId} is ${issue.status}. Pick an open issue.` };
6110
+ }
6111
+ ctx.updateDraft({
6112
+ currentIssue: { id: issue.id, title: issue.title, severity: issue.severity },
6113
+ ticket: void 0,
6114
+ reviews: { plan: [], code: [] },
6115
+ finalizeCheckpoint: null
6116
+ });
6117
+ return { action: "goto", target: "ISSUE_FIX" };
6118
+ }
5869
6119
  };
5870
6120
 
5871
6121
  // src/autonomous/stages/plan.ts
5872
6122
  init_esm_shims();
5873
- import { existsSync as existsSync8, readFileSync as readFileSync3 } from "fs";
5874
- import { join as join9 } from "path";
6123
+ import { existsSync as existsSync8, readFileSync as readFileSync5 } from "fs";
6124
+ import { join as join11 } from "path";
5875
6125
  function readFileSafe(path2) {
5876
6126
  try {
5877
- return readFileSync3(path2, "utf-8");
6127
+ return readFileSync5(path2, "utf-8");
5878
6128
  } catch {
5879
6129
  return "";
5880
6130
  }
@@ -5909,7 +6159,7 @@ var PlanStage = class {
5909
6159
  };
5910
6160
  }
5911
6161
  async report(ctx, _report) {
5912
- const planPath = join9(ctx.dir, "plan.md");
6162
+ const planPath = join11(ctx.dir, "plan.md");
5913
6163
  if (!existsSync8(planPath)) {
5914
6164
  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
6165
  }
@@ -6140,7 +6390,8 @@ var ImplementStage = class {
6140
6390
  reminders: [
6141
6391
  "Follow the plan exactly. Do NOT deviate without re-planning.",
6142
6392
  "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."
6393
+ "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.",
6394
+ "Track which files you create or modify. Only these files should be staged at commit time."
6144
6395
  ],
6145
6396
  transitionedFrom: ctx.state.previousState ?? void 0
6146
6397
  };
@@ -6798,10 +7049,13 @@ var FinalizeStage = class {
6798
7049
  "Code review passed. Time to commit.",
6799
7050
  "",
6800
7051
  ctx.state.ticket ? `1. Update ticket ${ctx.state.ticket.id} status to "complete" in .story/` : "",
6801
- "2. Stage all changed files (code + .story/ changes)",
7052
+ ctx.state.currentIssue ? `1. Ensure issue ${ctx.state.currentIssue.id} status is "resolved" in .story/issues/` : "",
7053
+ "2. Stage only the files you created or modified for this work (code + .story/ changes). Do NOT use `git add -A` or `git add .`",
6802
7054
  '3. Call me with completedAction: "files_staged"'
6803
7055
  ].filter(Boolean).join("\n"),
6804
- reminders: ["Stage both code changes and .story/ ticket update in the same commit."],
7056
+ reminders: [
7057
+ ctx.state.currentIssue ? "Stage both code changes and .story/ issue update in the same commit. Only stage files related to this fix." : "Stage both code changes and .story/ ticket update in the same commit. Only stage files related to this ticket."
7058
+ ],
6805
7059
  transitionedFrom: ctx.state.previousState ?? void 0
6806
7060
  };
6807
7061
  }
@@ -6844,9 +7098,9 @@ var FinalizeStage = class {
6844
7098
  const headResult = await gitHead(ctx.root);
6845
7099
  const previousHead = ctx.state.git.expectedHead ?? ctx.state.git.initHead;
6846
7100
  if (headResult.ok && previousHead && headResult.data.hash !== previousHead) {
7101
+ const treeResult = await gitDiffTreeNames(ctx.root, headResult.data.hash);
6847
7102
  const ticketId2 = ctx.state.ticket?.id;
6848
7103
  if (ticketId2) {
6849
- const treeResult = await gitDiffTreeNames(ctx.root, headResult.data.hash);
6850
7104
  const ticketPath = `.story/tickets/${ticketId2}.json`;
6851
7105
  if (treeResult.ok && !treeResult.data.includes(ticketPath)) {
6852
7106
  return {
@@ -6855,6 +7109,16 @@ var FinalizeStage = class {
6855
7109
  };
6856
7110
  }
6857
7111
  }
7112
+ const earlyIssueId = ctx.state.currentIssue?.id;
7113
+ if (earlyIssueId) {
7114
+ const issuePath = `.story/issues/${earlyIssueId}.json`;
7115
+ if (treeResult.ok && !treeResult.data.includes(issuePath)) {
7116
+ return {
7117
+ action: "retry",
7118
+ instruction: `Commit detected (${headResult.data.hash.slice(0, 7)}) but issue file ${issuePath} is not in the commit. Amend the commit to include it: \`git add ${issuePath} && git commit --amend --no-edit\`, then report completedAction: "commit_done" with the new hash.`
7119
+ };
7120
+ }
7121
+ }
6858
7122
  ctx.writeState({ finalizeCheckpoint: "precommit_passed" });
6859
7123
  return this.handleCommit(ctx, { ...report, commitHash: headResult.data.hash });
6860
7124
  }
@@ -6888,6 +7152,16 @@ var FinalizeStage = class {
6888
7152
  };
6889
7153
  }
6890
7154
  }
7155
+ const issueId = ctx.state.currentIssue?.id;
7156
+ if (issueId) {
7157
+ const issuePath = `.story/issues/${issueId}.json`;
7158
+ if (!stagedResult.data.includes(issuePath)) {
7159
+ return {
7160
+ action: "retry",
7161
+ instruction: `Issue file ${issuePath} is not staged. Run \`git add ${issuePath}\` and call me again with completedAction: "files_staged".`
7162
+ };
7163
+ }
7164
+ }
6891
7165
  ctx.writeState({
6892
7166
  finalizeCheckpoint: overlapOverridden ? "staged_override" : "staged"
6893
7167
  });
@@ -6935,6 +7209,16 @@ var FinalizeStage = class {
6935
7209
  };
6936
7210
  }
6937
7211
  }
7212
+ const precommitIssueId = ctx.state.currentIssue?.id;
7213
+ if (precommitIssueId) {
7214
+ const issuePath = `.story/issues/${precommitIssueId}.json`;
7215
+ if (!stagedResult.data.includes(issuePath)) {
7216
+ return {
7217
+ action: "retry",
7218
+ instruction: `Pre-commit hooks may have modified the staged set. Issue file ${issuePath} is no longer staged. Run \`git add ${issuePath}\` and call me again with completedAction: "files_staged".`
7219
+ };
7220
+ }
7221
+ }
6938
7222
  ctx.writeState({ finalizeCheckpoint: "precommit_passed" });
6939
7223
  return {
6940
7224
  action: "retry",
@@ -6972,6 +7256,21 @@ var FinalizeStage = class {
6972
7256
  if (previousHead && normalizedHash === previousHead) {
6973
7257
  return { action: "retry", instruction: `No new commit detected: HEAD (${normalizedHash}) has not changed. Create a commit first, then report the new hash.` };
6974
7258
  }
7259
+ const currentIssue = ctx.state.currentIssue;
7260
+ if (currentIssue) {
7261
+ ctx.writeState({
7262
+ finalizeCheckpoint: "committed",
7263
+ resolvedIssues: [...ctx.state.resolvedIssues ?? [], currentIssue.id],
7264
+ currentIssue: null,
7265
+ git: {
7266
+ ...ctx.state.git,
7267
+ mergeBase: normalizedHash,
7268
+ expectedHead: normalizedHash
7269
+ }
7270
+ });
7271
+ ctx.appendEvent("commit", { commitHash: normalizedHash, issueId: currentIssue.id });
7272
+ return { action: "goto", target: "PICK_TICKET" };
7273
+ }
6975
7274
  const completedTicket = ctx.state.ticket ? { id: ctx.state.ticket.id, title: ctx.state.ticket.title, commitHash: normalizedHash, risk: ctx.state.ticket.risk, realizedRisk: ctx.state.ticket.realizedRisk } : void 0;
6976
7275
  ctx.writeState({
6977
7276
  finalizeCheckpoint: "committed",
@@ -7180,6 +7479,79 @@ var LessonCaptureStage = class {
7180
7479
  }
7181
7480
  };
7182
7481
 
7482
+ // src/autonomous/stages/issue-fix.ts
7483
+ init_esm_shims();
7484
+ var IssueFixStage = class {
7485
+ id = "ISSUE_FIX";
7486
+ async enter(ctx) {
7487
+ const issue = ctx.state.currentIssue;
7488
+ if (!issue) {
7489
+ return { action: "goto", target: "PICK_TICKET" };
7490
+ }
7491
+ const { state: projectState } = await ctx.loadProject();
7492
+ const fullIssue = projectState.issues.find((i) => i.id === issue.id);
7493
+ const details = fullIssue ? [
7494
+ `**${fullIssue.id}**: ${fullIssue.title}`,
7495
+ "",
7496
+ `Severity: ${fullIssue.severity}`,
7497
+ fullIssue.impact ? `Impact: ${fullIssue.impact}` : "",
7498
+ fullIssue.components.length > 0 ? `Components: ${fullIssue.components.join(", ")}` : "",
7499
+ fullIssue.location.length > 0 ? `Location: ${fullIssue.location.join(", ")}` : ""
7500
+ ].filter(Boolean).join("\n") : `**${issue.id}**: ${issue.title} (severity: ${issue.severity})`;
7501
+ return {
7502
+ instruction: [
7503
+ "# Fix Issue",
7504
+ "",
7505
+ details,
7506
+ "",
7507
+ 'Fix this issue, then update its status to "resolved" in `.story/issues/`.',
7508
+ "Add a resolution description explaining the fix.",
7509
+ "",
7510
+ "When done, call `claudestory_autonomous_guide` with:",
7511
+ "```json",
7512
+ `{ "sessionId": "${ctx.state.sessionId}", "action": "report", "report": { "completedAction": "issue_fixed" } }`,
7513
+ "```"
7514
+ ].join("\n"),
7515
+ reminders: [
7516
+ 'Update the issue JSON: set status to "resolved", add resolution text, set resolvedDate.',
7517
+ "Do NOT ask the user for confirmation."
7518
+ ]
7519
+ };
7520
+ }
7521
+ async report(ctx, _report) {
7522
+ const issue = ctx.state.currentIssue;
7523
+ if (!issue) {
7524
+ return { action: "goto", target: "PICK_TICKET" };
7525
+ }
7526
+ const { state: projectState } = await ctx.loadProject();
7527
+ const current = projectState.issues.find((i) => i.id === issue.id);
7528
+ if (!current || current.status !== "resolved") {
7529
+ return {
7530
+ action: "retry",
7531
+ instruction: `Issue ${issue.id} is still ${current?.status ?? "missing"}. Update its status to "resolved" in .story/issues/${issue.id}.json with a resolution description and resolvedDate, then report again.`,
7532
+ reminders: ["Set status to 'resolved', add resolution text, set resolvedDate."]
7533
+ };
7534
+ }
7535
+ return {
7536
+ action: "goto",
7537
+ target: "FINALIZE",
7538
+ result: {
7539
+ instruction: [
7540
+ "# Finalize Issue Fix",
7541
+ "",
7542
+ `Issue ${issue.id} resolved. Time to commit.`,
7543
+ "",
7544
+ `1. Ensure .story/issues/${issue.id}.json is updated with status: "resolved"`,
7545
+ "2. Stage only the files you modified for this fix (code + .story/ changes). Do NOT use `git add -A` or `git add .`",
7546
+ '3. Call me with completedAction: "files_staged"'
7547
+ ].join("\n"),
7548
+ reminders: ["Stage both code changes and .story/ issue update in the same commit. Only stage files related to this fix."],
7549
+ transitionedFrom: "ISSUE_FIX"
7550
+ }
7551
+ };
7552
+ }
7553
+ };
7554
+
7183
7555
  // src/autonomous/stages/issue-sweep.ts
7184
7556
  init_esm_shims();
7185
7557
  var IssueSweepStage = class {
@@ -7285,7 +7657,7 @@ Impact: ${nextIssue.impact}` : ""}` : `Fix issue ${next}.`,
7285
7657
  init_esm_shims();
7286
7658
  init_handover();
7287
7659
  import { writeFileSync as writeFileSync2 } from "fs";
7288
- import { join as join10 } from "path";
7660
+ import { join as join12 } from "path";
7289
7661
  var HandoverStage = class {
7290
7662
  id = "HANDOVER";
7291
7663
  async enter(ctx) {
@@ -7315,7 +7687,7 @@ var HandoverStage = class {
7315
7687
  } catch {
7316
7688
  handoverFailed = true;
7317
7689
  try {
7318
- const fallbackPath = join10(ctx.dir, "handover-fallback.md");
7690
+ const fallbackPath = join12(ctx.dir, "handover-fallback.md");
7319
7691
  writeFileSync2(fallbackPath, content, "utf-8");
7320
7692
  } catch {
7321
7693
  }
@@ -7372,6 +7744,7 @@ registerStage(new VerifyStage());
7372
7744
  registerStage(new FinalizeStage());
7373
7745
  registerStage(new CompleteStage());
7374
7746
  registerStage(new LessonCaptureStage());
7747
+ registerStage(new IssueFixStage());
7375
7748
  registerStage(new IssueSweepStage());
7376
7749
  registerStage(new HandoverStage());
7377
7750
 
@@ -7381,6 +7754,47 @@ init_snapshot();
7381
7754
  init_snapshot();
7382
7755
  init_queries();
7383
7756
  init_handover();
7757
+ var RECOVERY_MAPPING = {
7758
+ PICK_TICKET: { state: "PICK_TICKET", resetPlan: false, resetCode: false },
7759
+ COMPLETE: { state: "PICK_TICKET", resetPlan: false, resetCode: false },
7760
+ HANDOVER: { state: "PICK_TICKET", resetPlan: false, resetCode: false },
7761
+ PLAN: { state: "PLAN", resetPlan: true, resetCode: false },
7762
+ IMPLEMENT: { state: "PLAN", resetPlan: true, resetCode: false },
7763
+ WRITE_TESTS: { state: "PLAN", resetPlan: true, resetCode: false },
7764
+ BUILD: { state: "IMPLEMENT", resetPlan: false, resetCode: true },
7765
+ VERIFY: { state: "IMPLEMENT", resetPlan: false, resetCode: true },
7766
+ PLAN_REVIEW: { state: "PLAN", resetPlan: true, resetCode: true },
7767
+ TEST: { state: "IMPLEMENT", resetPlan: false, resetCode: true },
7768
+ CODE_REVIEW: { state: "PLAN", resetPlan: true, resetCode: true },
7769
+ FINALIZE: { state: "IMPLEMENT", resetPlan: false, resetCode: true },
7770
+ LESSON_CAPTURE: { state: "PICK_TICKET", resetPlan: false, resetCode: false },
7771
+ ISSUE_FIX: { state: "PICK_TICKET", resetPlan: false, resetCode: false },
7772
+ ISSUE_SWEEP: { state: "PICK_TICKET", resetPlan: false, resetCode: false }
7773
+ };
7774
+ function buildGuideRecommendOptions(root) {
7775
+ const opts = {};
7776
+ try {
7777
+ const handoversDir = join13(root, ".story", "handovers");
7778
+ const files = readdirSync4(handoversDir, "utf-8").filter((f) => f.endsWith(".md")).sort();
7779
+ if (files.length > 0) {
7780
+ opts.latestHandoverContent = readFileSync6(join13(handoversDir, files[files.length - 1]), "utf-8");
7781
+ }
7782
+ } catch {
7783
+ }
7784
+ try {
7785
+ const snapshotsDir = join13(root, ".story", "snapshots");
7786
+ const snapFiles = readdirSync4(snapshotsDir, "utf-8").filter((f) => f.endsWith(".json")).sort();
7787
+ if (snapFiles.length > 0) {
7788
+ const raw = readFileSync6(join13(snapshotsDir, snapFiles[snapFiles.length - 1]), "utf-8");
7789
+ const snap = JSON.parse(raw);
7790
+ if (snap.issues) {
7791
+ opts.previousOpenIssueCount = snap.issues.filter((i) => i.status !== "resolved").length;
7792
+ }
7793
+ }
7794
+ } catch {
7795
+ }
7796
+ return opts;
7797
+ }
7384
7798
  async function recoverPendingMutation(dir, state, root) {
7385
7799
  const mutation = state.pendingProjectMutation;
7386
7800
  if (!mutation || typeof mutation !== "object") return state;
@@ -7758,7 +8172,7 @@ Staged: ${stagedResult.data.join(", ")}`
7758
8172
  }
7759
8173
  }
7760
8174
  const { state: projectState, warnings } = await loadProject(root);
7761
- const handoversDir = join11(root, ".story", "handovers");
8175
+ const handoversDir = join13(root, ".story", "handovers");
7762
8176
  const ctx = { state: projectState, warnings, root, handoversDir, format: "md" };
7763
8177
  let handoverText = "";
7764
8178
  try {
@@ -7775,7 +8189,7 @@ Staged: ${stagedResult.data.join(", ")}`
7775
8189
  }
7776
8190
  } catch {
7777
8191
  }
7778
- const rulesText = readFileSafe2(join11(root, "RULES.md"));
8192
+ const rulesText = readFileSafe2(join13(root, "RULES.md"));
7779
8193
  const lessonDigest = buildLessonDigest(projectState.lessons);
7780
8194
  const digestParts = [
7781
8195
  handoverText ? `## Recent Handovers
@@ -7791,7 +8205,7 @@ ${rulesText}` : "",
7791
8205
  ].filter(Boolean);
7792
8206
  const digest = digestParts.join("\n\n---\n\n");
7793
8207
  try {
7794
- writeFileSync3(join11(dir, "context-digest.md"), digest, "utf-8");
8208
+ writeFileSync3(join13(dir, "context-digest.md"), digest, "utf-8");
7795
8209
  } catch {
7796
8210
  }
7797
8211
  if (mode !== "auto" && args.ticketId) {
@@ -7810,6 +8224,18 @@ ${rulesText}` : "",
7810
8224
  return guideError(new Error(`Ticket ${args.ticketId} is blocked by: ${ticket.blockedBy.join(", ")}.`));
7811
8225
  }
7812
8226
  }
8227
+ if (mode !== "review") {
8228
+ const claimId = ticket.claimedBySession;
8229
+ if (claimId && typeof claimId === "string" && claimId !== session.sessionId) {
8230
+ const claimingSession = findSessionById(root, claimId);
8231
+ if (claimingSession && claimingSession.state.status === "active" && !isLeaseExpired(claimingSession.state)) {
8232
+ deleteSession(root, session.sessionId);
8233
+ return guideError(new Error(
8234
+ `Ticket ${args.ticketId} is claimed by active session ${claimId}. Wait for it to finish or stop it with "claudestory session stop ${claimId}".`
8235
+ ));
8236
+ }
8237
+ }
8238
+ }
7813
8239
  let entryState;
7814
8240
  if (mode === "review") {
7815
8241
  entryState = "CODE_REVIEW";
@@ -7905,12 +8331,22 @@ ${ticket.description}` : "",
7905
8331
  } else {
7906
8332
  candidatesText = "No tickets found.";
7907
8333
  }
7908
- const recResult = recommend(projectState, 5);
8334
+ const highIssues = projectState.issues.filter(
8335
+ (i) => i.status === "open" && (i.severity === "critical" || i.severity === "high")
8336
+ );
8337
+ let issuesText = "";
8338
+ if (highIssues.length > 0) {
8339
+ issuesText = "\n\n## Open Issues (high+ severity)\n\n" + highIssues.map(
8340
+ (i, idx) => `${idx + 1}. **${i.id}: ${i.title}** (${i.severity})`
8341
+ ).join("\n");
8342
+ }
8343
+ const guideRecOptions = buildGuideRecommendOptions(root);
8344
+ const recResult = recommend(projectState, 5, guideRecOptions);
7909
8345
  let recsText = "";
7910
8346
  if (recResult.recommendations.length > 0) {
7911
- const ticketRecs = recResult.recommendations.filter((r) => r.kind === "ticket");
7912
- if (ticketRecs.length > 0) {
7913
- recsText = "\n\n**Recommended:**\n" + ticketRecs.map(
8347
+ const actionableRecs = recResult.recommendations.filter((r) => r.kind === "ticket" || r.kind === "issue");
8348
+ if (actionableRecs.length > 0) {
8349
+ recsText = "\n\n**Recommended:**\n" + actionableRecs.map(
7914
8350
  (r) => `- ${r.id}: ${r.title} (${r.reason})`
7915
8351
  ).join("\n");
7916
8352
  }
@@ -7930,21 +8366,30 @@ ${ticket.description}` : "",
7930
8366
  const interval = updated.config.handoverInterval ?? 3;
7931
8367
  const sessionDesc = maxTickets > 0 ? `Work continuously until all tickets are done or you reach ${maxTickets} tickets.` : "Work continuously until all tickets are done.";
7932
8368
  const checkpointDesc = interval > 0 ? ` A checkpoint handover will be saved every ${interval} tickets.` : "";
8369
+ const hasHighIssues = highIssues.length > 0;
7933
8370
  const instruction = [
7934
8371
  "# Autonomous Session Started",
7935
8372
  "",
7936
8373
  `You are now in autonomous mode. ${sessionDesc}${checkpointDesc}`,
7937
- "Do NOT stop to summarize. Do NOT ask the user. Pick a ticket and start working immediately.",
8374
+ "Do NOT stop to summarize. Do NOT ask the user. Pick a ticket or issue and start working immediately.",
7938
8375
  "",
7939
8376
  "## Ticket Candidates",
7940
8377
  "",
7941
8378
  candidatesText,
8379
+ issuesText,
7942
8380
  recsText,
7943
8381
  "",
7944
- topCandidate ? `Pick **${topCandidate.ticket.id}** (highest priority) by calling \`claudestory_autonomous_guide\` now:` : "Pick a ticket by calling `claudestory_autonomous_guide` now:",
8382
+ topCandidate ? `Pick **${topCandidate.ticket.id}** (highest priority) or an open issue by calling \`claudestory_autonomous_guide\` now:` : hasHighIssues ? "Pick an issue to fix by calling `claudestory_autonomous_guide` now:" : "Pick a ticket by calling `claudestory_autonomous_guide` now:",
7945
8383
  "```json",
7946
8384
  topCandidate ? `{ "sessionId": "${updated.sessionId}", "action": "report", "report": { "completedAction": "ticket_picked", "ticketId": "${topCandidate.ticket.id}" } }` : `{ "sessionId": "${updated.sessionId}", "action": "report", "report": { "completedAction": "ticket_picked", "ticketId": "T-XXX" } }`,
7947
- "```"
8385
+ "```",
8386
+ ...hasHighIssues ? [
8387
+ "",
8388
+ "Or to fix an issue:",
8389
+ "```json",
8390
+ `{ "sessionId": "${updated.sessionId}", "action": "report", "report": { "completedAction": "issue_picked", "issueId": "${highIssues[0].id}" } }`,
8391
+ "```"
8392
+ ] : []
7948
8393
  ].join("\n");
7949
8394
  return guideResult(updated, "PICK_TICKET", {
7950
8395
  instruction,
@@ -8131,26 +8576,7 @@ async function handleResume(root, args) {
8131
8576
  ));
8132
8577
  }
8133
8578
  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 };
8579
+ const mapping = RECOVERY_MAPPING[resumeState] ?? { state: "PICK_TICKET", resetPlan: false, resetCode: false };
8154
8580
  const recoveryReviews = {
8155
8581
  plan: mapping.resetPlan ? [] : info.state.reviews.plan,
8156
8582
  code: mapping.resetCode ? [] : info.state.reviews.code
@@ -8498,7 +8924,7 @@ function guideError(err) {
8498
8924
  }
8499
8925
  function readFileSafe2(path2) {
8500
8926
  try {
8501
- return readFileSync4(path2, "utf-8");
8927
+ return readFileSync6(path2, "utf-8");
8502
8928
  } catch {
8503
8929
  return "";
8504
8930
  }
@@ -8508,8 +8934,8 @@ function readFileSafe2(path2) {
8508
8934
  init_esm_shims();
8509
8935
  init_session();
8510
8936
  init_session_types();
8511
- import { readFileSync as readFileSync5, existsSync as existsSync10 } from "fs";
8512
- import { join as join12 } from "path";
8937
+ import { readFileSync as readFileSync7, existsSync as existsSync10 } from "fs";
8938
+ import { join as join14 } from "path";
8513
8939
 
8514
8940
  // src/core/session-report-formatter.ts
8515
8941
  init_esm_shims();
@@ -8723,7 +9149,7 @@ async function handleSessionReport(sessionId, root, format = "md") {
8723
9149
  isError: true
8724
9150
  };
8725
9151
  }
8726
- const statePath2 = join12(dir, "state.json");
9152
+ const statePath2 = join14(dir, "state.json");
8727
9153
  if (!existsSync10(statePath2)) {
8728
9154
  return {
8729
9155
  output: `Error: Session ${sessionId} corrupt \u2014 state.json missing.`,
@@ -8733,7 +9159,7 @@ async function handleSessionReport(sessionId, root, format = "md") {
8733
9159
  };
8734
9160
  }
8735
9161
  try {
8736
- const rawJson = JSON.parse(readFileSync5(statePath2, "utf-8"));
9162
+ const rawJson = JSON.parse(readFileSync7(statePath2, "utf-8"));
8737
9163
  if (rawJson && typeof rawJson === "object" && "schemaVersion" in rawJson && rawJson.schemaVersion !== CURRENT_SESSION_SCHEMA_VERSION) {
8738
9164
  return {
8739
9165
  output: `Error: Session ${sessionId} \u2014 unsupported session schema version ${rawJson.schemaVersion}.`,
@@ -8762,7 +9188,7 @@ async function handleSessionReport(sessionId, root, format = "md") {
8762
9188
  const events = readEvents(dir);
8763
9189
  let planContent = null;
8764
9190
  try {
8765
- planContent = readFileSync5(join12(dir, "plan.md"), "utf-8");
9191
+ planContent = readFileSync7(join14(dir, "plan.md"), "utf-8");
8766
9192
  } catch {
8767
9193
  }
8768
9194
  let gitLog = null;
@@ -8787,7 +9213,7 @@ init_issue();
8787
9213
  init_roadmap();
8788
9214
  init_output_formatter();
8789
9215
  init_helpers();
8790
- import { join as join13, resolve as resolve6 } from "path";
9216
+ import { join as join15, resolve as resolve6 } from "path";
8791
9217
  var PHASE_ID_REGEX = /^[a-z0-9]+(-[a-z0-9]+)*$/;
8792
9218
  var PHASE_ID_MAX_LENGTH = 40;
8793
9219
  function validatePhaseId(id) {
@@ -8896,7 +9322,7 @@ function formatMcpError(code, message) {
8896
9322
  async function runMcpReadTool(pinnedRoot, handler) {
8897
9323
  try {
8898
9324
  const { state, warnings } = await loadProject(pinnedRoot);
8899
- const handoversDir = join14(pinnedRoot, ".story", "handovers");
9325
+ const handoversDir = join16(pinnedRoot, ".story", "handovers");
8900
9326
  const ctx = { state, warnings, root: pinnedRoot, handoversDir, format: "md" };
8901
9327
  const result = await handler(ctx);
8902
9328
  if (result.errorCode && INFRASTRUCTURE_ERROR_CODES.includes(result.errorCode)) {
@@ -9453,10 +9879,10 @@ init_esm_shims();
9453
9879
  init_project_loader();
9454
9880
  init_errors();
9455
9881
  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";
9882
+ import { join as join17, resolve as resolve7 } from "path";
9457
9883
  async function initProject(root, options) {
9458
9884
  const absRoot = resolve7(root);
9459
- const wrapDir = join15(absRoot, ".story");
9885
+ const wrapDir = join17(absRoot, ".story");
9460
9886
  let exists = false;
9461
9887
  try {
9462
9888
  const s = await stat2(wrapDir);
@@ -9476,11 +9902,11 @@ async function initProject(root, options) {
9476
9902
  ".story/ already exists. Use --force to overwrite config and roadmap."
9477
9903
  );
9478
9904
  }
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 });
9905
+ await mkdir4(join17(wrapDir, "tickets"), { recursive: true });
9906
+ await mkdir4(join17(wrapDir, "issues"), { recursive: true });
9907
+ await mkdir4(join17(wrapDir, "handovers"), { recursive: true });
9908
+ await mkdir4(join17(wrapDir, "notes"), { recursive: true });
9909
+ await mkdir4(join17(wrapDir, "lessons"), { recursive: true });
9484
9910
  const created = [
9485
9911
  ".story/config.json",
9486
9912
  ".story/roadmap.json",
@@ -9520,7 +9946,7 @@ async function initProject(root, options) {
9520
9946
  };
9521
9947
  await writeConfig(config, absRoot);
9522
9948
  await writeRoadmap(roadmap, absRoot);
9523
- const gitignorePath = join15(wrapDir, ".gitignore");
9949
+ const gitignorePath = join17(wrapDir, ".gitignore");
9524
9950
  await ensureGitignoreEntries(gitignorePath, STORY_GITIGNORE_ENTRIES);
9525
9951
  const warnings = [];
9526
9952
  if (options.force && exists) {
@@ -9559,7 +9985,7 @@ async function ensureGitignoreEntries(gitignorePath, entries) {
9559
9985
  // src/mcp/index.ts
9560
9986
  var ENV_VAR2 = "CLAUDESTORY_PROJECT_ROOT";
9561
9987
  var CONFIG_PATH2 = ".story/config.json";
9562
- var version = "0.1.35";
9988
+ var version = "0.1.37";
9563
9989
  function tryDiscoverRoot() {
9564
9990
  const envRoot = process.env[ENV_VAR2];
9565
9991
  if (envRoot) {
@@ -9571,7 +9997,7 @@ function tryDiscoverRoot() {
9571
9997
  const resolved = resolve8(envRoot);
9572
9998
  try {
9573
9999
  const canonical = realpathSync2(resolved);
9574
- if (existsSync11(join16(canonical, CONFIG_PATH2))) {
10000
+ if (existsSync11(join18(canonical, CONFIG_PATH2))) {
9575
10001
  return canonical;
9576
10002
  }
9577
10003
  process.stderr.write(`Warning: No .story/config.json at ${canonical}