@anthropologies/claudestory 0.1.62 → 0.1.63

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -221,8 +221,12 @@ var init_session_types = __esm({
221
221
  title: z.string().optional(),
222
222
  commitHash: z.string().optional(),
223
223
  risk: z.string().optional(),
224
- realizedRisk: z.string().optional()
224
+ realizedRisk: z.string().optional(),
225
+ startedAt: z.string().optional(),
226
+ completedAt: z.string().optional()
225
227
  })).default([]),
228
+ // T-187: Per-ticket timing -- set when ticket is picked, cleared on commit
229
+ ticketStartedAt: z.string().nullable().default(null),
226
230
  // FINALIZE checkpoint
227
231
  finalizeCheckpoint: z.enum(["staged", "staged_override", "precommit_passed", "committed"]).nullable().default(null),
228
232
  // Git state
@@ -284,6 +288,10 @@ var init_session_types = __esm({
284
288
  lastGuideCall: z.string().optional(),
285
289
  startedAt: z.string(),
286
290
  guideCallCount: z.number().default(0),
291
+ // ISS-098: Codex availability cache -- skip codex after failure
292
+ // ISS-110: Changed from boolean to ISO timestamp with 10-minute TTL
293
+ codexUnavailable: z.boolean().optional(),
294
+ codexUnavailableSince: z.string().optional(),
287
295
  // Supersession tracking
288
296
  supersededBy: z.string().optional(),
289
297
  supersededSession: z.string().optional(),
@@ -1831,6 +1839,9 @@ function validateFindings(raw, lensName) {
1831
1839
  continue;
1832
1840
  }
1833
1841
  const normalized = normalizeFields(item);
1842
+ if (typeof normalized.lens !== "string" && typeof lensName === "string") {
1843
+ normalized.lens = lensName;
1844
+ }
1834
1845
  const reason = checkFinding(normalized, lensName);
1835
1846
  if (reason) {
1836
1847
  invalid.push({ raw: item, reason });
@@ -2609,6 +2620,63 @@ var init_orchestrator = __esm({
2609
2620
  }
2610
2621
  });
2611
2622
 
2623
+ // src/autonomous/review-lenses/diff-scope.ts
2624
+ function parseDiffScope(diff) {
2625
+ const changedFiles = /* @__PURE__ */ new Set();
2626
+ const addedLines = /* @__PURE__ */ new Map();
2627
+ if (!diff) return { changedFiles, addedLines };
2628
+ const lines = diff.split("\n");
2629
+ let currentFile = null;
2630
+ let currentLineNum = 0;
2631
+ for (const line of lines) {
2632
+ if (line.startsWith("+++ ")) {
2633
+ if (line.startsWith("+++ /dev/null")) {
2634
+ currentFile = null;
2635
+ continue;
2636
+ }
2637
+ const rawPath = line.startsWith("+++ b/") ? line.slice(6) : line.slice(4);
2638
+ currentFile = normalizePath(rawPath);
2639
+ changedFiles.add(currentFile);
2640
+ if (!addedLines.has(currentFile)) {
2641
+ addedLines.set(currentFile, /* @__PURE__ */ new Set());
2642
+ }
2643
+ currentLineNum = 0;
2644
+ continue;
2645
+ }
2646
+ const hunkMatch = line.match(/^@@ -\d+(?:,\d+)? \+(\d+)/);
2647
+ if (hunkMatch) {
2648
+ currentLineNum = parseInt(hunkMatch[1], 10) - 1;
2649
+ continue;
2650
+ }
2651
+ if (!currentFile) continue;
2652
+ if (line.startsWith("-")) continue;
2653
+ currentLineNum++;
2654
+ if (line.startsWith("+")) {
2655
+ addedLines.get(currentFile).add(currentLineNum);
2656
+ }
2657
+ }
2658
+ return { changedFiles, addedLines };
2659
+ }
2660
+ function normalizePath(p) {
2661
+ return p.startsWith("./") ? p.slice(2) : p;
2662
+ }
2663
+ function classifyOrigin(finding, scope, stage) {
2664
+ if (stage === "PLAN_REVIEW") return "introduced";
2665
+ if (!finding.file) return "introduced";
2666
+ const file = normalizePath(finding.file);
2667
+ if (!scope.changedFiles.has(file)) return "pre-existing";
2668
+ if (finding.line == null) return "introduced";
2669
+ const fileLines = scope.addedLines.get(file);
2670
+ if (!fileLines || !fileLines.has(finding.line)) return "pre-existing";
2671
+ return "introduced";
2672
+ }
2673
+ var init_diff_scope = __esm({
2674
+ "src/autonomous/review-lenses/diff-scope.ts"() {
2675
+ "use strict";
2676
+ init_esm_shims();
2677
+ }
2678
+ });
2679
+
2612
2680
  // src/autonomous/review-lenses/mcp-handlers.ts
2613
2681
  import { readFileSync as readFileSync3 } from "fs";
2614
2682
  import { join as join6 } from "path";
@@ -2707,6 +2775,7 @@ function handleSynthesize(input) {
2707
2775
  }
2708
2776
  }
2709
2777
  const stage = input.stage ?? "CODE_REVIEW";
2778
+ const diffScope = input.diff && input.changedFiles && stage === "CODE_REVIEW" ? parseDiffScope(input.diff) : null;
2710
2779
  const lensesCompleted = [];
2711
2780
  const lensesFailed = [];
2712
2781
  const lensesInsufficientContext = [];
@@ -2739,7 +2808,8 @@ function handleSynthesize(input) {
2739
2808
  const enriched = {
2740
2809
  ...f,
2741
2810
  issueKey: generateIssueKey(f),
2742
- blocking: computeBlocking(f, stage, policy)
2811
+ blocking: computeBlocking(f, stage, policy),
2812
+ origin: diffScope ? classifyOrigin(f, diffScope, stage) : void 0
2743
2813
  };
2744
2814
  allFindings.push(enriched);
2745
2815
  }
@@ -2754,6 +2824,9 @@ function handleSynthesize(input) {
2754
2824
  lensesFailed.push(lens);
2755
2825
  }
2756
2826
  }
2827
+ const preExistingFindings = allFindings.filter(
2828
+ (f) => f.origin === "pre-existing" && f.severity !== "suggestion"
2829
+ );
2757
2830
  const lensMetadata = buildLensMetadata(lensesCompleted, lensesFailed, lensesInsufficientContext);
2758
2831
  const mergerPrompt = buildMergerPrompt(allFindings, lensMetadata, stage);
2759
2832
  return {
@@ -2763,7 +2836,9 @@ function handleSynthesize(input) {
2763
2836
  lensesFailed,
2764
2837
  lensesInsufficientContext,
2765
2838
  droppedFindings: droppedTotal,
2766
- droppedDetails: dropReasons.slice(0, 5)
2839
+ droppedDetails: dropReasons.slice(0, 5),
2840
+ preExistingFindings,
2841
+ preExistingCount: preExistingFindings.length
2767
2842
  };
2768
2843
  }
2769
2844
  function handleJudge(input) {
@@ -2788,8 +2863,8 @@ function handleJudge(input) {
2788
2863
  [...input.lensesSkipped]
2789
2864
  );
2790
2865
  if (input.convergenceHistory && input.convergenceHistory.length > 0) {
2791
- const sanitize = (s) => s.replace(/[|\n\r#>`*_~\[\]]/g, " ").slice(0, 50);
2792
- const historyTable = input.convergenceHistory.map((h) => `| R${h.round} | ${sanitize(h.verdict)} | ${h.blocking} | ${h.important} | ${sanitize(h.newCode)} |`).join("\n");
2866
+ const sanitize2 = (s) => s.replace(/[|\n\r#>`*_~\[\]]/g, " ").slice(0, 50);
2867
+ const historyTable = input.convergenceHistory.map((h) => `| R${h.round} | ${sanitize2(h.verdict)} | ${h.blocking} | ${h.important} | ${sanitize2(h.newCode)} |`).join("\n");
2793
2868
  judgePrompt += `
2794
2869
 
2795
2870
  ## Convergence History
@@ -2816,6 +2891,7 @@ var init_mcp_handlers = __esm({
2816
2891
  init_schema_validator();
2817
2892
  init_issue_key();
2818
2893
  init_blocking_policy();
2894
+ init_diff_scope();
2819
2895
  init_merger();
2820
2896
  init_judge();
2821
2897
  init_types();
@@ -3094,12 +3170,13 @@ var init_project_state = __esm({
3094
3170
  totalTicketCount;
3095
3171
  openTicketCount;
3096
3172
  completeTicketCount;
3097
- openIssueCount;
3173
+ activeIssueCount;
3098
3174
  issuesBySeverity;
3099
3175
  activeNoteCount;
3100
3176
  archivedNoteCount;
3101
3177
  activeLessonCount;
3102
3178
  deprecatedLessonCount;
3179
+ lessonTags;
3103
3180
  constructor(input) {
3104
3181
  this.tickets = input.tickets;
3105
3182
  this.issues = input.issues;
@@ -3180,19 +3257,19 @@ var init_project_state = __esm({
3180
3257
  lByID.set(l.id, l);
3181
3258
  }
3182
3259
  this.lessonsByID = lByID;
3183
- this.totalTicketCount = input.tickets.length;
3184
- this.openTicketCount = input.tickets.filter(
3260
+ this.totalTicketCount = this.leafTickets.length;
3261
+ this.openTicketCount = this.leafTickets.filter(
3185
3262
  (t) => t.status !== "complete"
3186
3263
  ).length;
3187
- this.completeTicketCount = input.tickets.filter(
3264
+ this.completeTicketCount = this.leafTickets.filter(
3188
3265
  (t) => t.status === "complete"
3189
3266
  ).length;
3190
- this.openIssueCount = input.issues.filter(
3191
- (i) => i.status === "open"
3267
+ this.activeIssueCount = input.issues.filter(
3268
+ (i) => i.status !== "resolved"
3192
3269
  ).length;
3193
3270
  const bySev = /* @__PURE__ */ new Map();
3194
3271
  for (const i of input.issues) {
3195
- if (i.status === "open") {
3272
+ if (i.status !== "resolved") {
3196
3273
  bySev.set(i.severity, (bySev.get(i.severity) ?? 0) + 1);
3197
3274
  }
3198
3275
  }
@@ -3209,6 +3286,7 @@ var init_project_state = __esm({
3209
3286
  this.deprecatedLessonCount = this.lessons.filter(
3210
3287
  (l) => l.status === "deprecated" || l.status === "superseded"
3211
3288
  ).length;
3289
+ this.lessonTags = [...new Set(this.lessons.flatMap((l) => l.tags ?? []))].sort();
3212
3290
  }
3213
3291
  // --- Query Methods ---
3214
3292
  isUmbrella(ticket) {
@@ -4525,7 +4603,7 @@ function formatStatus(state, format, activeSessions = []) {
4525
4603
  completeTickets: state.completeLeafTicketCount,
4526
4604
  openTickets: state.leafTicketCount - state.completeLeafTicketCount,
4527
4605
  blockedTickets: state.blockedCount,
4528
- openIssues: state.openIssueCount,
4606
+ openIssues: state.activeIssueCount,
4529
4607
  activeNotes: state.activeNoteCount,
4530
4608
  archivedNotes: state.archivedNoteCount,
4531
4609
  activeLessons: state.activeLessonCount,
@@ -4547,7 +4625,7 @@ function formatStatus(state, format, activeSessions = []) {
4547
4625
  `# ${escapeMarkdownInline(state.config.project)}`,
4548
4626
  "",
4549
4627
  `Tickets: ${state.completeLeafTicketCount}/${state.leafTicketCount} complete, ${state.blockedCount} blocked`,
4550
- `Issues: ${state.openIssueCount} open`,
4628
+ `Issues: ${state.activeIssueCount} open`,
4551
4629
  `Notes: ${state.activeNoteCount} active, ${state.archivedNoteCount} archived`,
4552
4630
  `Lessons: ${state.activeLessonCount} active, ${state.deprecatedLessonCount} deprecated`,
4553
4631
  `Handovers: ${state.handoverFilenames.length}`,
@@ -4996,7 +5074,7 @@ function formatRecap(recap, state, format) {
4996
5074
  lines.push("No snapshot found. Run `claudestory snapshot` to enable session diffs.");
4997
5075
  lines.push("");
4998
5076
  lines.push(`Tickets: ${state.completeLeafTicketCount}/${state.leafTicketCount} complete, ${state.blockedCount} blocked`);
4999
- lines.push(`Issues: ${state.openIssueCount} open`);
5077
+ lines.push(`Issues: ${state.activeIssueCount} open`);
5000
5078
  } else {
5001
5079
  lines.push(`# ${escapeMarkdownInline(state.config.project)} \u2014 Recap`);
5002
5080
  lines.push("");
@@ -5004,6 +5082,13 @@ function formatRecap(recap, state, format) {
5004
5082
  if (recap.partial) {
5005
5083
  lines.push("**Note:** Snapshot was taken from a project with integrity warnings. Diff may be incomplete.");
5006
5084
  }
5085
+ if (recap.staleness) {
5086
+ if (recap.staleness.status === "diverged") {
5087
+ lines.push("**Warning:** Snapshot commit is not an ancestor of current HEAD (history diverged; possible rebase, force-push, or branch switch).");
5088
+ } else if (recap.staleness.status === "behind" && recap.staleness.commitsBehind) {
5089
+ lines.push(`**Warning:** Snapshot is ${recap.staleness.commitsBehind} commit(s) behind HEAD -- context may be stale.`);
5090
+ }
5091
+ }
5007
5092
  const changes = recap.changes;
5008
5093
  const hasChanges = hasAnyChanges(changes);
5009
5094
  if (!hasChanges) {
@@ -5265,7 +5350,7 @@ function formatFullExport(state, format) {
5265
5350
  lines.push(`# ${escapeMarkdownInline(state.config.project)} \u2014 Full Export`);
5266
5351
  lines.push("");
5267
5352
  lines.push(`Tickets: ${state.completeLeafTicketCount}/${state.leafTicketCount} complete`);
5268
- lines.push(`Issues: ${state.openIssueCount} open`);
5353
+ lines.push(`Issues: ${state.activeIssueCount} open`);
5269
5354
  lines.push(`Notes: ${state.activeNoteCount} active, ${state.archivedNoteCount} archived`);
5270
5355
  lines.push(`Lessons: ${state.activeLessonCount} active, ${state.deprecatedLessonCount} deprecated`);
5271
5356
  lines.push("");
@@ -6633,6 +6718,204 @@ var init_issue2 = __esm({
6633
6718
  }
6634
6719
  });
6635
6720
 
6721
+ // src/autonomous/git-inspector.ts
6722
+ import { execFile } from "child_process";
6723
+ async function git(cwd, args, parse) {
6724
+ return new Promise((resolve10) => {
6725
+ execFile("git", args, { cwd, timeout: GIT_TIMEOUT, maxBuffer: 10 * 1024 * 1024 }, (err, stdout, stderr) => {
6726
+ if (err) {
6727
+ const message = stderr?.trim() || err.message || "unknown git error";
6728
+ resolve10({ ok: false, reason: "git_error", message });
6729
+ return;
6730
+ }
6731
+ try {
6732
+ resolve10({ ok: true, data: parse(stdout) });
6733
+ } catch (parseErr) {
6734
+ resolve10({ ok: false, reason: "parse_error", message: parseErr.message });
6735
+ }
6736
+ });
6737
+ });
6738
+ }
6739
+ async function gitStatus(cwd) {
6740
+ return git(
6741
+ cwd,
6742
+ ["status", "--porcelain"],
6743
+ (out) => out.split("\n").filter((l) => l.length > 0)
6744
+ );
6745
+ }
6746
+ async function gitHead(cwd) {
6747
+ const hashResult = await git(cwd, ["rev-parse", "HEAD"], (out) => out.trim());
6748
+ if (!hashResult.ok) return hashResult;
6749
+ const branchResult = await gitBranch(cwd);
6750
+ return {
6751
+ ok: true,
6752
+ data: {
6753
+ hash: hashResult.data,
6754
+ branch: branchResult.ok ? branchResult.data : null
6755
+ }
6756
+ };
6757
+ }
6758
+ async function gitBranch(cwd) {
6759
+ return git(cwd, ["symbolic-ref", "--short", "HEAD"], (out) => out.trim());
6760
+ }
6761
+ async function gitMergeBase(cwd, base) {
6762
+ return git(cwd, ["merge-base", "HEAD", base], (out) => out.trim());
6763
+ }
6764
+ async function gitDiffStat(cwd, base) {
6765
+ return git(cwd, ["diff", "--numstat", base], parseDiffNumstat);
6766
+ }
6767
+ async function gitDiffNames(cwd, base) {
6768
+ return git(
6769
+ cwd,
6770
+ ["diff", "--name-only", base],
6771
+ (out) => out.split("\n").filter((l) => l.length > 0)
6772
+ );
6773
+ }
6774
+ async function gitBlobHash(cwd, file) {
6775
+ return git(cwd, ["hash-object", file], (out) => out.trim());
6776
+ }
6777
+ async function gitDiffCachedNames(cwd) {
6778
+ return git(
6779
+ cwd,
6780
+ ["diff", "--cached", "--name-only"],
6781
+ (out) => out.split("\n").filter((l) => l.length > 0)
6782
+ );
6783
+ }
6784
+ async function gitStash(cwd, message) {
6785
+ const pushResult = await git(cwd, ["stash", "push", "-m", message], () => void 0);
6786
+ if (!pushResult.ok) return { ok: false, reason: pushResult.reason, message: pushResult.message };
6787
+ const hashResult = await git(cwd, ["rev-parse", "stash@{0}"], (out) => out.trim());
6788
+ if (!hashResult.ok) {
6789
+ const listResult = await git(
6790
+ cwd,
6791
+ ["stash", "list", "--format=%gd %s"],
6792
+ (out) => out.split("\n").filter((l) => l.includes(message))
6793
+ );
6794
+ if (listResult.ok && listResult.data.length > 0) {
6795
+ const ref = listResult.data[0].split(" ")[0];
6796
+ const refHash = await git(cwd, ["rev-parse", ref], (out) => out.trim());
6797
+ if (refHash.ok) return { ok: true, data: refHash.data };
6798
+ }
6799
+ return { ok: false, reason: "stash_hash_failed", message: "Stash created but could not be identified. Run `git stash list` to find and pop it manually." };
6800
+ }
6801
+ return { ok: true, data: hashResult.data };
6802
+ }
6803
+ async function gitStashPop(cwd, commitHash) {
6804
+ if (!commitHash) {
6805
+ return git(cwd, ["stash", "pop"], () => void 0);
6806
+ }
6807
+ const listResult = await git(
6808
+ cwd,
6809
+ ["stash", "list", "--format=%gd %H"],
6810
+ (out) => out.split("\n").filter((l) => l.length > 0).map((l) => {
6811
+ const [ref, hash] = l.split(" ", 2);
6812
+ return { ref, hash };
6813
+ })
6814
+ );
6815
+ if (!listResult.ok) {
6816
+ return { ok: false, reason: "stash_list_failed", message: `Cannot list stash entries to find ${commitHash}. Run \`git stash list\` and pop manually.` };
6817
+ }
6818
+ const match = listResult.data.find((e) => e.hash === commitHash);
6819
+ if (!match) {
6820
+ return { ok: false, reason: "stash_not_found", message: `No stash entry with commit hash ${commitHash}` };
6821
+ }
6822
+ return git(cwd, ["stash", "pop", match.ref], () => void 0);
6823
+ }
6824
+ async function gitDiffTreeNames(cwd, commitHash) {
6825
+ return git(
6826
+ cwd,
6827
+ ["diff-tree", "--name-only", "--no-commit-id", "-r", commitHash],
6828
+ (out) => out.split("\n").filter((l) => l.trim().length > 0)
6829
+ );
6830
+ }
6831
+ async function gitIsAncestor(cwd, ancestor, descendant) {
6832
+ if (!SAFE_REF.test(ancestor) || !SAFE_REF.test(descendant)) {
6833
+ return { ok: false, reason: "git_error", message: "invalid ref format" };
6834
+ }
6835
+ return new Promise((resolve10) => {
6836
+ execFile(
6837
+ "git",
6838
+ ["merge-base", "--is-ancestor", ancestor, descendant],
6839
+ { cwd, timeout: GIT_TIMEOUT },
6840
+ (err) => {
6841
+ if (!err) {
6842
+ resolve10({ ok: true, data: true });
6843
+ return;
6844
+ }
6845
+ const code = err.code;
6846
+ if (code === 1) {
6847
+ resolve10({ ok: true, data: false });
6848
+ return;
6849
+ }
6850
+ resolve10({ ok: false, reason: "git_error", message: err.message });
6851
+ }
6852
+ );
6853
+ });
6854
+ }
6855
+ async function gitLogRange(cwd, from, to, limit = 20) {
6856
+ if (from && !SAFE_REF.test(from)) {
6857
+ return { ok: false, reason: "invalid_ref", message: `Invalid git ref: ${from}` };
6858
+ }
6859
+ if (to && !SAFE_REF.test(to)) {
6860
+ return { ok: false, reason: "invalid_ref", message: `Invalid git ref: ${to}` };
6861
+ }
6862
+ if (!from || !to) {
6863
+ return { ok: true, data: [] };
6864
+ }
6865
+ return git(
6866
+ cwd,
6867
+ ["log", "--oneline", `-${limit}`, `${from}..${to}`],
6868
+ (out) => out.split("\n").filter((l) => l.trim().length > 0)
6869
+ );
6870
+ }
6871
+ async function gitHeadHash(cwd) {
6872
+ return new Promise((resolve10) => {
6873
+ execFile("git", ["rev-parse", "HEAD"], { cwd, timeout: 3e3 }, (err, stdout) => {
6874
+ if (err) {
6875
+ const message = err.stderr?.trim() || err.message || "unknown git error";
6876
+ resolve10({ ok: false, reason: "git_error", message });
6877
+ return;
6878
+ }
6879
+ resolve10({ ok: true, data: stdout.trim() });
6880
+ });
6881
+ });
6882
+ }
6883
+ async function gitCommitDistance(cwd, fromSha, toSha) {
6884
+ if (!SAFE_REF.test(fromSha) || !SAFE_REF.test(toSha)) {
6885
+ return { ok: false, reason: "git_error", message: "invalid ref format" };
6886
+ }
6887
+ return git(cwd, ["rev-list", "--count", `${fromSha}..${toSha}`], (out) => {
6888
+ const n = parseInt(out.trim(), 10);
6889
+ if (Number.isNaN(n)) throw new Error(`unexpected rev-list output: ${out}`);
6890
+ return n;
6891
+ });
6892
+ }
6893
+ function parseDiffNumstat(out) {
6894
+ const lines = out.split("\n").filter((l) => l.length > 0);
6895
+ let insertions = 0;
6896
+ let deletions = 0;
6897
+ let filesChanged = 0;
6898
+ for (const line of lines) {
6899
+ const parts = line.split(" ");
6900
+ if (parts.length < 3) continue;
6901
+ const added = parseInt(parts[0], 10);
6902
+ const removed = parseInt(parts[1], 10);
6903
+ if (!Number.isNaN(added)) insertions += added;
6904
+ if (!Number.isNaN(removed)) deletions += removed;
6905
+ filesChanged++;
6906
+ }
6907
+ return { filesChanged, insertions, deletions, totalLines: insertions + deletions };
6908
+ }
6909
+ var GIT_TIMEOUT, SAFE_REF;
6910
+ var init_git_inspector = __esm({
6911
+ "src/autonomous/git-inspector.ts"() {
6912
+ "use strict";
6913
+ init_esm_shims();
6914
+ GIT_TIMEOUT = 1e4;
6915
+ SAFE_REF = /^[0-9a-f]{4,40}$/i;
6916
+ }
6917
+ });
6918
+
6636
6919
  // src/core/snapshot.ts
6637
6920
  var snapshot_exports = {};
6638
6921
  __export(snapshot_exports, {
@@ -6672,6 +6955,10 @@ async function saveSnapshot(root, loadResult) {
6672
6955
  }))
6673
6956
  } : {}
6674
6957
  };
6958
+ const headResult = await gitHeadHash(absRoot);
6959
+ if (headResult.ok) {
6960
+ snapshot.gitHead = headResult.data;
6961
+ }
6675
6962
  const json = JSON.stringify(snapshot, null, 2) + "\n";
6676
6963
  const targetPath = join11(snapshotsDir, filename);
6677
6964
  const wrapDir = join11(absRoot, ".story");
@@ -6867,7 +7154,7 @@ function diffStates(snapshotState, currentState) {
6867
7154
  handovers: { added: handoversAdded, removed: handoversRemoved }
6868
7155
  };
6869
7156
  }
6870
- function buildRecap(currentState, snapshotInfo) {
7157
+ async function buildRecap(currentState, snapshotInfo, root) {
6871
7158
  const next = nextTicket(currentState);
6872
7159
  const nextTicketAction = next.kind === "found" ? { id: next.ticket.id, title: next.ticket.title, phase: next.ticket.phase } : null;
6873
7160
  const highSeverityIssues = currentState.issues.filter(
@@ -6897,6 +7184,30 @@ function buildRecap(currentState, snapshotInfo) {
6897
7184
  });
6898
7185
  const changes = diffStates(snapshotState, currentState);
6899
7186
  const recentlyClearedBlockers = changes.blockers.cleared;
7187
+ let staleness;
7188
+ if (snapshot.gitHead) {
7189
+ const currentHeadResult = await gitHeadHash(root);
7190
+ if (currentHeadResult.ok) {
7191
+ const snapshotSha = snapshot.gitHead;
7192
+ const currentSha = currentHeadResult.data;
7193
+ if (snapshotSha !== currentSha) {
7194
+ const ancestorResult = await gitIsAncestor(root, snapshotSha, currentSha);
7195
+ if (ancestorResult.ok && ancestorResult.data) {
7196
+ const distResult = await gitCommitDistance(root, snapshotSha, currentSha);
7197
+ if (distResult.ok) {
7198
+ staleness = {
7199
+ status: "behind",
7200
+ snapshotSha,
7201
+ currentSha,
7202
+ commitsBehind: distResult.data
7203
+ };
7204
+ }
7205
+ } else if (ancestorResult.ok && !ancestorResult.data) {
7206
+ staleness = { status: "diverged", snapshotSha, currentSha };
7207
+ }
7208
+ }
7209
+ }
7210
+ }
6900
7211
  return {
6901
7212
  snapshot: { filename, createdAt: snapshot.createdAt },
6902
7213
  changes,
@@ -6905,7 +7216,8 @@ function buildRecap(currentState, snapshotInfo) {
6905
7216
  highSeverityIssues,
6906
7217
  recentlyClearedBlockers
6907
7218
  },
6908
- partial: (snapshot.warnings ?? []).length > 0
7219
+ partial: (snapshot.warnings ?? []).length > 0,
7220
+ ...staleness ? { staleness } : {}
6909
7221
  };
6910
7222
  }
6911
7223
  function formatSnapshotFilename(date) {
@@ -6952,6 +7264,7 @@ var init_snapshot = __esm({
6952
7264
  init_project_state();
6953
7265
  init_queries();
6954
7266
  init_project_loader();
7267
+ init_git_inspector();
6955
7268
  LoadWarningSchema = z9.object({
6956
7269
  type: z9.string(),
6957
7270
  file: z9.string(),
@@ -6968,7 +7281,8 @@ var init_snapshot = __esm({
6968
7281
  notes: z9.array(NoteSchema).optional().default([]),
6969
7282
  lessons: z9.array(LessonSchema).optional().default([]),
6970
7283
  handoverFilenames: z9.array(z9.string()).optional().default([]),
6971
- warnings: z9.array(LoadWarningSchema).optional()
7284
+ warnings: z9.array(LoadWarningSchema).optional(),
7285
+ gitHead: z9.string().optional()
6972
7286
  });
6973
7287
  MAX_SNAPSHOTS = 20;
6974
7288
  }
@@ -6977,7 +7291,7 @@ var init_snapshot = __esm({
6977
7291
  // src/cli/commands/recap.ts
6978
7292
  async function handleRecap(ctx) {
6979
7293
  const snapshotInfo = await loadLatestSnapshot(ctx.root);
6980
- const recap = buildRecap(ctx.state, snapshotInfo);
7294
+ const recap = await buildRecap(ctx.state, snapshotInfo, ctx.root);
6981
7295
  return { output: formatRecap(recap, ctx.state, ctx.format) };
6982
7296
  }
6983
7297
  var init_recap = __esm({
@@ -8407,15 +8721,17 @@ var init_state_machine = __esm({
8407
8721
  // advance → IMPLEMENT, retry stays, exhaustion → PLAN, no-op → COMPLETE (ISS-069)
8408
8722
  TEST: ["CODE_REVIEW", "IMPLEMENT", "TEST"],
8409
8723
  // pass → CODE_REVIEW, fail → IMPLEMENT, retry
8410
- CODE_REVIEW: ["VERIFY", "FINALIZE", "IMPLEMENT", "PLAN", "CODE_REVIEW", "SESSION_END"],
8411
- // approve → VERIFY/FINALIZE, reject → IMPLEMENT/PLAN, stay for next round; SESSION_END for tiered exit
8412
- VERIFY: ["FINALIZE", "IMPLEMENT", "VERIFY"],
8724
+ CODE_REVIEW: ["VERIFY", "BUILD", "FINALIZE", "IMPLEMENT", "PLAN", "CODE_REVIEW", "SESSION_END", "ISSUE_FIX"],
8725
+ // approve → VERIFY/BUILD/FINALIZE, reject → IMPLEMENT/PLAN, stay for next round; SESSION_END for tiered exit; T-208: ISSUE_FIX for issue-fix reviews
8726
+ VERIFY: ["BUILD", "FINALIZE", "IMPLEMENT", "VERIFY"],
8727
+ // pass → BUILD/FINALIZE, fail → IMPLEMENT, retry
8728
+ BUILD: ["FINALIZE", "IMPLEMENT", "BUILD"],
8413
8729
  // pass → FINALIZE, fail → IMPLEMENT, retry
8414
8730
  FINALIZE: ["COMPLETE", "PICK_TICKET"],
8415
8731
  // ISS-084: issues now route through COMPLETE too; PICK_TICKET kept for in-flight session compat
8416
8732
  COMPLETE: ["PICK_TICKET", "HANDOVER", "ISSUE_SWEEP", "SESSION_END"],
8417
- ISSUE_FIX: ["FINALIZE", "PICK_TICKET", "ISSUE_FIX"],
8418
- // T-153: fix done → FINALIZE, cancel → PICK_TICKET, retry self
8733
+ ISSUE_FIX: ["FINALIZE", "PICK_TICKET", "ISSUE_FIX", "CODE_REVIEW"],
8734
+ // T-153: fix done → FINALIZE, cancel → PICK_TICKET, retry self; T-208: optional code review
8419
8735
  LESSON_CAPTURE: ["ISSUE_SWEEP", "HANDOVER", "LESSON_CAPTURE"],
8420
8736
  // advance → ISSUE_SWEEP, retry self, done → HANDOVER
8421
8737
  ISSUE_SWEEP: ["ISSUE_SWEEP", "HANDOVER", "PICK_TICKET"],
@@ -8494,16 +8810,24 @@ function requiredRounds(risk) {
8494
8810
  return 3;
8495
8811
  }
8496
8812
  }
8497
- function nextReviewer(previousRounds, backends) {
8498
- if (backends.length === 0) return "agent";
8499
- if (backends.length === 1) return backends[0];
8500
- if (previousRounds.length === 0) return backends[0];
8813
+ function isCodexUnavailable(codexUnavailableSince) {
8814
+ if (!codexUnavailableSince) return false;
8815
+ const since = new Date(codexUnavailableSince).getTime();
8816
+ if (Number.isNaN(since)) return false;
8817
+ return Date.now() - since < CODEX_CACHE_TTL_MS;
8818
+ }
8819
+ function nextReviewer(previousRounds, backends, codexUnavailable, codexUnavailableSince) {
8820
+ const unavailable = codexUnavailableSince ? isCodexUnavailable(codexUnavailableSince) : !!codexUnavailable;
8821
+ const effective = unavailable ? backends.filter((b) => b !== "codex") : backends;
8822
+ if (effective.length === 0) return "agent";
8823
+ if (effective.length === 1) return effective[0];
8824
+ if (previousRounds.length === 0) return effective[0];
8501
8825
  const lastReviewer = previousRounds[previousRounds.length - 1].reviewer;
8502
- const lastIndex = backends.indexOf(lastReviewer);
8503
- if (lastIndex === -1) return backends[0];
8504
- return backends[(lastIndex + 1) % backends.length];
8826
+ const lastIndex = effective.indexOf(lastReviewer);
8827
+ if (lastIndex === -1) return effective[0];
8828
+ return effective[(lastIndex + 1) % effective.length];
8505
8829
  }
8506
- var SENSITIVE_PATTERNS;
8830
+ var SENSITIVE_PATTERNS, CODEX_CACHE_TTL_MS;
8507
8831
  var init_review_depth = __esm({
8508
8832
  "src/autonomous/review-depth.ts"() {
8509
8833
  "use strict";
@@ -8516,173 +8840,22 @@ var init_review_depth = __esm({
8516
8840
  /\bmiddleware\b/i,
8517
8841
  /\.env/i
8518
8842
  ];
8843
+ CODEX_CACHE_TTL_MS = 10 * 60 * 1e3;
8519
8844
  }
8520
8845
  });
8521
8846
 
8522
- // src/autonomous/git-inspector.ts
8523
- import { execFile } from "child_process";
8524
- async function git(cwd, args, parse) {
8525
- return new Promise((resolve10) => {
8526
- execFile("git", args, { cwd, timeout: GIT_TIMEOUT, maxBuffer: 10 * 1024 * 1024 }, (err, stdout, stderr) => {
8527
- if (err) {
8528
- const message = stderr?.trim() || err.message || "unknown git error";
8529
- resolve10({ ok: false, reason: "git_error", message });
8530
- return;
8531
- }
8532
- try {
8533
- resolve10({ ok: true, data: parse(stdout) });
8534
- } catch (parseErr) {
8535
- resolve10({ ok: false, reason: "parse_error", message: parseErr.message });
8536
- }
8537
- });
8538
- });
8539
- }
8540
- async function gitStatus(cwd) {
8541
- return git(
8542
- cwd,
8543
- ["status", "--porcelain"],
8544
- (out) => out.split("\n").filter((l) => l.length > 0)
8545
- );
8546
- }
8547
- async function gitHead(cwd) {
8548
- const hashResult = await git(cwd, ["rev-parse", "HEAD"], (out) => out.trim());
8549
- if (!hashResult.ok) return hashResult;
8550
- const branchResult = await gitBranch(cwd);
8551
- return {
8552
- ok: true,
8553
- data: {
8554
- hash: hashResult.data,
8555
- branch: branchResult.ok ? branchResult.data : null
8556
- }
8557
- };
8558
- }
8559
- async function gitBranch(cwd) {
8560
- return git(cwd, ["symbolic-ref", "--short", "HEAD"], (out) => out.trim());
8561
- }
8562
- async function gitMergeBase(cwd, base) {
8563
- return git(cwd, ["merge-base", "HEAD", base], (out) => out.trim());
8564
- }
8565
- async function gitDiffStat(cwd, base) {
8566
- return git(cwd, ["diff", "--numstat", base], parseDiffNumstat);
8567
- }
8568
- async function gitDiffNames(cwd, base) {
8569
- return git(
8570
- cwd,
8571
- ["diff", "--name-only", base],
8572
- (out) => out.split("\n").filter((l) => l.length > 0)
8573
- );
8574
- }
8575
- async function gitBlobHash(cwd, file) {
8576
- return git(cwd, ["hash-object", file], (out) => out.trim());
8577
- }
8578
- async function gitDiffCachedNames(cwd) {
8579
- return git(
8580
- cwd,
8581
- ["diff", "--cached", "--name-only"],
8582
- (out) => out.split("\n").filter((l) => l.length > 0)
8583
- );
8584
- }
8585
- async function gitStash(cwd, message) {
8586
- const pushResult = await git(cwd, ["stash", "push", "-m", message], () => void 0);
8587
- if (!pushResult.ok) return { ok: false, reason: pushResult.reason, message: pushResult.message };
8588
- const hashResult = await git(cwd, ["rev-parse", "stash@{0}"], (out) => out.trim());
8589
- if (!hashResult.ok) {
8590
- const listResult = await git(
8591
- cwd,
8592
- ["stash", "list", "--format=%gd %s"],
8593
- (out) => out.split("\n").filter((l) => l.includes(message))
8594
- );
8595
- if (listResult.ok && listResult.data.length > 0) {
8596
- const ref = listResult.data[0].split(" ")[0];
8597
- const refHash = await git(cwd, ["rev-parse", ref], (out) => out.trim());
8598
- if (refHash.ok) return { ok: true, data: refHash.data };
8599
- }
8600
- return { ok: false, reason: "stash_hash_failed", message: "Stash created but could not be identified. Run `git stash list` to find and pop it manually." };
8601
- }
8602
- return { ok: true, data: hashResult.data };
8603
- }
8604
- async function gitStashPop(cwd, commitHash) {
8605
- if (!commitHash) {
8606
- return git(cwd, ["stash", "pop"], () => void 0);
8607
- }
8608
- const listResult = await git(
8609
- cwd,
8610
- ["stash", "list", "--format=%gd %H"],
8611
- (out) => out.split("\n").filter((l) => l.length > 0).map((l) => {
8612
- const [ref, hash] = l.split(" ", 2);
8613
- return { ref, hash };
8614
- })
8615
- );
8616
- if (!listResult.ok) {
8617
- return { ok: false, reason: "stash_list_failed", message: `Cannot list stash entries to find ${commitHash}. Run \`git stash list\` and pop manually.` };
8618
- }
8619
- const match = listResult.data.find((e) => e.hash === commitHash);
8620
- if (!match) {
8621
- return { ok: false, reason: "stash_not_found", message: `No stash entry with commit hash ${commitHash}` };
8622
- }
8623
- return git(cwd, ["stash", "pop", match.ref], () => void 0);
8624
- }
8625
- async function gitDiffTreeNames(cwd, commitHash) {
8626
- return git(
8627
- cwd,
8628
- ["diff-tree", "--name-only", "--no-commit-id", "-r", commitHash],
8629
- (out) => out.split("\n").filter((l) => l.trim().length > 0)
8630
- );
8631
- }
8632
- async function gitLogRange(cwd, from, to, limit = 20) {
8633
- if (from && !SAFE_REF.test(from)) {
8634
- return { ok: false, reason: "invalid_ref", message: `Invalid git ref: ${from}` };
8635
- }
8636
- if (to && !SAFE_REF.test(to)) {
8637
- return { ok: false, reason: "invalid_ref", message: `Invalid git ref: ${to}` };
8638
- }
8639
- if (!from || !to) {
8640
- return { ok: true, data: [] };
8641
- }
8642
- return git(
8643
- cwd,
8644
- ["log", "--oneline", `-${limit}`, `${from}..${to}`],
8645
- (out) => out.split("\n").filter((l) => l.trim().length > 0)
8646
- );
8647
- }
8648
- function parseDiffNumstat(out) {
8649
- const lines = out.split("\n").filter((l) => l.length > 0);
8650
- let insertions = 0;
8651
- let deletions = 0;
8652
- let filesChanged = 0;
8653
- for (const line of lines) {
8654
- const parts = line.split(" ");
8655
- if (parts.length < 3) continue;
8656
- const added = parseInt(parts[0], 10);
8657
- const removed = parseInt(parts[1], 10);
8658
- if (!Number.isNaN(added)) insertions += added;
8659
- if (!Number.isNaN(removed)) deletions += removed;
8660
- filesChanged++;
8661
- }
8662
- return { filesChanged, insertions, deletions, totalLines: insertions + deletions };
8663
- }
8664
- var GIT_TIMEOUT, SAFE_REF;
8665
- var init_git_inspector = __esm({
8666
- "src/autonomous/git-inspector.ts"() {
8667
- "use strict";
8668
- init_esm_shims();
8669
- GIT_TIMEOUT = 1e4;
8670
- SAFE_REF = /^[0-9a-f]{4,40}$/i;
8671
- }
8672
- });
8673
-
8674
- // src/autonomous/recipes/loader.ts
8675
- import { readFileSync as readFileSync7 } from "fs";
8676
- import { join as join14, dirname as dirname3 } from "path";
8677
- import { fileURLToPath as fileURLToPath2 } from "url";
8678
- function loadRecipe(recipeName) {
8679
- if (!/^[A-Za-z0-9_-]+$/.test(recipeName)) {
8680
- throw new Error(`Invalid recipe name: ${recipeName}`);
8681
- }
8682
- const recipesDir = join14(dirname3(fileURLToPath2(import.meta.url)), "..", "recipes");
8683
- const path2 = join14(recipesDir, `${recipeName}.json`);
8684
- const raw = readFileSync7(path2, "utf-8");
8685
- return JSON.parse(raw);
8847
+ // src/autonomous/recipes/loader.ts
8848
+ import { readFileSync as readFileSync7 } from "fs";
8849
+ import { join as join14, dirname as dirname3 } from "path";
8850
+ import { fileURLToPath as fileURLToPath2 } from "url";
8851
+ function loadRecipe(recipeName) {
8852
+ if (!/^[A-Za-z0-9_-]+$/.test(recipeName)) {
8853
+ throw new Error(`Invalid recipe name: ${recipeName}`);
8854
+ }
8855
+ const recipesDir = join14(dirname3(fileURLToPath2(import.meta.url)), "..", "recipes");
8856
+ const path2 = join14(recipesDir, `${recipeName}.json`);
8857
+ const raw = readFileSync7(path2, "utf-8");
8858
+ return JSON.parse(raw);
8686
8859
  }
8687
8860
  function resolveRecipe(recipeName, projectOverrides) {
8688
8861
  let raw;
@@ -8736,6 +8909,13 @@ function resolveRecipe(recipeName, projectOverrides) {
8736
8909
  pipeline.splice(implementIdx + 1, 0, "TEST");
8737
8910
  }
8738
8911
  }
8912
+ if (stages2.ISSUE_FIX?.enableCodeReview) {
8913
+ if (pipeline.includes("VERIFY") || pipeline.includes("BUILD")) {
8914
+ throw new Error(
8915
+ "ISSUE_FIX.enableCodeReview is incompatible with VERIFY/BUILD in the pipeline (issue fixes use goto transitions, not pipeline walker)"
8916
+ );
8917
+ }
8918
+ }
8739
8919
  const postComplete = raw.postComplete ? [...raw.postComplete] : [];
8740
8920
  const recipeDefaults = raw.defaults ?? {};
8741
8921
  const defaults = {
@@ -9072,7 +9252,15 @@ var init_pick_ticket = __esm({
9072
9252
  PickTicketStage = class {
9073
9253
  id = "PICK_TICKET";
9074
9254
  async enter(ctx) {
9075
- const { state: projectState } = await ctx.loadProject();
9255
+ let projectState;
9256
+ try {
9257
+ ({ state: projectState } = await ctx.loadProject());
9258
+ } catch (err) {
9259
+ return {
9260
+ action: "retry",
9261
+ instruction: `Failed to load project state: ${err instanceof Error ? err.message : String(err)}. Check .story/ files for corruption, then call autonomous_guide with action "report" again.`
9262
+ };
9263
+ }
9076
9264
  if (isTargetedMode(ctx.state)) {
9077
9265
  const remaining = getRemainingTargets(ctx.state);
9078
9266
  if (remaining.length === 0) {
@@ -9172,7 +9360,12 @@ var init_pick_ticket = __esm({
9172
9360
  }
9173
9361
  const targetReject = this.enforceTargetList(ctx, ticketId);
9174
9362
  if (targetReject) return targetReject;
9175
- const { state: projectState } = await ctx.loadProject();
9363
+ let projectState;
9364
+ try {
9365
+ ({ state: projectState } = await ctx.loadProject());
9366
+ } catch (err) {
9367
+ return { action: "retry", instruction: `Failed to load project state: ${err instanceof Error ? err.message : String(err)}. Check .story/ files for corruption.` };
9368
+ }
9176
9369
  const ticket = projectState.ticketByID(ticketId);
9177
9370
  if (!ticket) {
9178
9371
  return { action: "retry", instruction: `Ticket ${ticketId} not found. Pick a valid ticket.` };
@@ -9194,7 +9387,8 @@ var init_pick_ticket = __esm({
9194
9387
  ctx.updateDraft({
9195
9388
  ticket: { id: ticket.id, title: ticket.title, claimed: true },
9196
9389
  reviews: { plan: [], code: [] },
9197
- finalizeCheckpoint: null
9390
+ finalizeCheckpoint: null,
9391
+ ticketStartedAt: (/* @__PURE__ */ new Date()).toISOString()
9198
9392
  });
9199
9393
  return {
9200
9394
  action: "advance",
@@ -9225,7 +9419,12 @@ ${ticket.description}` : "",
9225
9419
  async handleIssuePick(ctx, issueId) {
9226
9420
  const targetReject = this.enforceTargetList(ctx, issueId);
9227
9421
  if (targetReject) return targetReject;
9228
- const { state: projectState } = await ctx.loadProject();
9422
+ let projectState;
9423
+ try {
9424
+ ({ state: projectState } = await ctx.loadProject());
9425
+ } catch (err) {
9426
+ return { action: "retry", instruction: `Failed to load project state: ${err instanceof Error ? err.message : String(err)}. Check .story/ files for corruption.` };
9427
+ }
9229
9428
  const issue = projectState.issues.find((i) => i.id === issueId);
9230
9429
  if (!issue) {
9231
9430
  return { action: "retry", instruction: `Issue ${issueId} not found. Pick a valid issue or ticket.` };
@@ -9234,11 +9433,16 @@ ${ticket.description}` : "",
9234
9433
  if (issue.status !== "open" && !(targeted && issue.status === "inprogress")) {
9235
9434
  return { action: "retry", instruction: `Issue ${issueId} is ${issue.status}. Pick an open issue.` };
9236
9435
  }
9436
+ const transitionId = `issue-pick-${issueId}-${Date.now()}`;
9437
+ ctx.writeState({
9438
+ pendingProjectMutation: { type: "issue_update", target: issueId, field: "status", value: "inprogress", expectedCurrent: issue.status, transitionId }
9439
+ });
9237
9440
  try {
9238
9441
  const { handleIssueUpdate: handleIssueUpdate2 } = await Promise.resolve().then(() => (init_issue2(), issue_exports));
9239
- await handleIssueUpdate2({ id: issueId, status: "inprogress" }, "json", ctx.root);
9442
+ await handleIssueUpdate2(issueId, { status: "inprogress" }, "json", ctx.root);
9240
9443
  } catch {
9241
9444
  }
9445
+ ctx.writeState({ pendingProjectMutation: null });
9242
9446
  ctx.updateDraft({
9243
9447
  currentIssue: { id: issue.id, title: issue.title, severity: issue.severity },
9244
9448
  ticket: void 0,
@@ -9394,7 +9598,7 @@ var init_plan_review = __esm({
9394
9598
  const backends = ctx.state.config.reviewBackends;
9395
9599
  const existingReviews = ctx.state.reviews.plan;
9396
9600
  const roundNum = existingReviews.length + 1;
9397
- const reviewer = nextReviewer(existingReviews, backends);
9601
+ const reviewer = nextReviewer(existingReviews, backends, ctx.state.codexUnavailable, ctx.state.codexUnavailableSince);
9398
9602
  const risk = ctx.state.ticket?.risk ?? "low";
9399
9603
  const minRounds = requiredRounds(risk);
9400
9604
  if (reviewer === "lenses") {
@@ -9405,10 +9609,11 @@ var init_plan_review = __esm({
9405
9609
  "This round uses the **multi-lens review orchestrator** for plan review. It fans out to specialized review agents (Clean Code, Security, Error Handling, and more) in parallel to evaluate the plan from multiple perspectives.",
9406
9610
  "",
9407
9611
  "1. Read the plan file",
9408
- "2. Call `prepareLensReview()` with the plan text (stage: PLAN_REVIEW)",
9409
- "3. Spawn all lens subagents in parallel",
9410
- "4. Collect results and pass through the merger and judge pipeline",
9411
- "5. Report the final SynthesisResult verdict and findings",
9612
+ "2. Call `claudestory_review_lenses_prepare` with the plan text as diff, stage: PLAN_REVIEW, and ticketDescription",
9613
+ "3. Spawn all lens subagents in parallel (each prompt is returned by the prepare tool)",
9614
+ "4. Collect results and call `claudestory_review_lenses_synthesize` with the lens results",
9615
+ "5. Run the merger agent with the returned mergerPrompt, then call `claudestory_review_lenses_judge`",
9616
+ "6. Run the judge agent and report the final SynthesisResult verdict and findings",
9412
9617
  "",
9413
9618
  "When done, call `claudestory_autonomous_guide` with:",
9414
9619
  "```json",
@@ -9435,7 +9640,11 @@ var init_plan_review = __esm({
9435
9640
  `{ "sessionId": "${ctx.state.sessionId}", "action": "report", "report": { "completedAction": "plan_review_round", "verdict": "<approve|revise|request_changes|reject>", "findings": [...] } }`,
9436
9641
  "```"
9437
9642
  ].join("\n"),
9438
- reminders: ["Report the exact verdict and findings from the reviewer."],
9643
+ reminders: [
9644
+ "Report the exact verdict and findings from the reviewer.",
9645
+ "IMPORTANT: After the review, file ANY pre-existing issues discovered using claudestory_issue_create with severity and impact. Do NOT skip this step.",
9646
+ ...reviewer === "codex" ? ["If codex is unavailable (usage limit, error, etc.), fall back to agent review and include 'codex unavailable' in your report notes."] : []
9647
+ ],
9439
9648
  transitionedFrom: ctx.state.previousState ?? void 0
9440
9649
  };
9441
9650
  }
@@ -9448,7 +9657,8 @@ var init_plan_review = __esm({
9448
9657
  const roundNum = planReviews.length + 1;
9449
9658
  const findings = report.findings ?? [];
9450
9659
  const backends = ctx.state.config.reviewBackends;
9451
- const reviewerBackend = nextReviewer(planReviews, backends);
9660
+ const computedReviewer = nextReviewer(planReviews, backends, ctx.state.codexUnavailable, ctx.state.codexUnavailableSince);
9661
+ const reviewerBackend = report.reviewer ?? (computedReviewer === "codex" && report.notes && /codex\b.*\b(unavail|limit|failed|down|error|usage)/i.test(report.notes) ? "agent" : null) ?? computedReviewer;
9452
9662
  planReviews.push({
9453
9663
  round: roundNum,
9454
9664
  reviewer: reviewerBackend,
@@ -9460,6 +9670,9 @@ var init_plan_review = __esm({
9460
9670
  codexSessionId: report.reviewerSessionId,
9461
9671
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
9462
9672
  });
9673
+ if (report.notes && /codex\b.*\b(unavail|limit|failed|down|error|usage)/i.test(report.notes)) {
9674
+ ctx.writeState({ codexUnavailable: true, codexUnavailableSince: (/* @__PURE__ */ new Date()).toISOString() });
9675
+ }
9463
9676
  const risk = ctx.state.ticket?.risk ?? "low";
9464
9677
  const minRounds = requiredRounds(risk);
9465
9678
  const hasCriticalOrMajor = findings.some(
@@ -9547,7 +9760,7 @@ var init_plan_review = __esm({
9547
9760
  }
9548
9761
  return { action: "advance" };
9549
9762
  }
9550
- const nextReviewerName = nextReviewer(planReviews, backends);
9763
+ const nextReviewerName = nextReviewer(planReviews, backends, ctx.state.codexUnavailable, ctx.state.codexUnavailableSince);
9551
9764
  return {
9552
9765
  action: "retry",
9553
9766
  instruction: [
@@ -9697,7 +9910,7 @@ var init_write_tests = __esm({
9697
9910
  const exitMatch = notes.match(EXIT_CODE_REGEX);
9698
9911
  const exitCode = exitMatch ? parseInt(exitMatch[1], 10) : -1;
9699
9912
  const failMatch = notes.match(FAIL_COUNT_REGEX);
9700
- const currentFailCount = failMatch ? parseInt(failMatch[1], 10) : -1;
9913
+ const currentFailCount = failMatch ? parseInt(failMatch[1], 10) : exitCode === 0 ? 0 : -1;
9701
9914
  const baseline = ctx.state.testBaseline;
9702
9915
  const baselineFailCount = baseline?.failCount ?? -1;
9703
9916
  const nextRetry = retryCount + 1;
@@ -9874,26 +10087,29 @@ var init_code_review = __esm({
9874
10087
  const backends = ctx.state.config.reviewBackends;
9875
10088
  const codeReviews = ctx.state.reviews.code;
9876
10089
  const roundNum = codeReviews.length + 1;
9877
- const reviewer = nextReviewer(codeReviews, backends);
10090
+ const reviewer = nextReviewer(codeReviews, backends, ctx.state.codexUnavailable, ctx.state.codexUnavailableSince);
9878
10091
  const risk = ctx.state.ticket?.realizedRisk ?? ctx.state.ticket?.risk ?? "low";
9879
10092
  const rounds = requiredRounds(risk);
9880
10093
  const mergeBase = ctx.state.git.mergeBase;
10094
+ const isIssueFix = !!ctx.state.currentIssue;
10095
+ const issueHeader = isIssueFix ? `Issue Fix Code Review (${ctx.state.currentIssue.id})` : "Code Review";
9881
10096
  const diffCommand = mergeBase ? `\`git diff ${mergeBase}\`` : `\`git diff HEAD\` AND \`git ls-files --others --exclude-standard\``;
9882
10097
  const diffReminder = mergeBase ? `Run: git diff ${mergeBase} \u2014 pass FULL output to reviewer.` : "Run: git diff HEAD + git ls-files --others --exclude-standard \u2014 pass FULL output to reviewer.";
9883
10098
  if (reviewer === "lenses") {
9884
10099
  return {
9885
10100
  instruction: [
9886
- `# Multi-Lens Code Review \u2014 Round ${roundNum} of ${rounds} minimum`,
10101
+ `# Multi-Lens ${issueHeader} \u2014 Round ${roundNum} of ${rounds} minimum`,
9887
10102
  "",
9888
10103
  `Capture the diff with: ${diffCommand}`,
9889
10104
  "",
9890
10105
  "This round uses the **multi-lens review orchestrator**. It fans out to specialized review agents (Clean Code, Security, Error Handling, and more) in parallel, then synthesizes findings into a single verdict.",
9891
10106
  "",
9892
- "1. Capture the full diff",
9893
- "2. Call `prepareLensReview()` with the diff and changed file list",
9894
- "3. Spawn all lens subagents in parallel (each prompt is provided by the orchestrator)",
9895
- "4. Collect results and pass through the merger and judge pipeline",
9896
- "5. Report the final SynthesisResult verdict and findings",
10107
+ "1. Capture the full diff and changed file list (`git diff --name-only`)",
10108
+ "2. Call `claudestory_review_lenses_prepare` with the diff, changedFiles, stage: CODE_REVIEW, and ticketDescription",
10109
+ "3. Spawn all lens subagents in parallel (each prompt is returned by the prepare tool)",
10110
+ "4. Collect results and call `claudestory_review_lenses_synthesize` with the lens results, plus the diff and changedFiles from step 1 and the sessionId (enables automatic origin classification and issue filing for pre-existing findings)",
10111
+ "5. Run the merger agent with the returned mergerPrompt, then call `claudestory_review_lenses_judge`",
10112
+ "6. Run the judge agent and report the final SynthesisResult verdict and findings",
9897
10113
  "",
9898
10114
  "When done, report verdict and findings."
9899
10115
  ].join("\n"),
@@ -9901,14 +10117,14 @@ var init_code_review = __esm({
9901
10117
  diffReminder,
9902
10118
  "Do NOT compress or summarize the diff.",
9903
10119
  "Lens subagents run in parallel with read-only tools (Read, Grep, Glob).",
9904
- "If the reviewer flags pre-existing issues unrelated to your changes, file them as issues using claudestory_issue_create with severity and impact. Do not fix them in this ticket."
10120
+ "Pre-existing issues in surrounding code are automatically classified and filed by the synthesize tool when you pass diff, changedFiles, and sessionId. Check filedIssues in the synthesize response."
9905
10121
  ],
9906
10122
  transitionedFrom: ctx.state.previousState ?? void 0
9907
10123
  };
9908
10124
  }
9909
10125
  return {
9910
10126
  instruction: [
9911
- `# Code Review \u2014 Round ${roundNum} of ${rounds} minimum`,
10127
+ `# ${issueHeader} \u2014 Round ${roundNum} of ${rounds} minimum`,
9912
10128
  "",
9913
10129
  `Capture the diff with: ${diffCommand}`,
9914
10130
  "",
@@ -9920,7 +10136,8 @@ var init_code_review = __esm({
9920
10136
  reminders: [
9921
10137
  diffReminder,
9922
10138
  "Do NOT compress or summarize the diff.",
9923
- "If the reviewer flags pre-existing issues unrelated to your changes, file them as issues using claudestory_issue_create with severity and impact. Do not fix them in this ticket."
10139
+ "If the reviewer flags pre-existing issues unrelated to your changes, file them as issues using claudestory_issue_create with severity and impact. Do not fix them in this ticket.",
10140
+ ...reviewer === "codex" ? ["If codex is unavailable (usage limit, error, etc.), fall back to agent review and include 'codex unavailable' in your report notes."] : []
9924
10141
  ],
9925
10142
  transitionedFrom: ctx.state.previousState ?? void 0
9926
10143
  };
@@ -9934,7 +10151,8 @@ var init_code_review = __esm({
9934
10151
  const roundNum = codeReviews.length + 1;
9935
10152
  const findings = report.findings ?? [];
9936
10153
  const backends = ctx.state.config.reviewBackends;
9937
- const reviewerBackend = nextReviewer(codeReviews, backends);
10154
+ const computedReviewer = nextReviewer(codeReviews, backends, ctx.state.codexUnavailable, ctx.state.codexUnavailableSince);
10155
+ const reviewerBackend = report.reviewer ?? (computedReviewer === "codex" && report.notes && /codex\b.*\b(unavail|limit|failed|down|error|usage)/i.test(report.notes) ? "agent" : null) ?? computedReviewer;
9938
10156
  codeReviews.push({
9939
10157
  round: roundNum,
9940
10158
  reviewer: reviewerBackend,
@@ -9946,6 +10164,9 @@ var init_code_review = __esm({
9946
10164
  codexSessionId: report.reviewerSessionId,
9947
10165
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
9948
10166
  });
10167
+ if (report.notes && /codex\b.*\b(unavail|limit|failed|down|error|usage)/i.test(report.notes)) {
10168
+ ctx.writeState({ codexUnavailable: true, codexUnavailableSince: (/* @__PURE__ */ new Date()).toISOString() });
10169
+ }
9949
10170
  const risk = ctx.state.ticket?.realizedRisk ?? ctx.state.ticket?.risk ?? "low";
9950
10171
  const minRounds = requiredRounds(risk);
9951
10172
  const hasCriticalOrMajor = findings.some(
@@ -9970,6 +10191,7 @@ var init_code_review = __esm({
9970
10191
  } else {
9971
10192
  nextAction = "CODE_REVIEW";
9972
10193
  }
10194
+ const isIssueFix = !!ctx.state.currentIssue;
9973
10195
  if (nextAction === "PLAN") {
9974
10196
  clearCache(ctx.dir);
9975
10197
  ctx.writeState({
@@ -9981,9 +10203,12 @@ var init_code_review = __esm({
9981
10203
  round: roundNum,
9982
10204
  verdict,
9983
10205
  findingCount: findings.length,
9984
- redirectedTo: "PLAN"
10206
+ redirectedTo: isIssueFix ? "ISSUE_FIX" : "PLAN"
9985
10207
  });
9986
10208
  await ctx.fileDeferredFindings(findings, "code");
10209
+ if (isIssueFix) {
10210
+ return { action: "goto", target: "ISSUE_FIX" };
10211
+ }
9987
10212
  return { action: "back", target: "PLAN", reason: "plan_redirect" };
9988
10213
  }
9989
10214
  const stateUpdate = {
@@ -10006,6 +10231,9 @@ var init_code_review = __esm({
10006
10231
  });
10007
10232
  await ctx.fileDeferredFindings(findings, "code");
10008
10233
  if (nextAction === "IMPLEMENT") {
10234
+ if (isIssueFix) {
10235
+ return { action: "goto", target: "ISSUE_FIX" };
10236
+ }
10009
10237
  return { action: "back", target: "IMPLEMENT", reason: "request_changes" };
10010
10238
  }
10011
10239
  if (nextAction === "FINALIZE") {
@@ -10032,7 +10260,7 @@ var init_code_review = __esm({
10032
10260
  }
10033
10261
  return { action: "advance" };
10034
10262
  }
10035
- const nextReviewerName = nextReviewer(codeReviews, backends);
10263
+ const nextReviewerName = nextReviewer(codeReviews, backends, ctx.state.codexUnavailable, ctx.state.codexUnavailableSince);
10036
10264
  const mergeBase = ctx.state.git.mergeBase;
10037
10265
  return {
10038
10266
  action: "retry",
@@ -10336,16 +10564,49 @@ var init_finalize = __esm({
10336
10564
  FinalizeStage = class {
10337
10565
  id = "FINALIZE";
10338
10566
  async enter(ctx) {
10567
+ if (ctx.state.finalizeCheckpoint === "committed") {
10568
+ return { action: "advance" };
10569
+ }
10570
+ const previousHead = ctx.state.git.expectedHead ?? ctx.state.git.initHead;
10571
+ if (previousHead) {
10572
+ const headResult = await gitHead(ctx.root);
10573
+ if (headResult.ok && headResult.data.hash !== previousHead) {
10574
+ const treeResult = await gitDiffTreeNames(ctx.root, headResult.data.hash);
10575
+ const ticketId = ctx.state.ticket?.id;
10576
+ if (ticketId) {
10577
+ const ticketPath = `.story/tickets/${ticketId}.json`;
10578
+ if (treeResult.ok && !treeResult.data.includes(ticketPath)) {
10579
+ } else {
10580
+ ctx.writeState({ finalizeCheckpoint: "precommit_passed" });
10581
+ return this.handleCommit(ctx, { completedAction: "commit_done", commitHash: headResult.data.hash });
10582
+ }
10583
+ }
10584
+ const issueId = ctx.state.currentIssue?.id;
10585
+ if (issueId) {
10586
+ const issuePath = `.story/issues/${issueId}.json`;
10587
+ if (treeResult.ok && !treeResult.data.includes(issuePath)) {
10588
+ } else {
10589
+ ctx.writeState({ finalizeCheckpoint: "precommit_passed" });
10590
+ return this.handleCommit(ctx, { completedAction: "commit_done", commitHash: headResult.data.hash });
10591
+ }
10592
+ }
10593
+ if (!ticketId && !issueId) {
10594
+ ctx.writeState({ finalizeCheckpoint: "precommit_passed" });
10595
+ return this.handleCommit(ctx, { completedAction: "commit_done", commitHash: headResult.data.hash });
10596
+ }
10597
+ }
10598
+ }
10339
10599
  return {
10340
10600
  instruction: [
10341
10601
  "# Finalize",
10342
10602
  "",
10343
10603
  "Code review passed. Time to commit.",
10344
10604
  "",
10345
- ctx.state.ticket ? `1. Update ticket ${ctx.state.ticket.id} status to "complete" in .story/` : "",
10346
- ctx.state.currentIssue ? `1. Ensure issue ${ctx.state.currentIssue.id} status is "resolved" in .story/issues/` : "",
10347
- "2. Stage only the files you created or modified for this work (code + .story/ changes). Do NOT use `git add -A` or `git add .`",
10348
- '3. Call me with completedAction: "files_staged"'
10605
+ "1. Run `git reset` to clear the staging area (ensures no stale files from prior operations)",
10606
+ ctx.state.ticket ? `2. Update ticket ${ctx.state.ticket.id} status to "complete" in .story/` : "",
10607
+ ctx.state.currentIssue ? `2. Ensure .story/issues/${ctx.state.currentIssue.id}.json is updated with status: "resolved"` : "",
10608
+ "3. Stage only the files you modified for this fix (code + .story/ changes). Do NOT use `git add -A` or `git add .`",
10609
+ '4. Call me with completedAction: "files_staged"'
10349
10610
  ].filter(Boolean).join("\n"),
10350
10611
  reminders: [
10351
10612
  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."
@@ -10366,6 +10627,9 @@ var init_finalize = __esm({
10366
10627
  return this.handlePrecommit(ctx);
10367
10628
  }
10368
10629
  if (action === "commit_done") {
10630
+ if (!checkpoint) {
10631
+ ctx.writeState({ finalizeCheckpoint: "precommit_passed" });
10632
+ }
10369
10633
  return this.handleCommit(ctx, report);
10370
10634
  }
10371
10635
  return {
@@ -10379,12 +10643,12 @@ var init_finalize = __esm({
10379
10643
  return {
10380
10644
  action: "retry",
10381
10645
  instruction: [
10382
- "Files staged. Now run pre-commit checks.",
10646
+ "Files staged. Now commit.",
10383
10647
  "",
10384
- 'Run any pre-commit hooks or linting, then call me with completedAction: "precommit_passed".',
10385
- 'If pre-commit fails, fix the issues, re-stage, and call me with completedAction: "files_staged" again.'
10386
- ].join("\n"),
10387
- reminders: ["Verify staged set is intact after pre-commit hooks."]
10648
+ ctx.state.ticket ? `Commit with message: "feat: <description> (${ctx.state.ticket.id})"` : "Commit with a descriptive message.",
10649
+ "",
10650
+ 'Call me with completedAction: "commit_done" and include the commitHash.'
10651
+ ].join("\n")
10388
10652
  };
10389
10653
  }
10390
10654
  const stagedResult = await gitDiffCachedNames(ctx.root);
@@ -10419,15 +10683,14 @@ var init_finalize = __esm({
10419
10683
  return { action: "retry", instruction: 'No files are staged. Stage your changes and call me again with completedAction: "files_staged".' };
10420
10684
  }
10421
10685
  const baselineUntracked = ctx.state.git.baseline?.untrackedPaths ?? [];
10422
- let overlapOverridden = false;
10423
10686
  if (baselineUntracked.length > 0) {
10424
10687
  const sessionTicketPath = ctx.state.ticket?.id ? `.story/tickets/${ctx.state.ticket.id}.json` : null;
10688
+ const sessionIssuePath = ctx.state.currentIssue?.id ? `.story/issues/${ctx.state.currentIssue.id}.json` : null;
10425
10689
  const overlap = stagedResult.data.filter(
10426
- (f) => baselineUntracked.includes(f) && f !== sessionTicketPath
10690
+ (f) => baselineUntracked.includes(f) && f !== sessionTicketPath && f !== sessionIssuePath
10427
10691
  );
10428
10692
  if (overlap.length > 0) {
10429
10693
  if (report.overrideOverlap) {
10430
- overlapOverridden = true;
10431
10694
  } else {
10432
10695
  return {
10433
10696
  action: "retry",
@@ -10457,17 +10720,17 @@ var init_finalize = __esm({
10457
10720
  }
10458
10721
  }
10459
10722
  ctx.writeState({
10460
- finalizeCheckpoint: overlapOverridden ? "staged_override" : "staged"
10723
+ finalizeCheckpoint: "precommit_passed"
10461
10724
  });
10462
10725
  return {
10463
10726
  action: "retry",
10464
10727
  instruction: [
10465
- "Files staged. Now run pre-commit checks.",
10728
+ "Files staged. Now commit.",
10466
10729
  "",
10467
- 'Run any pre-commit hooks or linting, then call me with completedAction: "precommit_passed".',
10468
- 'If pre-commit fails, fix the issues, re-stage, and call me with completedAction: "files_staged" again.'
10469
- ].join("\n"),
10470
- reminders: ["Verify staged set is intact after pre-commit hooks."]
10730
+ ctx.state.ticket ? `Commit with message: "feat: <description> (${ctx.state.ticket.id})"` : "Commit with a descriptive message.",
10731
+ "",
10732
+ 'Call me with completedAction: "commit_done" and include the commitHash.'
10733
+ ].join("\n")
10471
10734
  };
10472
10735
  }
10473
10736
  async handlePrecommit(ctx) {
@@ -10484,8 +10747,9 @@ var init_finalize = __esm({
10484
10747
  const baselineUntracked = ctx.state.git.baseline?.untrackedPaths ?? [];
10485
10748
  if (baselineUntracked.length > 0) {
10486
10749
  const sessionTicketPath = ctx.state.ticket?.id ? `.story/tickets/${ctx.state.ticket.id}.json` : null;
10750
+ const sessionIssuePath = ctx.state.currentIssue?.id ? `.story/issues/${ctx.state.currentIssue.id}.json` : null;
10487
10751
  const overlap = stagedResult.data.filter(
10488
- (f) => baselineUntracked.includes(f) && f !== sessionTicketPath
10752
+ (f) => baselineUntracked.includes(f) && f !== sessionTicketPath && f !== sessionIssuePath
10489
10753
  );
10490
10754
  if (overlap.length > 0) {
10491
10755
  ctx.writeState({ finalizeCheckpoint: null });
@@ -10556,6 +10820,7 @@ var init_finalize = __esm({
10556
10820
  finalizeCheckpoint: "committed",
10557
10821
  resolvedIssues: [...ctx.state.resolvedIssues ?? [], currentIssue.id],
10558
10822
  currentIssue: null,
10823
+ ticketStartedAt: null,
10559
10824
  git: {
10560
10825
  ...ctx.state.git,
10561
10826
  mergeBase: normalizedHash,
@@ -10565,11 +10830,20 @@ var init_finalize = __esm({
10565
10830
  ctx.appendEvent("commit", { commitHash: normalizedHash, issueId: currentIssue.id });
10566
10831
  return { action: "goto", target: "COMPLETE" };
10567
10832
  }
10568
- 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;
10833
+ const completedTicket = ctx.state.ticket ? {
10834
+ id: ctx.state.ticket.id,
10835
+ title: ctx.state.ticket.title,
10836
+ commitHash: normalizedHash,
10837
+ risk: ctx.state.ticket.risk,
10838
+ realizedRisk: ctx.state.ticket.realizedRisk,
10839
+ startedAt: ctx.state.ticketStartedAt ?? void 0,
10840
+ completedAt: (/* @__PURE__ */ new Date()).toISOString()
10841
+ } : void 0;
10569
10842
  ctx.writeState({
10570
10843
  finalizeCheckpoint: "committed",
10571
10844
  completedTickets: completedTicket ? [...ctx.state.completedTickets, completedTicket] : ctx.state.completedTickets,
10572
10845
  ticket: void 0,
10846
+ ticketStartedAt: null,
10573
10847
  git: {
10574
10848
  ...ctx.state.git,
10575
10849
  mergeBase: normalizedHash,
@@ -10612,7 +10886,7 @@ var init_complete = __esm({
10612
10886
  target: "HANDOVER",
10613
10887
  result: {
10614
10888
  instruction: [
10615
- `# Ticket Complete \u2014 ${mode} mode session ending`,
10889
+ `# Ticket Complete -- ${mode} mode session ending`,
10616
10890
  "",
10617
10891
  `Ticket **${ctx.state.ticket?.id}** completed. Write a brief session handover.`,
10618
10892
  "",
@@ -10623,36 +10897,23 @@ var init_complete = __esm({
10623
10897
  }
10624
10898
  };
10625
10899
  }
10626
- const handoverInterval = ctx.state.config.handoverInterval ?? 5;
10627
- if (handoverInterval > 0 && totalWorkDone > 0 && totalWorkDone % handoverInterval === 0) {
10628
- try {
10629
- const { handleHandoverCreate: handleHandoverCreate3 } = await Promise.resolve().then(() => (init_handover(), handover_exports));
10630
- const completedIds = ctx.state.completedTickets.map((t) => t.id).join(", ");
10631
- const resolvedIds = (ctx.state.resolvedIssues ?? []).join(", ");
10632
- const content = [
10633
- `# Checkpoint \u2014 ${totalWorkDone} items completed`,
10634
- "",
10635
- `**Session:** ${ctx.state.sessionId}`,
10636
- ...completedIds ? [`**Tickets:** ${completedIds}`] : [],
10637
- ...resolvedIds ? [`**Issues resolved:** ${resolvedIds}`] : [],
10638
- "",
10639
- "This is an automatic mid-session checkpoint. The session is still active."
10640
- ].join("\n");
10641
- await handleHandoverCreate3(content, "checkpoint", "md", ctx.root);
10642
- } catch {
10643
- }
10644
- try {
10645
- const { loadProject: loadProject2 } = await Promise.resolve().then(() => (init_project_loader(), project_loader_exports));
10646
- const { saveSnapshot: saveSnapshot2 } = await Promise.resolve().then(() => (init_snapshot(), snapshot_exports));
10647
- const loadResult = await loadProject2(ctx.root);
10648
- await saveSnapshot2(ctx.root, loadResult);
10649
- } catch {
10650
- }
10651
- ctx.appendEvent("checkpoint", { ticketsDone, issuesDone, totalWorkDone, interval: handoverInterval });
10900
+ await this.tryCheckpoint(ctx, totalWorkDone, ticketsDone, issuesDone);
10901
+ let projectState;
10902
+ try {
10903
+ ({ state: projectState } = await ctx.loadProject());
10904
+ } catch (err) {
10905
+ return {
10906
+ action: "goto",
10907
+ target: "HANDOVER",
10908
+ result: {
10909
+ instruction: `Failed to load project state: ${err instanceof Error ? err.message : String(err)}. Ending session -- write a handover noting the error.`,
10910
+ reminders: [],
10911
+ transitionedFrom: "COMPLETE"
10912
+ }
10913
+ };
10652
10914
  }
10653
- const { state: projectState } = await ctx.loadProject();
10654
- let nextTarget;
10655
10915
  const targetedRemaining = isTargetedMode(ctx.state) ? getRemainingTargets(ctx.state) : null;
10916
+ let nextTarget;
10656
10917
  if (targetedRemaining !== null) {
10657
10918
  nextTarget = targetedRemaining.length === 0 ? "HANDOVER" : "PICK_TICKET";
10658
10919
  } else if (maxTickets > 0 && totalWorkDone >= maxTickets) {
@@ -10667,66 +10928,118 @@ var init_complete = __esm({
10667
10928
  }
10668
10929
  }
10669
10930
  if (nextTarget === "HANDOVER") {
10670
- const postComplete = ctx.state.resolvedPostComplete ?? ctx.recipe.postComplete;
10671
- const postResult = findFirstPostComplete(postComplete, ctx);
10672
- if (postResult.kind === "found") {
10673
- ctx.writeState({ pipelinePhase: "postComplete" });
10674
- return { action: "goto", target: postResult.stage.id };
10931
+ return this.buildHandoverResult(ctx, targetedRemaining, ticketsDone, issuesDone);
10932
+ }
10933
+ if (targetedRemaining !== null) {
10934
+ return this.buildTargetedPickResult(ctx, targetedRemaining, projectState);
10935
+ }
10936
+ return this.buildStandardPickResult(ctx, projectState, ticketsDone, maxTickets);
10937
+ }
10938
+ async report(ctx, _report) {
10939
+ return this.enter(ctx);
10940
+ }
10941
+ // ---------------------------------------------------------------------------
10942
+ // Checkpoint -- mid-session handover + snapshot (best-effort)
10943
+ // ---------------------------------------------------------------------------
10944
+ async tryCheckpoint(ctx, totalWorkDone, ticketsDone, issuesDone) {
10945
+ const handoverInterval = ctx.state.config.handoverInterval ?? 5;
10946
+ if (handoverInterval <= 0 || totalWorkDone <= 0 || totalWorkDone % handoverInterval !== 0) return;
10947
+ try {
10948
+ const { handleHandoverCreate: handleHandoverCreate3 } = await Promise.resolve().then(() => (init_handover(), handover_exports));
10949
+ const completedIds = ctx.state.completedTickets.map((t) => t.id).join(", ");
10950
+ const resolvedIds = (ctx.state.resolvedIssues ?? []).join(", ");
10951
+ const content = [
10952
+ `# Checkpoint -- ${totalWorkDone} items completed`,
10953
+ "",
10954
+ `**Session:** ${ctx.state.sessionId}`,
10955
+ ...completedIds ? [`**Tickets:** ${completedIds}`] : [],
10956
+ ...resolvedIds ? [`**Issues resolved:** ${resolvedIds}`] : [],
10957
+ "",
10958
+ "This is an automatic mid-session checkpoint. The session is still active."
10959
+ ].join("\n");
10960
+ await handleHandoverCreate3(content, "checkpoint", "md", ctx.root);
10961
+ } catch {
10962
+ }
10963
+ try {
10964
+ const { loadProject: loadProject2 } = await Promise.resolve().then(() => (init_project_loader(), project_loader_exports));
10965
+ const { saveSnapshot: saveSnapshot2 } = await Promise.resolve().then(() => (init_snapshot(), snapshot_exports));
10966
+ const loadResult = await loadProject2(ctx.root);
10967
+ await saveSnapshot2(ctx.root, loadResult);
10968
+ } catch {
10969
+ }
10970
+ ctx.appendEvent("checkpoint", { ticketsDone, issuesDone, totalWorkDone, interval: handoverInterval });
10971
+ }
10972
+ // ---------------------------------------------------------------------------
10973
+ // HANDOVER instruction -- session ending
10974
+ // ---------------------------------------------------------------------------
10975
+ buildHandoverResult(ctx, targetedRemaining, ticketsDone, issuesDone) {
10976
+ const postComplete = ctx.state.resolvedPostComplete ?? ctx.recipe.postComplete;
10977
+ const postResult = findFirstPostComplete(postComplete, ctx);
10978
+ if (postResult.kind === "found") {
10979
+ ctx.writeState({ pipelinePhase: "postComplete" });
10980
+ return { action: "goto", target: postResult.stage.id };
10981
+ }
10982
+ const handoverHeader = targetedRemaining !== null ? `# Targeted Session Complete -- All ${ctx.state.targetWork.length} target(s) done` : `# Session Complete -- ${ticketsDone} ticket(s) and ${issuesDone} issue(s) done`;
10983
+ return {
10984
+ action: "goto",
10985
+ target: "HANDOVER",
10986
+ result: {
10987
+ instruction: [
10988
+ handoverHeader,
10989
+ "",
10990
+ "Write a session handover summarizing what was accomplished, decisions made, and what's next.",
10991
+ "",
10992
+ 'Call me with completedAction: "handover_written" and include the content in handoverContent.'
10993
+ ].join("\n"),
10994
+ reminders: [],
10995
+ transitionedFrom: "COMPLETE",
10996
+ contextAdvice: "ok"
10675
10997
  }
10676
- const handoverHeader = targetedRemaining !== null ? `# Targeted Session Complete -- All ${ctx.state.targetWork.length} target(s) done` : `# Session Complete \u2014 ${ticketsDone} ticket(s) and ${issuesDone} issue(s) done`;
10998
+ };
10999
+ }
11000
+ // ---------------------------------------------------------------------------
11001
+ // Targeted PICK_TICKET instruction
11002
+ // ---------------------------------------------------------------------------
11003
+ buildTargetedPickResult(ctx, targetedRemaining, projectState) {
11004
+ const { text: candidatesText, firstReady } = buildTargetedCandidatesText(targetedRemaining, projectState);
11005
+ if (!firstReady) {
10677
11006
  return {
10678
11007
  action: "goto",
10679
11008
  target: "HANDOVER",
10680
11009
  result: {
10681
- instruction: [
10682
- handoverHeader,
10683
- "",
10684
- "Write a session handover summarizing what was accomplished, decisions made, and what's next.",
10685
- "",
10686
- 'Call me with completedAction: "handover_written" and include the content in handoverContent.'
10687
- ].join("\n"),
11010
+ instruction: buildTargetedStuckHandover(candidatesText, ctx.state.sessionId),
10688
11011
  reminders: [],
10689
- transitionedFrom: "COMPLETE",
10690
- contextAdvice: "ok"
10691
- }
10692
- };
10693
- }
10694
- if (targetedRemaining !== null) {
10695
- const { text: candidatesText2, firstReady } = buildTargetedCandidatesText(targetedRemaining, projectState);
10696
- if (!firstReady) {
10697
- return {
10698
- action: "goto",
10699
- target: "HANDOVER",
10700
- result: {
10701
- instruction: buildTargetedStuckHandover(candidatesText2, ctx.state.sessionId),
10702
- reminders: [],
10703
- transitionedFrom: "COMPLETE"
10704
- }
10705
- };
10706
- }
10707
- const precomputed = { text: candidatesText2, firstReady };
10708
- const targetedInstruction = buildTargetedPickInstruction(targetedRemaining, projectState, ctx.state.sessionId, precomputed);
10709
- return {
10710
- action: "goto",
10711
- target: "PICK_TICKET",
10712
- result: {
10713
- instruction: [
10714
- `# Item Complete -- Continuing (${ctx.state.targetWork.length - targetedRemaining.length}/${ctx.state.targetWork.length} targets done)`,
10715
- "",
10716
- "Do NOT stop. Do NOT ask the user. Continue immediately with the next target.",
10717
- "",
10718
- targetedInstruction
10719
- ].join("\n"),
10720
- reminders: [
10721
- "Do NOT stop or summarize. Call autonomous_guide IMMEDIATELY to pick the next target.",
10722
- "Do NOT ask the user for confirmation.",
10723
- "You are in targeted auto mode -- pick ONLY from the listed items."
10724
- ],
10725
- transitionedFrom: "COMPLETE",
10726
- contextAdvice: "ok"
11012
+ transitionedFrom: "COMPLETE"
10727
11013
  }
10728
11014
  };
10729
11015
  }
11016
+ const precomputed = { text: candidatesText, firstReady };
11017
+ const targetedInstruction = buildTargetedPickInstruction(targetedRemaining, projectState, ctx.state.sessionId, precomputed);
11018
+ return {
11019
+ action: "goto",
11020
+ target: "PICK_TICKET",
11021
+ result: {
11022
+ instruction: [
11023
+ `# Item Complete -- Continuing (${ctx.state.targetWork.length - targetedRemaining.length}/${ctx.state.targetWork.length} targets done)`,
11024
+ "",
11025
+ "Do NOT stop. Do NOT ask the user. Continue immediately with the next target.",
11026
+ "",
11027
+ targetedInstruction
11028
+ ].join("\n"),
11029
+ reminders: [
11030
+ "Do NOT stop or summarize. Call autonomous_guide IMMEDIATELY to pick the next target.",
11031
+ "Do NOT ask the user for confirmation.",
11032
+ "You are in targeted auto mode -- pick ONLY from the listed items."
11033
+ ],
11034
+ transitionedFrom: "COMPLETE",
11035
+ contextAdvice: "ok"
11036
+ }
11037
+ };
11038
+ }
11039
+ // ---------------------------------------------------------------------------
11040
+ // Standard auto PICK_TICKET instruction
11041
+ // ---------------------------------------------------------------------------
11042
+ buildStandardPickResult(ctx, projectState, ticketsDone, maxTickets) {
10730
11043
  const candidates = nextTickets(projectState, 5);
10731
11044
  let candidatesText = "";
10732
11045
  if (candidates.kind === "found") {
@@ -10740,7 +11053,7 @@ var init_complete = __esm({
10740
11053
  target: "PICK_TICKET",
10741
11054
  result: {
10742
11055
  instruction: [
10743
- `# Ticket Complete \u2014 Continuing (${ticketsDone}/${maxTickets})`,
11056
+ `# Ticket Complete -- Continuing (${ticketsDone}/${maxTickets})`,
10744
11057
  "",
10745
11058
  "Do NOT stop. Do NOT ask the user. Continue immediately with the next ticket.",
10746
11059
  "",
@@ -10754,16 +11067,13 @@ var init_complete = __esm({
10754
11067
  reminders: [
10755
11068
  "Do NOT stop or summarize. Call autonomous_guide IMMEDIATELY to pick the next ticket.",
10756
11069
  "Do NOT ask the user for confirmation.",
10757
- "You are in autonomous mode \u2014 continue working until all tickets are done or the session limit is reached."
11070
+ "You are in autonomous mode -- continue working until all tickets are done or the session limit is reached."
10758
11071
  ],
10759
11072
  transitionedFrom: "COMPLETE",
10760
11073
  contextAdvice: "ok"
10761
11074
  }
10762
11075
  };
10763
11076
  }
10764
- async report(ctx, _report) {
10765
- return this.enter(ctx);
10766
- }
10767
11077
  };
10768
11078
  }
10769
11079
  });
@@ -10891,7 +11201,32 @@ var init_issue_fix = __esm({
10891
11201
  if (!issue) {
10892
11202
  return { action: "goto", target: "PICK_TICKET" };
10893
11203
  }
10894
- const { state: projectState } = await ctx.loadProject();
11204
+ let projectState;
11205
+ try {
11206
+ ({ state: projectState } = await ctx.loadProject());
11207
+ } catch {
11208
+ return {
11209
+ instruction: [
11210
+ "# Fix Issue",
11211
+ "",
11212
+ `**${issue.id}**: ${issue.title} (severity: ${issue.severity})`,
11213
+ "",
11214
+ "(Warning: could not load full issue details from .story/ -- using session state.)",
11215
+ "",
11216
+ 'Fix this issue, then update its status to "resolved" in `.story/issues/`.',
11217
+ "Add a resolution description explaining the fix.",
11218
+ "",
11219
+ "When done, call `claudestory_autonomous_guide` with:",
11220
+ "```json",
11221
+ `{ "sessionId": "${ctx.state.sessionId}", "action": "report", "report": { "completedAction": "issue_fixed" } }`,
11222
+ "```"
11223
+ ].join("\n"),
11224
+ reminders: [
11225
+ 'Update the issue JSON: set status to "resolved", add resolution text, set resolvedDate.',
11226
+ "Do NOT ask the user for confirmation."
11227
+ ]
11228
+ };
11229
+ }
10895
11230
  const fullIssue = projectState.issues.find((i) => i.id === issue.id);
10896
11231
  const details = fullIssue ? [
10897
11232
  `**${fullIssue.id}**: ${fullIssue.title}`,
@@ -10926,7 +11261,12 @@ var init_issue_fix = __esm({
10926
11261
  if (!issue) {
10927
11262
  return { action: "goto", target: "PICK_TICKET" };
10928
11263
  }
10929
- const { state: projectState } = await ctx.loadProject();
11264
+ let projectState;
11265
+ try {
11266
+ ({ state: projectState } = await ctx.loadProject());
11267
+ } catch (err) {
11268
+ return { action: "retry", instruction: `Failed to load project state: ${err instanceof Error ? err.message : String(err)}. Check .story/ files for corruption, then report again.` };
11269
+ }
10930
11270
  const current = projectState.issues.find((i) => i.id === issue.id);
10931
11271
  if (!current || current.status !== "resolved") {
10932
11272
  return {
@@ -10935,6 +11275,10 @@ var init_issue_fix = __esm({
10935
11275
  reminders: ["Set status to 'resolved', add resolution text, set resolvedDate."]
10936
11276
  };
10937
11277
  }
11278
+ const enableCodeReview = !!ctx.recipe.stages.ISSUE_FIX?.enableCodeReview;
11279
+ if (enableCodeReview) {
11280
+ return { action: "goto", target: "CODE_REVIEW" };
11281
+ }
10938
11282
  return {
10939
11283
  action: "goto",
10940
11284
  target: "FINALIZE",
@@ -10944,9 +11288,10 @@ var init_issue_fix = __esm({
10944
11288
  "",
10945
11289
  `Issue ${issue.id} resolved. Time to commit.`,
10946
11290
  "",
10947
- `1. Ensure .story/issues/${issue.id}.json is updated with status: "resolved"`,
10948
- "2. Stage only the files you modified for this fix (code + .story/ changes). Do NOT use `git add -A` or `git add .`",
10949
- '3. Call me with completedAction: "files_staged"'
11291
+ "1. Run `git reset` to clear the staging area (ensures no stale files from prior operations)",
11292
+ `2. Ensure .story/issues/${issue.id}.json is updated with status: "resolved"`,
11293
+ "3. Stage only the files you modified for this fix (code + .story/ changes). Do NOT use `git add -A` or `git add .`",
11294
+ '4. Call me with completedAction: "files_staged"'
10950
11295
  ].join("\n"),
10951
11296
  reminders: ["Stage both code changes and .story/ issue update in the same commit. Only stage files related to this fix."],
10952
11297
  transitionedFrom: "ISSUE_FIX"
@@ -10970,7 +11315,12 @@ var init_issue_sweep = __esm({
10970
11315
  return !issueConfig?.enabled;
10971
11316
  }
10972
11317
  async enter(ctx) {
10973
- const { state: projectState } = await ctx.loadProject();
11318
+ let projectState;
11319
+ try {
11320
+ ({ state: projectState } = await ctx.loadProject());
11321
+ } catch {
11322
+ return { action: "goto", target: "HANDOVER" };
11323
+ }
10974
11324
  const allIssues = projectState.issues.filter((i) => i.status === "open");
10975
11325
  if (allIssues.length === 0) {
10976
11326
  return { action: "goto", target: "HANDOVER" };
@@ -11022,7 +11372,12 @@ var init_issue_sweep = __esm({
11022
11372
  }
11023
11373
  const current = sweep.current;
11024
11374
  if (current) {
11025
- const { state: verifyState } = await ctx.loadProject();
11375
+ let verifyState;
11376
+ try {
11377
+ ({ state: verifyState } = await ctx.loadProject());
11378
+ } catch (err) {
11379
+ return { action: "retry", instruction: `Failed to load project state: ${err instanceof Error ? err.message : String(err)}. Check .story/ files, then report again.` };
11380
+ }
11026
11381
  const currentIssue = verifyState.issues.find((i) => i.id === current);
11027
11382
  if (currentIssue && currentIssue.status === "open") {
11028
11383
  return {
@@ -11041,7 +11396,16 @@ var init_issue_sweep = __esm({
11041
11396
  ctx.appendEvent("issue_sweep_complete", { resolved: resolved.length });
11042
11397
  return { action: "goto", target: "HANDOVER" };
11043
11398
  }
11044
- const { state: projectState } = await ctx.loadProject();
11399
+ let projectState;
11400
+ try {
11401
+ ({ state: projectState } = await ctx.loadProject());
11402
+ } catch {
11403
+ return {
11404
+ action: "retry",
11405
+ instruction: `Issue ${next} is next. Fix it and report again. (Could not load full details from .story/.)`,
11406
+ reminders: ["Set status to 'resolved' and add a resolution description."]
11407
+ };
11408
+ }
11045
11409
  const nextIssue = projectState.issues.find((i) => i.id === next);
11046
11410
  return {
11047
11411
  action: "retry",
@@ -11064,204 +11428,511 @@ Impact: ${nextIssue.impact}` : ""}` : `Fix issue ${next}.`,
11064
11428
  }
11065
11429
  });
11066
11430
 
11067
- // src/autonomous/stages/handover.ts
11068
- import { writeFileSync as writeFileSync4 } from "fs";
11431
+ // src/autonomous/resume-marker.ts
11432
+ import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync4, unlinkSync as unlinkSync3, existsSync as existsSync11 } from "fs";
11069
11433
  import { join as join17 } from "path";
11070
- var HandoverStage;
11071
- var init_handover2 = __esm({
11072
- "src/autonomous/stages/handover.ts"() {
11073
- "use strict";
11074
- init_esm_shims();
11075
- init_handover();
11076
- init_git_inspector();
11077
- HandoverStage = class {
11078
- id = "HANDOVER";
11079
- async enter(ctx) {
11080
- const ticketsDone = ctx.state.completedTickets.length;
11081
- const issuesDone = (ctx.state.resolvedIssues ?? []).length;
11082
- return {
11083
- instruction: [
11084
- `# Session Complete \u2014 ${ticketsDone} ticket(s) and ${issuesDone} issue(s) done`,
11085
- "",
11086
- "Write a session handover summarizing what was accomplished, decisions made, and what's next.",
11087
- "",
11088
- 'Call me with completedAction: "handover_written" and include the content in handoverContent.'
11089
- ].join("\n"),
11090
- reminders: [
11091
- "Before recording a new lesson, call claudestory_lesson_list to check existing lessons. Then choose: create (new insight), reinforce (existing lesson confirmed), update (refine wording), or skip."
11092
- ],
11093
- transitionedFrom: ctx.state.previousState ?? void 0
11094
- };
11095
- }
11096
- async report(ctx, report) {
11097
- const content = report.handoverContent;
11098
- if (!content) {
11099
- return { action: "retry", instruction: "Missing handoverContent. Write the handover and include it in the report." };
11100
- }
11101
- let handoverFailed = false;
11102
- try {
11103
- await handleHandoverCreate(content, "auto-session", "md", ctx.root);
11104
- } catch {
11105
- handoverFailed = true;
11106
- try {
11107
- const fallbackPath = join17(ctx.dir, "handover-fallback.md");
11108
- writeFileSync4(fallbackPath, content, "utf-8");
11109
- } catch {
11110
- }
11111
- }
11112
- let stashPopFailed = false;
11113
- const autoStash = ctx.state.git.autoStash;
11114
- if (autoStash) {
11115
- const popResult = await gitStashPop(ctx.root, autoStash.ref);
11116
- if (!popResult.ok) {
11117
- stashPopFailed = true;
11118
- }
11119
- }
11120
- await ctx.drainDeferrals();
11121
- const hasUnfiled = (ctx.state.pendingDeferrals ?? []).length > 0;
11122
- ctx.writeState({
11123
- state: "SESSION_END",
11124
- previousState: "HANDOVER",
11125
- status: "completed",
11126
- terminationReason: "normal",
11127
- deferralsUnfiled: hasUnfiled
11128
- });
11129
- ctx.appendEvent("session_end", {
11130
- ticketsCompleted: ctx.state.completedTickets.length,
11131
- issuesResolved: (ctx.state.resolvedIssues ?? []).length,
11132
- handoverFailed
11133
- });
11134
- const ticketsDone = ctx.state.completedTickets.length;
11135
- const issuesDone = (ctx.state.resolvedIssues ?? []).length;
11136
- const resolvedList = (ctx.state.resolvedIssues ?? []).map((id) => `- ${id} (resolved)`).join("\n");
11137
- return {
11138
- action: "advance",
11139
- result: {
11140
- instruction: [
11141
- "# Session Complete",
11142
- "",
11143
- `${ticketsDone} ticket(s) and ${issuesDone} issue(s) completed.${handoverFailed ? " Handover creation failed \u2014 fallback saved to session directory." : " Handover written."}${stashPopFailed ? " Auto-stash pop failed \u2014 run `git stash pop` manually." : ""} Session ended.`,
11144
- "",
11145
- ctx.state.completedTickets.map((t) => `- ${t.id}${t.title ? `: ${t.title}` : ""} (${t.commitHash ?? "no commit"})`).join("\n"),
11146
- ...resolvedList ? [resolvedList] : []
11147
- ].join("\n"),
11148
- reminders: [],
11149
- transitionedFrom: "HANDOVER"
11150
- }
11151
- };
11152
- }
11153
- };
11154
- }
11155
- });
11156
-
11157
- // src/autonomous/stages/index.ts
11158
- var init_stages = __esm({
11159
- "src/autonomous/stages/index.ts"() {
11160
- "use strict";
11161
- init_esm_shims();
11162
- init_registry();
11163
- init_pick_ticket();
11164
- init_plan();
11165
- init_plan_review();
11166
- init_implement();
11167
- init_write_tests();
11168
- init_test();
11169
- init_code_review();
11170
- init_build();
11171
- init_verify();
11172
- init_finalize();
11173
- init_complete();
11174
- init_lesson_capture();
11175
- init_issue_fix();
11176
- init_issue_sweep();
11177
- init_handover2();
11178
- registerStage(new PickTicketStage());
11179
- registerStage(new PlanStage());
11180
- registerStage(new PlanReviewStage());
11181
- registerStage(new ImplementStage());
11182
- registerStage(new WriteTestsStage());
11183
- registerStage(new TestStage());
11184
- registerStage(new CodeReviewStage());
11185
- registerStage(new BuildStage());
11186
- registerStage(new VerifyStage());
11187
- registerStage(new FinalizeStage());
11188
- registerStage(new CompleteStage());
11189
- registerStage(new LessonCaptureStage());
11190
- registerStage(new IssueFixStage());
11191
- registerStage(new IssueSweepStage());
11192
- registerStage(new HandoverStage());
11193
- }
11194
- });
11195
-
11196
- // src/autonomous/version-check.ts
11197
- import { readFileSync as readFileSync9 } from "fs";
11198
- import { join as join18, dirname as dirname4 } from "path";
11199
- import { fileURLToPath as fileURLToPath3 } from "url";
11200
- function checkVersionMismatch(runningVersion, installedVersion) {
11201
- if (!installedVersion) return null;
11202
- if (runningVersion === "0.0.0-dev") return null;
11203
- if (runningVersion === installedVersion) return null;
11204
- return `claudestory MCP server is running v${runningVersion} but v${installedVersion} is installed. Restart Claude Code to load the updated version.`;
11434
+ function sanitize(input, maxLen = 120) {
11435
+ return input.replace(/[\r\n]+/g, " ").replace(/\s+/g, " ").trim().slice(0, maxLen);
11205
11436
  }
11206
- function getInstalledVersion() {
11437
+ function writeResumeMarker(root, sessionId, state) {
11207
11438
  try {
11208
- const thisFile = fileURLToPath3(import.meta.url);
11209
- const candidates = [
11210
- join18(dirname4(thisFile), "..", "..", "package.json"),
11211
- join18(dirname4(thisFile), "..", "package.json")
11439
+ const rulesDir = join17(root, ".claude", "rules");
11440
+ mkdirSync4(rulesDir, { recursive: true });
11441
+ const ticketInfo = state.ticket ? `Working on: ${sanitize(state.ticket.id, 20)} (${sanitize(state.ticket.title)})` : "Between tickets";
11442
+ const progress = `Progress: ${state.completedTickets.length} tickets completed, ${(state.resolvedIssues ?? []).length} issues resolved`;
11443
+ const lines = [
11444
+ "CRITICAL: An autonomous coding session is active and waiting to resume.",
11445
+ "",
11446
+ `Session: ${sessionId}`,
11447
+ ticketInfo,
11448
+ progress
11212
11449
  ];
11213
- for (const candidate of candidates) {
11214
- try {
11215
- const raw = readFileSync9(candidate, "utf-8");
11216
- const pkg = JSON.parse(raw);
11217
- if (pkg.version) return pkg.version;
11218
- } catch {
11219
- }
11450
+ if (state.preCompactState) {
11451
+ lines.push(`State before compaction: ${state.preCompactState}`);
11220
11452
  }
11221
- return null;
11453
+ lines.push(
11454
+ "",
11455
+ "You MUST call `claudestory_autonomous_guide` with:",
11456
+ `{"sessionId": "${sessionId}", "action": "resume"}`,
11457
+ "",
11458
+ "Do NOT do any other work until you have called this.",
11459
+ "Do NOT manually create tickets, issues, or handovers.",
11460
+ "The guide manages your workflow."
11461
+ );
11462
+ const content = lines.join("\n") + "\n";
11463
+ writeFileSync4(join17(rulesDir, MARKER_FILENAME), content, "utf-8");
11222
11464
  } catch {
11223
- return null;
11224
11465
  }
11225
11466
  }
11226
- function getRunningVersion() {
11227
- return "0.1.61";
11467
+ function removeResumeMarker(root) {
11468
+ try {
11469
+ const markerPath = join17(root, ".claude", "rules", MARKER_FILENAME);
11470
+ if (existsSync11(markerPath)) unlinkSync3(markerPath);
11471
+ } catch {
11472
+ }
11228
11473
  }
11229
- var init_version_check = __esm({
11230
- "src/autonomous/version-check.ts"() {
11474
+ var MARKER_FILENAME;
11475
+ var init_resume_marker = __esm({
11476
+ "src/autonomous/resume-marker.ts"() {
11231
11477
  "use strict";
11232
11478
  init_esm_shims();
11479
+ MARKER_FILENAME = "autonomous-resume.md";
11233
11480
  }
11234
11481
  });
11235
11482
 
11236
- // src/autonomous/guide.ts
11237
- import { readFileSync as readFileSync10, writeFileSync as writeFileSync5, readdirSync as readdirSync4 } from "fs";
11238
- import { join as join19 } from "path";
11239
- function buildGuideRecommendOptions(root) {
11240
- const opts = {};
11241
- try {
11242
- const handoversDir = join19(root, ".story", "handovers");
11243
- const files = readdirSync4(handoversDir, "utf-8").filter((f) => f.endsWith(".md")).sort();
11244
- if (files.length > 0) {
11245
- opts.latestHandoverContent = readFileSync10(join19(handoversDir, files[files.length - 1]), "utf-8");
11246
- }
11247
- } catch {
11248
- }
11249
- try {
11250
- const snapshotsDir = join19(root, ".story", "snapshots");
11251
- const snapFiles = readdirSync4(snapshotsDir, "utf-8").filter((f) => f.endsWith(".json")).sort();
11252
- if (snapFiles.length > 0) {
11253
- const raw = readFileSync10(join19(snapshotsDir, snapFiles[snapFiles.length - 1]), "utf-8");
11254
- const snap = JSON.parse(raw);
11255
- if (snap.issues) {
11256
- opts.previousOpenIssueCount = snap.issues.filter((i) => i.status !== "resolved").length;
11483
+ // src/core/session-report-formatter.ts
11484
+ function formatSessionReport(data, format) {
11485
+ const { state, events, planContent, gitLog } = data;
11486
+ if (format === "json") {
11487
+ return JSON.stringify({
11488
+ ok: true,
11489
+ data: {
11490
+ summary: buildSummaryData(state),
11491
+ ticketProgression: state.completedTickets,
11492
+ reviewStats: state.reviews,
11493
+ events: events.events.slice(-50),
11494
+ malformedEventCount: events.malformedCount,
11495
+ contextPressure: state.contextPressure,
11496
+ git: {
11497
+ branch: state.git.branch,
11498
+ initHead: state.git.initHead,
11499
+ commits: gitLog
11500
+ },
11501
+ problems: buildProblems(state, events)
11257
11502
  }
11258
- }
11259
- } catch {
11503
+ }, null, 2);
11260
11504
  }
11261
- return opts;
11262
- }
11263
- async function buildTargetedResumeResult(root, state, dir) {
11264
- const remaining = getRemainingTargets(state);
11505
+ const sections = [];
11506
+ sections.push(buildSummarySection(state));
11507
+ sections.push(buildTicketSection(state));
11508
+ sections.push(buildReviewSection(state));
11509
+ sections.push(buildEventSection(events));
11510
+ sections.push(buildPressureSection(state));
11511
+ sections.push(buildGitSection(state, gitLog));
11512
+ sections.push(buildProblemsSection(state, events));
11513
+ return sections.join("\n\n---\n\n");
11514
+ }
11515
+ function buildSummaryData(state) {
11516
+ return {
11517
+ sessionId: state.sessionId,
11518
+ mode: state.mode ?? "auto",
11519
+ recipe: state.recipe,
11520
+ status: state.status,
11521
+ terminationReason: state.terminationReason,
11522
+ startedAt: state.startedAt,
11523
+ lastGuideCall: state.lastGuideCall,
11524
+ guideCallCount: state.guideCallCount,
11525
+ ticketsCompleted: state.completedTickets.length
11526
+ };
11527
+ }
11528
+ function buildSummarySection(state) {
11529
+ const duration = state.startedAt && state.lastGuideCall ? formatDuration(state.startedAt, state.lastGuideCall) : "unknown";
11530
+ return [
11531
+ "## Session Summary",
11532
+ "",
11533
+ `- **ID:** ${state.sessionId}`,
11534
+ `- **Mode:** ${state.mode ?? "auto"}`,
11535
+ `- **Recipe:** ${state.recipe}`,
11536
+ `- **Status:** ${state.status}${state.terminationReason ? ` (${state.terminationReason})` : ""}`,
11537
+ `- **Duration:** ${duration}`,
11538
+ `- **Guide calls:** ${state.guideCallCount}`,
11539
+ `- **Tickets completed:** ${state.completedTickets.length}`
11540
+ ].join("\n");
11541
+ }
11542
+ function buildTicketSection(state) {
11543
+ if (state.completedTickets.length === 0) {
11544
+ const current = state.ticket;
11545
+ if (current) {
11546
+ return [
11547
+ "## Ticket Progression",
11548
+ "",
11549
+ `In progress: **${current.id}** \u2014 ${current.title} (risk: ${current.risk ?? "unknown"})`
11550
+ ].join("\n");
11551
+ }
11552
+ return "## Ticket Progression\n\nNo tickets completed.";
11553
+ }
11554
+ const lines = ["## Ticket Progression", ""];
11555
+ for (const t of state.completedTickets) {
11556
+ const risk = t.realizedRisk ? `${t.risk ?? "?"} \u2192 ${t.realizedRisk}` : t.risk ?? "unknown";
11557
+ const duration = t.startedAt && t.completedAt ? formatDuration(t.startedAt, t.completedAt) : null;
11558
+ const durationPart = duration ? ` | duration: ${duration}` : "";
11559
+ lines.push(`- **${t.id}:** ${t.title} | risk: ${risk}${durationPart} | commit: \`${t.commitHash ?? "?"}\``);
11560
+ }
11561
+ return lines.join("\n");
11562
+ }
11563
+ function buildReviewSection(state) {
11564
+ const plan = state.reviews.plan;
11565
+ const code = state.reviews.code;
11566
+ if (plan.length === 0 && code.length === 0) {
11567
+ return "## Review Stats\n\nNo reviews recorded.";
11568
+ }
11569
+ const lines = ["## Review Stats", ""];
11570
+ if (plan.length > 0) {
11571
+ lines.push(`**Plan reviews:** ${plan.length} round(s)`);
11572
+ for (const r of plan) {
11573
+ lines.push(` - Round ${r.round}: ${r.verdict} (${r.findingCount} findings, ${r.criticalCount} critical, ${r.majorCount} major) \u2014 ${r.reviewer}`);
11574
+ }
11575
+ }
11576
+ if (code.length > 0) {
11577
+ lines.push(`**Code reviews:** ${code.length} round(s)`);
11578
+ for (const r of code) {
11579
+ lines.push(` - Round ${r.round}: ${r.verdict} (${r.findingCount} findings, ${r.criticalCount} critical, ${r.majorCount} major) \u2014 ${r.reviewer}`);
11580
+ }
11581
+ }
11582
+ const totalFindings = [...plan, ...code].reduce((sum, r) => sum + r.findingCount, 0);
11583
+ lines.push("", `**Total findings:** ${totalFindings}`);
11584
+ return lines.join("\n");
11585
+ }
11586
+ function buildEventSection(events) {
11587
+ if (events.events.length === 0 && events.malformedCount === 0) {
11588
+ return "## Event Timeline\n\nNot available.";
11589
+ }
11590
+ const capped = events.events.slice(-50);
11591
+ const omitted = events.events.length - capped.length;
11592
+ const lines = ["## Event Timeline", ""];
11593
+ if (omitted > 0) {
11594
+ lines.push(`*${omitted} earlier events omitted*`, "");
11595
+ }
11596
+ for (const e of capped) {
11597
+ const ts = e.timestamp ? e.timestamp.slice(11, 19) : "??:??:??";
11598
+ const detail = e.data ? Object.entries(e.data).map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(" ") : "";
11599
+ lines.push(`- \`${ts}\` [${e.type}] ${detail}`.trimEnd());
11600
+ }
11601
+ if (events.malformedCount > 0) {
11602
+ lines.push("", `*${events.malformedCount} malformed event line(s) skipped*`);
11603
+ }
11604
+ return lines.join("\n");
11605
+ }
11606
+ function buildPressureSection(state) {
11607
+ const p = state.contextPressure;
11608
+ return [
11609
+ "## Context Pressure",
11610
+ "",
11611
+ `- **Level:** ${p.level}`,
11612
+ `- **Guide calls:** ${p.guideCallCount}`,
11613
+ `- **Tickets completed:** ${p.ticketsCompleted}`,
11614
+ `- **Compactions:** ${p.compactionCount}`,
11615
+ `- **Events log:** ${p.eventsLogBytes} bytes`
11616
+ ].join("\n");
11617
+ }
11618
+ function buildGitSection(state, gitLog) {
11619
+ const lines = [
11620
+ "## Git Summary",
11621
+ "",
11622
+ `- **Branch:** ${state.git.branch ?? "unknown"}`,
11623
+ `- **Init HEAD:** \`${state.git.initHead ?? "?"}\``,
11624
+ `- **Expected HEAD:** \`${state.git.expectedHead ?? "?"}\``
11625
+ ];
11626
+ if (gitLog && gitLog.length > 0) {
11627
+ lines.push("", "**Commits:**");
11628
+ for (const c of gitLog) {
11629
+ lines.push(`- ${c}`);
11630
+ }
11631
+ } else {
11632
+ lines.push("", "Commits: Not available.");
11633
+ }
11634
+ return lines.join("\n");
11635
+ }
11636
+ function buildProblems(state, events) {
11637
+ const problems = [];
11638
+ if (state.terminationReason && state.terminationReason !== "normal") {
11639
+ problems.push(`Abnormal termination: ${state.terminationReason}`);
11640
+ }
11641
+ if (events.malformedCount > 0) {
11642
+ problems.push(`${events.malformedCount} malformed event line(s) in events.log`);
11643
+ }
11644
+ for (const e of events.events) {
11645
+ if (e.type.includes("error") || e.type.includes("exhaustion")) {
11646
+ problems.push(`[${e.type}] ${e.timestamp ?? ""} ${JSON.stringify(e.data)}`);
11647
+ } else if (e.data?.result === "exhaustion") {
11648
+ problems.push(`[${e.type}] exhaustion at ${e.timestamp ?? ""}`);
11649
+ }
11650
+ }
11651
+ if (state.deferralsUnfiled) {
11652
+ problems.push("Session has unfiled deferrals");
11653
+ }
11654
+ return problems;
11655
+ }
11656
+ function buildProblemsSection(state, events) {
11657
+ const problems = buildProblems(state, events);
11658
+ if (problems.length === 0) {
11659
+ return "## Problems\n\nNone detected.";
11660
+ }
11661
+ return ["## Problems", "", ...problems.map((p) => `- ${p}`)].join("\n");
11662
+ }
11663
+ function formatCompactReport(data) {
11664
+ const { state, remainingWork } = data;
11665
+ const endTime = data.endedAt ?? state.lastGuideCall ?? (/* @__PURE__ */ new Date()).toISOString();
11666
+ const duration = state.startedAt ? formatDuration(state.startedAt, endTime) : "unknown";
11667
+ const ticketCount = state.completedTickets.length;
11668
+ const issueCount = (state.resolvedIssues ?? []).length;
11669
+ const reviewRounds = state.reviews.plan.length + state.reviews.code.length;
11670
+ const totalFindings = [...state.reviews.plan, ...state.reviews.code].reduce((s, r) => s + r.findingCount, 0);
11671
+ const compactions = state.contextPressure?.compactionCount ?? 0;
11672
+ const lines = [
11673
+ "## Session Report",
11674
+ "",
11675
+ `**Duration:** ${duration} | **Tickets:** ${ticketCount} | **Issues:** ${issueCount} | **Reviews:** ${reviewRounds} rounds (${totalFindings} findings) | **Compactions:** ${compactions}`
11676
+ ];
11677
+ if (ticketCount > 0) {
11678
+ lines.push("", "### Completed", "| Ticket | Title | Duration |", "|--------|-------|----------|");
11679
+ for (const t of state.completedTickets) {
11680
+ const ticketDuration = t.startedAt && t.completedAt ? formatDuration(t.startedAt, t.completedAt) : "--";
11681
+ const safeTitle = (t.title ?? "").replace(/\|/g, "\\|");
11682
+ lines.push(`| ${t.id} | ${safeTitle} | ${ticketDuration} |`);
11683
+ }
11684
+ const timings = state.completedTickets.filter((t) => t.startedAt && t.completedAt).map((t) => new Date(t.completedAt).getTime() - new Date(t.startedAt).getTime());
11685
+ if (timings.length > 0) {
11686
+ const avgMs = timings.reduce((a, b) => a + b, 0) / timings.length;
11687
+ const avgMins = Math.round(avgMs / 6e4);
11688
+ lines.push("", `**Avg time per ticket:** ${avgMins}m`);
11689
+ }
11690
+ }
11691
+ if (remainingWork && (remainingWork.tickets.length > 0 || remainingWork.issues.length > 0)) {
11692
+ lines.push("", "### What's Left");
11693
+ for (const t of remainingWork.tickets) {
11694
+ lines.push(`- ${t.id}: ${t.title} (unblocked)`);
11695
+ }
11696
+ for (const i of remainingWork.issues) {
11697
+ lines.push(`- ${i.id}: ${i.title} (${i.severity})`);
11698
+ }
11699
+ }
11700
+ return lines.join("\n");
11701
+ }
11702
+ function formatDuration(start, end) {
11703
+ try {
11704
+ const ms = new Date(end).getTime() - new Date(start).getTime();
11705
+ if (isNaN(ms) || ms < 0) return "unknown";
11706
+ const mins = Math.floor(ms / 6e4);
11707
+ if (mins < 60) return `${mins}m`;
11708
+ const hours = Math.floor(mins / 60);
11709
+ return `${hours}h ${mins % 60}m`;
11710
+ } catch {
11711
+ return "unknown";
11712
+ }
11713
+ }
11714
+ var init_session_report_formatter = __esm({
11715
+ "src/core/session-report-formatter.ts"() {
11716
+ "use strict";
11717
+ init_esm_shims();
11718
+ }
11719
+ });
11720
+
11721
+ // src/autonomous/stages/handover.ts
11722
+ import { writeFileSync as writeFileSync5 } from "fs";
11723
+ import { join as join18 } from "path";
11724
+ var HandoverStage;
11725
+ var init_handover2 = __esm({
11726
+ "src/autonomous/stages/handover.ts"() {
11727
+ "use strict";
11728
+ init_esm_shims();
11729
+ init_handover();
11730
+ init_git_inspector();
11731
+ init_resume_marker();
11732
+ init_session_report_formatter();
11733
+ init_project_loader();
11734
+ init_queries();
11735
+ HandoverStage = class {
11736
+ id = "HANDOVER";
11737
+ async enter(ctx) {
11738
+ const ticketsDone = ctx.state.completedTickets.length;
11739
+ const issuesDone = (ctx.state.resolvedIssues ?? []).length;
11740
+ return {
11741
+ instruction: [
11742
+ `# Session Complete \u2014 ${ticketsDone} ticket(s) and ${issuesDone} issue(s) done`,
11743
+ "",
11744
+ "Write a session handover summarizing what was accomplished, decisions made, and what's next.",
11745
+ "",
11746
+ 'Call me with completedAction: "handover_written" and include the content in handoverContent.'
11747
+ ].join("\n"),
11748
+ reminders: [
11749
+ "Before recording a new lesson, call claudestory_lesson_list to check existing lessons. Then choose: create (new insight), reinforce (existing lesson confirmed), update (refine wording), or skip."
11750
+ ],
11751
+ transitionedFrom: ctx.state.previousState ?? void 0
11752
+ };
11753
+ }
11754
+ async report(ctx, report) {
11755
+ const content = report.handoverContent;
11756
+ if (!content) {
11757
+ return { action: "retry", instruction: "Missing handoverContent. Write the handover and include it in the report." };
11758
+ }
11759
+ let handoverFailed = false;
11760
+ try {
11761
+ await handleHandoverCreate(content, "auto-session", "md", ctx.root);
11762
+ } catch {
11763
+ handoverFailed = true;
11764
+ try {
11765
+ const fallbackPath = join18(ctx.dir, "handover-fallback.md");
11766
+ writeFileSync5(fallbackPath, content, "utf-8");
11767
+ } catch {
11768
+ }
11769
+ }
11770
+ let stashPopFailed = false;
11771
+ const autoStash = ctx.state.git.autoStash;
11772
+ if (autoStash) {
11773
+ const popResult = await gitStashPop(ctx.root, autoStash.ref);
11774
+ if (!popResult.ok) {
11775
+ stashPopFailed = true;
11776
+ }
11777
+ }
11778
+ await ctx.drainDeferrals();
11779
+ const hasUnfiled = (ctx.state.pendingDeferrals ?? []).length > 0;
11780
+ ctx.writeState({
11781
+ state: "SESSION_END",
11782
+ previousState: "HANDOVER",
11783
+ status: "completed",
11784
+ terminationReason: "normal",
11785
+ deferralsUnfiled: hasUnfiled
11786
+ });
11787
+ ctx.appendEvent("session_end", {
11788
+ ticketsCompleted: ctx.state.completedTickets.length,
11789
+ issuesResolved: (ctx.state.resolvedIssues ?? []).length,
11790
+ handoverFailed
11791
+ });
11792
+ removeResumeMarker(ctx.root);
11793
+ let reportSection = "";
11794
+ try {
11795
+ const { state: projectState } = await loadProject(ctx.root);
11796
+ const nextResult = nextTickets(projectState, 5);
11797
+ const openIssues = projectState.issues.filter((i) => i.status === "open" || i.status === "inprogress").slice(0, 5);
11798
+ const remainingWork = {
11799
+ tickets: nextResult.kind === "found" ? nextResult.candidates.map((c) => ({ id: c.ticket.id, title: c.ticket.title })) : [],
11800
+ issues: openIssues.map((i) => ({ id: i.id, title: i.title, severity: i.severity }))
11801
+ };
11802
+ reportSection = "\n\n" + formatCompactReport({ state: ctx.state, endedAt: (/* @__PURE__ */ new Date()).toISOString(), remainingWork });
11803
+ } catch {
11804
+ }
11805
+ const ticketsDone = ctx.state.completedTickets.length;
11806
+ const issuesDone = (ctx.state.resolvedIssues ?? []).length;
11807
+ const resolvedList = (ctx.state.resolvedIssues ?? []).map((id) => `- ${id} (resolved)`).join("\n");
11808
+ return {
11809
+ action: "advance",
11810
+ result: {
11811
+ instruction: [
11812
+ "# Session Complete",
11813
+ "",
11814
+ `${ticketsDone} ticket(s) and ${issuesDone} issue(s) completed.${handoverFailed ? " Handover creation failed \u2014 fallback saved to session directory." : " Handover written."}${stashPopFailed ? " Auto-stash pop failed \u2014 run `git stash pop` manually." : ""} Session ended.`,
11815
+ "",
11816
+ ctx.state.completedTickets.map((t) => `- ${t.id}${t.title ? `: ${t.title}` : ""} (${t.commitHash ?? "no commit"})`).join("\n"),
11817
+ ...resolvedList ? [resolvedList] : []
11818
+ ].join("\n") + reportSection,
11819
+ reminders: [],
11820
+ transitionedFrom: "HANDOVER"
11821
+ }
11822
+ };
11823
+ }
11824
+ };
11825
+ }
11826
+ });
11827
+
11828
+ // src/autonomous/stages/index.ts
11829
+ var init_stages = __esm({
11830
+ "src/autonomous/stages/index.ts"() {
11831
+ "use strict";
11832
+ init_esm_shims();
11833
+ init_registry();
11834
+ init_pick_ticket();
11835
+ init_plan();
11836
+ init_plan_review();
11837
+ init_implement();
11838
+ init_write_tests();
11839
+ init_test();
11840
+ init_code_review();
11841
+ init_build();
11842
+ init_verify();
11843
+ init_finalize();
11844
+ init_complete();
11845
+ init_lesson_capture();
11846
+ init_issue_fix();
11847
+ init_issue_sweep();
11848
+ init_handover2();
11849
+ registerStage(new PickTicketStage());
11850
+ registerStage(new PlanStage());
11851
+ registerStage(new PlanReviewStage());
11852
+ registerStage(new ImplementStage());
11853
+ registerStage(new WriteTestsStage());
11854
+ registerStage(new TestStage());
11855
+ registerStage(new CodeReviewStage());
11856
+ registerStage(new BuildStage());
11857
+ registerStage(new VerifyStage());
11858
+ registerStage(new FinalizeStage());
11859
+ registerStage(new CompleteStage());
11860
+ registerStage(new LessonCaptureStage());
11861
+ registerStage(new IssueFixStage());
11862
+ registerStage(new IssueSweepStage());
11863
+ registerStage(new HandoverStage());
11864
+ }
11865
+ });
11866
+
11867
+ // src/autonomous/version-check.ts
11868
+ import { readFileSync as readFileSync9 } from "fs";
11869
+ import { join as join19, dirname as dirname4 } from "path";
11870
+ import { fileURLToPath as fileURLToPath3 } from "url";
11871
+ function checkVersionMismatch(runningVersion, installedVersion) {
11872
+ if (!installedVersion) return null;
11873
+ if (runningVersion === "0.0.0-dev") return null;
11874
+ if (runningVersion === installedVersion) return null;
11875
+ return `claudestory MCP server is running v${runningVersion} but v${installedVersion} is installed. Restart Claude Code to load the updated version.`;
11876
+ }
11877
+ function getInstalledVersion() {
11878
+ try {
11879
+ const thisFile = fileURLToPath3(import.meta.url);
11880
+ const candidates = [
11881
+ join19(dirname4(thisFile), "..", "..", "package.json"),
11882
+ join19(dirname4(thisFile), "..", "package.json")
11883
+ ];
11884
+ for (const candidate of candidates) {
11885
+ try {
11886
+ const raw = readFileSync9(candidate, "utf-8");
11887
+ const pkg = JSON.parse(raw);
11888
+ if (pkg.version) return pkg.version;
11889
+ } catch {
11890
+ }
11891
+ }
11892
+ return null;
11893
+ } catch {
11894
+ return null;
11895
+ }
11896
+ }
11897
+ function getRunningVersion() {
11898
+ return "0.1.63";
11899
+ }
11900
+ var init_version_check = __esm({
11901
+ "src/autonomous/version-check.ts"() {
11902
+ "use strict";
11903
+ init_esm_shims();
11904
+ }
11905
+ });
11906
+
11907
+ // src/autonomous/guide.ts
11908
+ import { readFileSync as readFileSync10, writeFileSync as writeFileSync6, readdirSync as readdirSync4 } from "fs";
11909
+ import { join as join20 } from "path";
11910
+ function buildGuideRecommendOptions(root) {
11911
+ const opts = {};
11912
+ try {
11913
+ const handoversDir = join20(root, ".story", "handovers");
11914
+ const files = readdirSync4(handoversDir, "utf-8").filter((f) => f.endsWith(".md")).sort();
11915
+ if (files.length > 0) {
11916
+ opts.latestHandoverContent = readFileSync10(join20(handoversDir, files[files.length - 1]), "utf-8");
11917
+ }
11918
+ } catch {
11919
+ }
11920
+ try {
11921
+ const snapshotsDir = join20(root, ".story", "snapshots");
11922
+ const snapFiles = readdirSync4(snapshotsDir, "utf-8").filter((f) => f.endsWith(".json")).sort();
11923
+ if (snapFiles.length > 0) {
11924
+ const raw = readFileSync10(join20(snapshotsDir, snapFiles[snapFiles.length - 1]), "utf-8");
11925
+ const snap = JSON.parse(raw);
11926
+ if (snap.issues) {
11927
+ opts.previousOpenIssueCount = snap.issues.filter((i) => i.status !== "resolved").length;
11928
+ }
11929
+ }
11930
+ } catch {
11931
+ }
11932
+ return opts;
11933
+ }
11934
+ async function buildTargetedResumeResult(root, state, dir) {
11935
+ const remaining = getRemainingTargets(state);
11265
11936
  if (remaining.length === 0) {
11266
11937
  return { instruction: "", stuck: false, allDone: true, candidatesText: "" };
11267
11938
  }
@@ -11328,6 +11999,33 @@ async function recoverPendingMutation(dir, state, root) {
11328
11999
  const mutation = state.pendingProjectMutation;
11329
12000
  if (!mutation || typeof mutation !== "object") return state;
11330
12001
  const m = mutation;
12002
+ if (m.type === "issue_update") {
12003
+ const targetId2 = m.target;
12004
+ const targetValue2 = m.value;
12005
+ const expectedCurrent2 = m.expectedCurrent;
12006
+ try {
12007
+ const { loadProject: loadProject2 } = await Promise.resolve().then(() => (init_project_loader(), project_loader_exports));
12008
+ const { state: projectState } = await loadProject2(root);
12009
+ const issue = projectState.issues.find((i) => i.id === targetId2);
12010
+ if (issue) {
12011
+ if (issue.status === targetValue2) {
12012
+ } else if (expectedCurrent2 && issue.status === expectedCurrent2) {
12013
+ const { handleIssueUpdate: handleIssueUpdate2 } = await Promise.resolve().then(() => (init_issue2(), issue_exports));
12014
+ await handleIssueUpdate2(targetId2, { status: targetValue2 }, "json", root);
12015
+ } else {
12016
+ appendEvent(dir, {
12017
+ rev: state.revision,
12018
+ type: "mutation_conflict",
12019
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
12020
+ data: { targetId: targetId2, expected: expectedCurrent2, actual: issue.status, transitionId: m.transitionId }
12021
+ });
12022
+ }
12023
+ }
12024
+ } catch {
12025
+ }
12026
+ const cleared2 = { ...state, pendingProjectMutation: null };
12027
+ return writeSessionSync(dir, cleared2);
12028
+ }
11331
12029
  if (m.type !== "ticket_update") return state;
11332
12030
  const targetId = m.target;
11333
12031
  const targetValue = m.value;
@@ -11597,6 +12295,7 @@ async function handleStart(root, args) {
11597
12295
  reviewBackends: sessionConfig.reviewBackends,
11598
12296
  stages: sessionConfig.stageOverrides
11599
12297
  });
12298
+ removeResumeMarker(root);
11600
12299
  const session = createSession(root, recipe, wsId, sessionConfig);
11601
12300
  const dir = sessionDir(root, session.sessionId);
11602
12301
  try {
@@ -11719,7 +12418,7 @@ Staged: ${stagedResult.data.join(", ")}`
11719
12418
  const passMatch = combined.match(/(\d+)\s*pass/i);
11720
12419
  const failMatch = combined.match(/(\d+)\s*fail/i);
11721
12420
  const passCount = passMatch ? parseInt(passMatch[1], 10) : -1;
11722
- const failCount = failMatch ? parseInt(failMatch[1], 10) : -1;
12421
+ const failCount = failMatch ? parseInt(failMatch[1], 10) : exitCode === 0 && passCount > 0 ? 0 : -1;
11723
12422
  const output = combined.slice(-500);
11724
12423
  updated = { ...updated, testBaseline: { exitCode, passCount, failCount, summary: output } };
11725
12424
  if (writeTestsEnabled && failCount < 0) {
@@ -11759,7 +12458,7 @@ Staged: ${stagedResult.data.join(", ")}`
11759
12458
  }
11760
12459
  }
11761
12460
  const { state: projectState, warnings } = await loadProject(root);
11762
- const handoversDir = join19(root, ".story", "handovers");
12461
+ const handoversDir = join20(root, ".story", "handovers");
11763
12462
  const ctx = { state: projectState, warnings, root, handoversDir, format: "md" };
11764
12463
  let handoverText = "";
11765
12464
  try {
@@ -11770,13 +12469,13 @@ Staged: ${stagedResult.data.join(", ")}`
11770
12469
  let recapText = "";
11771
12470
  try {
11772
12471
  const snapshotInfo = await loadLatestSnapshot(root);
11773
- const recap = buildRecap(projectState, snapshotInfo);
12472
+ const recap = await buildRecap(projectState, snapshotInfo, root);
11774
12473
  if (recap.changes) {
11775
12474
  recapText = "Changes since last snapshot available.";
11776
12475
  }
11777
12476
  } catch {
11778
12477
  }
11779
- const rulesText = readFileSafe2(join19(root, "RULES.md"));
12478
+ const rulesText = readFileSafe2(join20(root, "RULES.md"));
11780
12479
  const lessonDigest = buildLessonDigest(projectState.lessons);
11781
12480
  const digestParts = [
11782
12481
  handoverText ? `## Recent Handovers
@@ -11792,7 +12491,7 @@ ${rulesText}` : "",
11792
12491
  ].filter(Boolean);
11793
12492
  const digest = digestParts.join("\n\n---\n\n");
11794
12493
  try {
11795
- writeFileSync5(join19(dir, "context-digest.md"), digest, "utf-8");
12494
+ writeFileSync6(join20(dir, "context-digest.md"), digest, "utf-8");
11796
12495
  } catch {
11797
12496
  }
11798
12497
  if (mode !== "auto" && args.ticketId) {
@@ -12189,8 +12888,18 @@ async function handleResume(root, args) {
12189
12888
  `Cannot validate git state for session ${args.sessionId}. Check git status and try "resume" again, or run "claudestory session stop ${args.sessionId}" to end the session.`
12190
12889
  ));
12191
12890
  }
12891
+ let ownCommitDrift = false;
12192
12892
  if (expectedHead && headResult.data.hash !== expectedHead) {
12193
- const mapping = RECOVERY_MAPPING[resumeState] ?? { state: "PICK_TICKET", resetPlan: false, resetCode: false };
12893
+ const ancestorCheck = await gitIsAncestor(root, expectedHead, headResult.data.hash);
12894
+ if (ancestorCheck.ok && ancestorCheck.data) {
12895
+ ownCommitDrift = true;
12896
+ }
12897
+ }
12898
+ if (expectedHead && headResult.data.hash !== expectedHead && !ownCommitDrift) {
12899
+ let mapping = RECOVERY_MAPPING[resumeState] ?? { state: "PICK_TICKET", resetPlan: false, resetCode: false };
12900
+ if (info.state.currentIssue && resumeState === "CODE_REVIEW") {
12901
+ mapping = { state: "ISSUE_FIX", resetPlan: false, resetCode: true };
12902
+ }
12194
12903
  const recoveryReviews = {
12195
12904
  plan: mapping.resetPlan ? [] : info.state.reviews.plan,
12196
12905
  code: mapping.resetCode ? [] : info.state.reviews.code
@@ -12218,6 +12927,19 @@ async function handleResume(root, args) {
12218
12927
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
12219
12928
  data: { drift: true, previousState: resumeState, recoveryState: mapping.state, expectedHead, actualHead: headResult.data.hash, ticketId: info.state.ticket?.id }
12220
12929
  });
12930
+ appendEvent(info.dir, {
12931
+ rev: driftWritten.revision,
12932
+ type: "resumed",
12933
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
12934
+ data: {
12935
+ preCompactState: resumeState,
12936
+ compactionCount: driftWritten.contextPressure?.compactionCount ?? 0,
12937
+ ticketId: info.state.ticket?.id ?? null,
12938
+ headMatch: false,
12939
+ recoveryState: mapping.state
12940
+ }
12941
+ });
12942
+ removeResumeMarker(root);
12221
12943
  const driftPreamble = `**HEAD changed during compaction** (expected ${expectedHead.slice(0, 8)}, got ${headResult.data.hash.slice(0, 8)}). Review state invalidated.
12222
12944
 
12223
12945
  `;
@@ -12290,6 +13012,29 @@ async function handleResume(root, args) {
12290
13012
  reminders: ["Re-implement and verify before re-submitting for code review."]
12291
13013
  });
12292
13014
  }
13015
+ if (mapping.state === "ISSUE_FIX") {
13016
+ const issueFixStage = getStage("ISSUE_FIX");
13017
+ if (issueFixStage) {
13018
+ const recipe = resolveRecipeFromState(driftWritten);
13019
+ const ctx = new StageContext(root, info.dir, driftWritten, recipe);
13020
+ const enterResult = await issueFixStage.enter(ctx);
13021
+ if (isStageAdvance(enterResult)) {
13022
+ return processAdvance(ctx, issueFixStage, enterResult);
13023
+ }
13024
+ return guideResult(ctx.state, "ISSUE_FIX", {
13025
+ instruction: [
13026
+ "# Resumed After Compact \u2014 HEAD Mismatch",
13027
+ "",
13028
+ `${driftPreamble}Recovered to **ISSUE_FIX**. Re-fix the issue and mark resolved.`,
13029
+ "",
13030
+ "---",
13031
+ "",
13032
+ enterResult.instruction
13033
+ ].join("\n"),
13034
+ reminders: enterResult.reminders ?? []
13035
+ });
13036
+ }
13037
+ }
12293
13038
  return guideResult(driftWritten, mapping.state, {
12294
13039
  instruction: `# Resumed After Compact \u2014 HEAD Mismatch
12295
13040
 
@@ -12311,8 +13056,23 @@ ${driftPreamble}Recovered to state: **${mapping.state}**. Continue from here.`,
12311
13056
  compactPreparedAt: null,
12312
13057
  resumeBlocked: false,
12313
13058
  guideCallCount: 0,
12314
- contextPressure: { ...resumePressure, level: evaluatePressure({ ...info.state, guideCallCount: 0, contextPressure: resumePressure }) }
13059
+ contextPressure: { ...resumePressure, level: evaluatePressure({ ...info.state, guideCallCount: 0, contextPressure: resumePressure }) },
13060
+ // T-184: Update expectedHead on own-commit drift (mergeBase stays at branch-off point)
13061
+ ...ownCommitDrift ? { git: { ...info.state.git, expectedHead: headResult.data.hash } } : {}
12315
13062
  });
13063
+ appendEvent(info.dir, {
13064
+ rev: written.revision,
13065
+ type: "resumed",
13066
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
13067
+ data: {
13068
+ preCompactState: resumeState,
13069
+ compactionCount: written.contextPressure?.compactionCount ?? 0,
13070
+ ticketId: info.state.ticket?.id ?? null,
13071
+ headMatch: !ownCommitDrift,
13072
+ ownCommit: ownCommitDrift || void 0
13073
+ }
13074
+ });
13075
+ removeResumeMarker(root);
12316
13076
  if (resumeState === "PICK_TICKET") {
12317
13077
  if (isTargetedMode(written)) {
12318
13078
  const dispatched = await dispatchTargetedResume(root, written, info.dir, [
@@ -12426,6 +13186,12 @@ async function handlePreCompact(root, args) {
12426
13186
  await saveSnapshot2(root, loadResult);
12427
13187
  } catch {
12428
13188
  }
13189
+ writeResumeMarker(root, result.sessionId, {
13190
+ ticket: info.state.ticket,
13191
+ completedTickets: info.state.completedTickets,
13192
+ resolvedIssues: info.state.resolvedIssues,
13193
+ preCompactState: result.preCompactState
13194
+ });
12429
13195
  const reread = findSessionById(root, args.sessionId);
12430
13196
  const written = reread?.state ?? info.state;
12431
13197
  return guideResult(written, "COMPACT", {
@@ -12531,9 +13297,22 @@ async function handleCancel(root, args) {
12531
13297
  stashPopFailed
12532
13298
  }
12533
13299
  });
13300
+ removeResumeMarker(root);
13301
+ let reportSection = "";
13302
+ try {
13303
+ const { state: projectState } = await loadProject(root);
13304
+ const nextResult = nextTickets(projectState, 5);
13305
+ const openIssues = projectState.issues.filter((i) => i.status === "open" || i.status === "inprogress").slice(0, 5);
13306
+ const remainingWork = {
13307
+ tickets: nextResult.kind === "found" ? nextResult.candidates.map((c) => ({ id: c.ticket.id, title: c.ticket.title })) : [],
13308
+ issues: openIssues.map((i) => ({ id: i.id, title: i.title, severity: i.severity }))
13309
+ };
13310
+ reportSection = "\n\n" + formatCompactReport({ state: written, endedAt: (/* @__PURE__ */ new Date()).toISOString(), remainingWork });
13311
+ } catch {
13312
+ }
12534
13313
  const stashNote = stashPopFailed ? " Auto-stash pop failed \u2014 run `git stash pop` manually." : "";
12535
13314
  return {
12536
- content: [{ type: "text", text: `Session ${args.sessionId} cancelled. ${written.completedTickets.length} ticket(s) and ${(written.resolvedIssues ?? []).length} issue(s) were completed.${stashNote}` }]
13315
+ content: [{ type: "text", text: `Session ${args.sessionId} cancelled. ${written.completedTickets.length} ticket(s) and ${(written.resolvedIssues ?? []).length} issue(s) were completed.${stashNote}${reportSection}` }]
12537
13316
  };
12538
13317
  }
12539
13318
  function guideResult(state, currentState, opts) {
@@ -12580,268 +13359,74 @@ ${output.reminders.map((r) => `- ${r}`).join("\n")}` : ""
12580
13359
  function guideError(err) {
12581
13360
  const message = err instanceof Error ? err.message : String(err);
12582
13361
  return {
12583
- content: [{ type: "text", text: `[autonomous_guide error] ${message}` }],
12584
- isError: true
12585
- };
12586
- }
12587
- function readFileSafe2(path2) {
12588
- try {
12589
- return readFileSync10(path2, "utf-8");
12590
- } catch {
12591
- return "";
12592
- }
12593
- }
12594
- var RECOVERY_MAPPING, SEVERITY_MAP, workspaceLocks, MAX_AUTO_ADVANCE_DEPTH;
12595
- var init_guide = __esm({
12596
- "src/autonomous/guide.ts"() {
12597
- "use strict";
12598
- init_esm_shims();
12599
- init_session_types();
12600
- init_session();
12601
- init_state_machine();
12602
- init_context_pressure();
12603
- init_review_depth();
12604
- init_git_inspector();
12605
- init_loader();
12606
- init_registry();
12607
- init_types3();
12608
- init_stages();
12609
- init_project_loader();
12610
- init_lessons();
12611
- init_snapshot();
12612
- init_snapshot();
12613
- init_queries();
12614
- init_recommend();
12615
- init_version_check();
12616
- init_target_work();
12617
- init_handover();
12618
- RECOVERY_MAPPING = {
12619
- PICK_TICKET: { state: "PICK_TICKET", resetPlan: false, resetCode: false },
12620
- COMPLETE: { state: "PICK_TICKET", resetPlan: false, resetCode: false },
12621
- HANDOVER: { state: "PICK_TICKET", resetPlan: false, resetCode: false },
12622
- PLAN: { state: "PLAN", resetPlan: true, resetCode: false },
12623
- IMPLEMENT: { state: "PLAN", resetPlan: true, resetCode: false },
12624
- WRITE_TESTS: { state: "PLAN", resetPlan: true, resetCode: false },
12625
- BUILD: { state: "IMPLEMENT", resetPlan: false, resetCode: true },
12626
- VERIFY: { state: "IMPLEMENT", resetPlan: false, resetCode: true },
12627
- PLAN_REVIEW: { state: "PLAN", resetPlan: true, resetCode: true },
12628
- TEST: { state: "IMPLEMENT", resetPlan: false, resetCode: true },
12629
- CODE_REVIEW: { state: "PLAN", resetPlan: true, resetCode: true },
12630
- FINALIZE: { state: "IMPLEMENT", resetPlan: false, resetCode: true },
12631
- LESSON_CAPTURE: { state: "PICK_TICKET", resetPlan: false, resetCode: false },
12632
- ISSUE_FIX: { state: "PICK_TICKET", resetPlan: false, resetCode: false },
12633
- ISSUE_SWEEP: { state: "PICK_TICKET", resetPlan: false, resetCode: false }
12634
- };
12635
- SEVERITY_MAP = {
12636
- critical: "critical",
12637
- major: "high",
12638
- minor: "medium"
12639
- };
12640
- workspaceLocks = /* @__PURE__ */ new Map();
12641
- MAX_AUTO_ADVANCE_DEPTH = 10;
12642
- }
12643
- });
12644
-
12645
- // src/core/session-report-formatter.ts
12646
- function formatSessionReport(data, format) {
12647
- const { state, events, planContent, gitLog } = data;
12648
- if (format === "json") {
12649
- return JSON.stringify({
12650
- ok: true,
12651
- data: {
12652
- summary: buildSummaryData(state),
12653
- ticketProgression: state.completedTickets,
12654
- reviewStats: state.reviews,
12655
- events: events.events.slice(-50),
12656
- malformedEventCount: events.malformedCount,
12657
- contextPressure: state.contextPressure,
12658
- git: {
12659
- branch: state.git.branch,
12660
- initHead: state.git.initHead,
12661
- commits: gitLog
12662
- },
12663
- problems: buildProblems(state, events)
12664
- }
12665
- }, null, 2);
12666
- }
12667
- const sections = [];
12668
- sections.push(buildSummarySection(state));
12669
- sections.push(buildTicketSection(state));
12670
- sections.push(buildReviewSection(state));
12671
- sections.push(buildEventSection(events));
12672
- sections.push(buildPressureSection(state));
12673
- sections.push(buildGitSection(state, gitLog));
12674
- sections.push(buildProblemsSection(state, events));
12675
- return sections.join("\n\n---\n\n");
12676
- }
12677
- function buildSummaryData(state) {
12678
- return {
12679
- sessionId: state.sessionId,
12680
- mode: state.mode ?? "auto",
12681
- recipe: state.recipe,
12682
- status: state.status,
12683
- terminationReason: state.terminationReason,
12684
- startedAt: state.startedAt,
12685
- lastGuideCall: state.lastGuideCall,
12686
- guideCallCount: state.guideCallCount,
12687
- ticketsCompleted: state.completedTickets.length
12688
- };
12689
- }
12690
- function buildSummarySection(state) {
12691
- const duration = state.startedAt && state.lastGuideCall ? formatDuration(state.startedAt, state.lastGuideCall) : "unknown";
12692
- return [
12693
- "## Session Summary",
12694
- "",
12695
- `- **ID:** ${state.sessionId}`,
12696
- `- **Mode:** ${state.mode ?? "auto"}`,
12697
- `- **Recipe:** ${state.recipe}`,
12698
- `- **Status:** ${state.status}${state.terminationReason ? ` (${state.terminationReason})` : ""}`,
12699
- `- **Duration:** ${duration}`,
12700
- `- **Guide calls:** ${state.guideCallCount}`,
12701
- `- **Tickets completed:** ${state.completedTickets.length}`
12702
- ].join("\n");
12703
- }
12704
- function buildTicketSection(state) {
12705
- if (state.completedTickets.length === 0) {
12706
- const current = state.ticket;
12707
- if (current) {
12708
- return [
12709
- "## Ticket Progression",
12710
- "",
12711
- `In progress: **${current.id}** \u2014 ${current.title} (risk: ${current.risk ?? "unknown"})`
12712
- ].join("\n");
12713
- }
12714
- return "## Ticket Progression\n\nNo tickets completed.";
12715
- }
12716
- const lines = ["## Ticket Progression", ""];
12717
- for (const t of state.completedTickets) {
12718
- const risk = t.realizedRisk ? `${t.risk ?? "?"} \u2192 ${t.realizedRisk}` : t.risk ?? "unknown";
12719
- lines.push(`- **${t.id}:** ${t.title} | risk: ${risk} | commit: \`${t.commitHash ?? "?"}\``);
12720
- }
12721
- return lines.join("\n");
12722
- }
12723
- function buildReviewSection(state) {
12724
- const plan = state.reviews.plan;
12725
- const code = state.reviews.code;
12726
- if (plan.length === 0 && code.length === 0) {
12727
- return "## Review Stats\n\nNo reviews recorded.";
12728
- }
12729
- const lines = ["## Review Stats", ""];
12730
- if (plan.length > 0) {
12731
- lines.push(`**Plan reviews:** ${plan.length} round(s)`);
12732
- for (const r of plan) {
12733
- lines.push(` - Round ${r.round}: ${r.verdict} (${r.findingCount} findings, ${r.criticalCount} critical, ${r.majorCount} major) \u2014 ${r.reviewer}`);
12734
- }
12735
- }
12736
- if (code.length > 0) {
12737
- lines.push(`**Code reviews:** ${code.length} round(s)`);
12738
- for (const r of code) {
12739
- lines.push(` - Round ${r.round}: ${r.verdict} (${r.findingCount} findings, ${r.criticalCount} critical, ${r.majorCount} major) \u2014 ${r.reviewer}`);
12740
- }
12741
- }
12742
- const totalFindings = [...plan, ...code].reduce((sum, r) => sum + r.findingCount, 0);
12743
- lines.push("", `**Total findings:** ${totalFindings}`);
12744
- return lines.join("\n");
12745
- }
12746
- function buildEventSection(events) {
12747
- if (events.events.length === 0 && events.malformedCount === 0) {
12748
- return "## Event Timeline\n\nNot available.";
12749
- }
12750
- const capped = events.events.slice(-50);
12751
- const omitted = events.events.length - capped.length;
12752
- const lines = ["## Event Timeline", ""];
12753
- if (omitted > 0) {
12754
- lines.push(`*${omitted} earlier events omitted*`, "");
12755
- }
12756
- for (const e of capped) {
12757
- const ts = e.timestamp ? e.timestamp.slice(11, 19) : "??:??:??";
12758
- const detail = e.data ? Object.entries(e.data).map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(" ") : "";
12759
- lines.push(`- \`${ts}\` [${e.type}] ${detail}`.trimEnd());
12760
- }
12761
- if (events.malformedCount > 0) {
12762
- lines.push("", `*${events.malformedCount} malformed event line(s) skipped*`);
12763
- }
12764
- return lines.join("\n");
12765
- }
12766
- function buildPressureSection(state) {
12767
- const p = state.contextPressure;
12768
- return [
12769
- "## Context Pressure",
12770
- "",
12771
- `- **Level:** ${p.level}`,
12772
- `- **Guide calls:** ${p.guideCallCount}`,
12773
- `- **Tickets completed:** ${p.ticketsCompleted}`,
12774
- `- **Compactions:** ${p.compactionCount}`,
12775
- `- **Events log:** ${p.eventsLogBytes} bytes`
12776
- ].join("\n");
12777
- }
12778
- function buildGitSection(state, gitLog) {
12779
- const lines = [
12780
- "## Git Summary",
12781
- "",
12782
- `- **Branch:** ${state.git.branch ?? "unknown"}`,
12783
- `- **Init HEAD:** \`${state.git.initHead ?? "?"}\``,
12784
- `- **Expected HEAD:** \`${state.git.expectedHead ?? "?"}\``
12785
- ];
12786
- if (gitLog && gitLog.length > 0) {
12787
- lines.push("", "**Commits:**");
12788
- for (const c of gitLog) {
12789
- lines.push(`- ${c}`);
12790
- }
12791
- } else {
12792
- lines.push("", "Commits: Not available.");
12793
- }
12794
- return lines.join("\n");
12795
- }
12796
- function buildProblems(state, events) {
12797
- const problems = [];
12798
- if (state.terminationReason && state.terminationReason !== "normal") {
12799
- problems.push(`Abnormal termination: ${state.terminationReason}`);
12800
- }
12801
- if (events.malformedCount > 0) {
12802
- problems.push(`${events.malformedCount} malformed event line(s) in events.log`);
12803
- }
12804
- for (const e of events.events) {
12805
- if (e.type.includes("error") || e.type.includes("exhaustion")) {
12806
- problems.push(`[${e.type}] ${e.timestamp ?? ""} ${JSON.stringify(e.data)}`);
12807
- } else if (e.data?.result === "exhaustion") {
12808
- problems.push(`[${e.type}] exhaustion at ${e.timestamp ?? ""}`);
12809
- }
12810
- }
12811
- if (state.deferralsUnfiled) {
12812
- problems.push("Session has unfiled deferrals");
12813
- }
12814
- return problems;
12815
- }
12816
- function buildProblemsSection(state, events) {
12817
- const problems = buildProblems(state, events);
12818
- if (problems.length === 0) {
12819
- return "## Problems\n\nNone detected.";
12820
- }
12821
- return ["## Problems", "", ...problems.map((p) => `- ${p}`)].join("\n");
13362
+ content: [{ type: "text", text: `[autonomous_guide error] ${message}` }],
13363
+ isError: true
13364
+ };
12822
13365
  }
12823
- function formatDuration(start, end) {
13366
+ function readFileSafe2(path2) {
12824
13367
  try {
12825
- const ms = new Date(end).getTime() - new Date(start).getTime();
12826
- if (isNaN(ms) || ms < 0) return "unknown";
12827
- const mins = Math.floor(ms / 6e4);
12828
- if (mins < 60) return `${mins}m`;
12829
- const hours = Math.floor(mins / 60);
12830
- return `${hours}h ${mins % 60}m`;
13368
+ return readFileSync10(path2, "utf-8");
12831
13369
  } catch {
12832
- return "unknown";
13370
+ return "";
12833
13371
  }
12834
13372
  }
12835
- var init_session_report_formatter = __esm({
12836
- "src/core/session-report-formatter.ts"() {
13373
+ var RECOVERY_MAPPING, SEVERITY_MAP, workspaceLocks, MAX_AUTO_ADVANCE_DEPTH;
13374
+ var init_guide = __esm({
13375
+ "src/autonomous/guide.ts"() {
12837
13376
  "use strict";
12838
13377
  init_esm_shims();
13378
+ init_session_types();
13379
+ init_session();
13380
+ init_state_machine();
13381
+ init_context_pressure();
13382
+ init_review_depth();
13383
+ init_git_inspector();
13384
+ init_loader();
13385
+ init_registry();
13386
+ init_types3();
13387
+ init_stages();
13388
+ init_project_loader();
13389
+ init_lessons();
13390
+ init_snapshot();
13391
+ init_snapshot();
13392
+ init_queries();
13393
+ init_recommend();
13394
+ init_version_check();
13395
+ init_resume_marker();
13396
+ init_session_report_formatter();
13397
+ init_target_work();
13398
+ init_handover();
13399
+ RECOVERY_MAPPING = {
13400
+ PICK_TICKET: { state: "PICK_TICKET", resetPlan: false, resetCode: false },
13401
+ COMPLETE: { state: "PICK_TICKET", resetPlan: false, resetCode: false },
13402
+ HANDOVER: { state: "PICK_TICKET", resetPlan: false, resetCode: false },
13403
+ PLAN: { state: "PLAN", resetPlan: true, resetCode: false },
13404
+ IMPLEMENT: { state: "PLAN", resetPlan: true, resetCode: false },
13405
+ WRITE_TESTS: { state: "PLAN", resetPlan: true, resetCode: false },
13406
+ BUILD: { state: "IMPLEMENT", resetPlan: false, resetCode: true },
13407
+ VERIFY: { state: "IMPLEMENT", resetPlan: false, resetCode: true },
13408
+ PLAN_REVIEW: { state: "PLAN", resetPlan: true, resetCode: true },
13409
+ TEST: { state: "IMPLEMENT", resetPlan: false, resetCode: true },
13410
+ CODE_REVIEW: { state: "PLAN", resetPlan: true, resetCode: true },
13411
+ FINALIZE: { state: "IMPLEMENT", resetPlan: false, resetCode: true },
13412
+ LESSON_CAPTURE: { state: "PICK_TICKET", resetPlan: false, resetCode: false },
13413
+ ISSUE_FIX: { state: "ISSUE_FIX", resetPlan: false, resetCode: false },
13414
+ // T-208: self-recover to avoid dangling currentIssue
13415
+ ISSUE_SWEEP: { state: "PICK_TICKET", resetPlan: false, resetCode: false }
13416
+ };
13417
+ SEVERITY_MAP = {
13418
+ critical: "critical",
13419
+ major: "high",
13420
+ minor: "medium"
13421
+ };
13422
+ workspaceLocks = /* @__PURE__ */ new Map();
13423
+ MAX_AUTO_ADVANCE_DEPTH = 10;
12839
13424
  }
12840
13425
  });
12841
13426
 
12842
13427
  // src/cli/commands/session-report.ts
12843
- import { readFileSync as readFileSync11, existsSync as existsSync12 } from "fs";
12844
- import { join as join20 } from "path";
13428
+ import { readFileSync as readFileSync11, existsSync as existsSync13 } from "fs";
13429
+ import { join as join21 } from "path";
12845
13430
  async function handleSessionReport(sessionId, root, format = "md") {
12846
13431
  if (!UUID_REGEX.test(sessionId)) {
12847
13432
  return {
@@ -12852,7 +13437,7 @@ async function handleSessionReport(sessionId, root, format = "md") {
12852
13437
  };
12853
13438
  }
12854
13439
  const dir = sessionDir(root, sessionId);
12855
- if (!existsSync12(dir)) {
13440
+ if (!existsSync13(dir)) {
12856
13441
  return {
12857
13442
  output: `Error: Session ${sessionId} not found.`,
12858
13443
  exitCode: 1,
@@ -12860,8 +13445,8 @@ async function handleSessionReport(sessionId, root, format = "md") {
12860
13445
  isError: true
12861
13446
  };
12862
13447
  }
12863
- const statePath2 = join20(dir, "state.json");
12864
- if (!existsSync12(statePath2)) {
13448
+ const statePath2 = join21(dir, "state.json");
13449
+ if (!existsSync13(statePath2)) {
12865
13450
  return {
12866
13451
  output: `Error: Session ${sessionId} corrupt \u2014 state.json missing.`,
12867
13452
  exitCode: 1,
@@ -12899,7 +13484,7 @@ async function handleSessionReport(sessionId, root, format = "md") {
12899
13484
  const events = readEvents(dir);
12900
13485
  let planContent = null;
12901
13486
  try {
12902
- planContent = readFileSync11(join20(dir, "plan.md"), "utf-8");
13487
+ planContent = readFileSync11(join21(dir, "plan.md"), "utf-8");
12903
13488
  } catch {
12904
13489
  }
12905
13490
  let gitLog = null;
@@ -12928,7 +13513,7 @@ var init_session_report = __esm({
12928
13513
  });
12929
13514
 
12930
13515
  // src/cli/commands/phase.ts
12931
- import { join as join21, resolve as resolve7 } from "path";
13516
+ import { join as join22, resolve as resolve7 } from "path";
12932
13517
  function validatePhaseId(id) {
12933
13518
  if (id.length > PHASE_ID_MAX_LENGTH) {
12934
13519
  throw new CliValidationError("invalid_input", `Phase ID "${id}" exceeds ${PHASE_ID_MAX_LENGTH} characters`);
@@ -13117,21 +13702,21 @@ async function handlePhaseDelete(id, reassign, format, root) {
13117
13702
  const updated = { ...ticket, phase: reassign, order: maxOrder };
13118
13703
  const parsed = TicketSchema.parse(updated);
13119
13704
  const content = serializeJSON(parsed);
13120
- const target = join21(wrapDir, "tickets", `${parsed.id}.json`);
13705
+ const target = join22(wrapDir, "tickets", `${parsed.id}.json`);
13121
13706
  operations.push({ op: "write", target, content });
13122
13707
  }
13123
13708
  for (const issue of affectedIssues) {
13124
13709
  const updated = { ...issue, phase: reassign };
13125
13710
  const parsed = IssueSchema.parse(updated);
13126
13711
  const content = serializeJSON(parsed);
13127
- const target = join21(wrapDir, "issues", `${parsed.id}.json`);
13712
+ const target = join22(wrapDir, "issues", `${parsed.id}.json`);
13128
13713
  operations.push({ op: "write", target, content });
13129
13714
  }
13130
13715
  const newPhases = state.roadmap.phases.filter((p) => p.id !== id);
13131
13716
  const newRoadmap = { ...state.roadmap, phases: newPhases };
13132
13717
  const parsedRoadmap = RoadmapSchema.parse(newRoadmap);
13133
13718
  const roadmapContent = serializeJSON(parsedRoadmap);
13134
- const roadmapTarget = join21(wrapDir, "roadmap.json");
13719
+ const roadmapTarget = join22(wrapDir, "roadmap.json");
13135
13720
  operations.push({ op: "write", target: roadmapTarget, content: roadmapContent });
13136
13721
  await runTransactionUnlocked(root, operations);
13137
13722
  } else {
@@ -13164,14 +13749,15 @@ var init_phase = __esm({
13164
13749
 
13165
13750
  // src/mcp/tools.ts
13166
13751
  import { z as z10 } from "zod";
13167
- import { join as join22 } from "path";
13752
+ import { readFileSync as readFileSync12, writeFileSync as writeFileSync7, mkdirSync as mkdirSync5 } from "fs";
13753
+ import { join as join23 } from "path";
13168
13754
  function formatMcpError(code, message) {
13169
13755
  return `[${code}] ${message}`;
13170
13756
  }
13171
13757
  async function runMcpReadTool(pinnedRoot, handler) {
13172
13758
  try {
13173
13759
  const { state, warnings } = await loadProject(pinnedRoot);
13174
- const handoversDir = join22(pinnedRoot, ".story", "handovers");
13760
+ const handoversDir = join23(pinnedRoot, ".story", "handovers");
13175
13761
  const ctx = { state, warnings, root: pinnedRoot, handoversDir, format: "md" };
13176
13762
  const result = await handler(ctx);
13177
13763
  if (result.errorCode && INFRASTRUCTURE_ERROR_CODES.includes(result.errorCode)) {
@@ -13719,6 +14305,7 @@ function registerAllTools(server, pinnedRoot) {
13719
14305
  disposition: z10.string()
13720
14306
  })).optional().describe("Review findings"),
13721
14307
  reviewerSessionId: z10.string().optional().describe("Codex session ID"),
14308
+ reviewer: z10.string().optional().describe("Actual reviewer backend used (e.g. 'agent' when codex was unavailable)"),
13722
14309
  notes: z10.string().optional().describe("Free-text notes")
13723
14310
  }).optional().describe("Report data (required for report action)")
13724
14311
  }
@@ -13743,7 +14330,7 @@ function registerAllTools(server, pinnedRoot) {
13743
14330
  }
13744
14331
  });
13745
14332
  server.registerTool("claudestory_review_lenses_synthesize", {
13746
- description: "Synthesize lens results after parallel review. Validates findings, applies blocking policy, generates merger prompt. Call after collecting all lens subagent results.",
14333
+ description: "Synthesize lens results after parallel review. Validates findings, applies blocking policy, classifies origin (introduced vs pre-existing), auto-files pre-existing issues, generates merger prompt. Call after collecting all lens subagent results.",
13747
14334
  inputSchema: {
13748
14335
  stage: z10.enum(["CODE_REVIEW", "PLAN_REVIEW"]).optional().describe("Review stage (defaults to CODE_REVIEW)"),
13749
14336
  lensResults: z10.array(z10.object({
@@ -13754,9 +14341,13 @@ function registerAllTools(server, pinnedRoot) {
13754
14341
  activeLenses: z10.array(z10.string()).describe("Active lens names from prepare step"),
13755
14342
  skippedLenses: z10.array(z10.string()).describe("Skipped lens names from prepare step"),
13756
14343
  reviewRound: z10.number().int().min(1).optional().describe("Current review round"),
13757
- reviewId: z10.string().optional().describe("Review ID from prepare step")
14344
+ reviewId: z10.string().optional().describe("Review ID from prepare step"),
14345
+ // T-192: Origin classification inputs
14346
+ diff: z10.string().optional().describe("The diff being reviewed (for origin classification of findings into introduced vs pre-existing)"),
14347
+ changedFiles: z10.array(z10.string()).optional().describe("Changed file paths from prepare step (for origin classification)"),
14348
+ sessionId: z10.string().uuid().optional().describe("Active session ID (for dedup of auto-filed pre-existing issues across review rounds)")
13758
14349
  }
13759
- }, (args) => {
14350
+ }, async (args) => {
13760
14351
  try {
13761
14352
  const result = handleSynthesize({
13762
14353
  stage: args.stage,
@@ -13767,9 +14358,55 @@ function registerAllTools(server, pinnedRoot) {
13767
14358
  reviewRound: args.reviewRound ?? 1,
13768
14359
  reviewId: args.reviewId ?? "unknown"
13769
14360
  },
13770
- projectRoot: pinnedRoot
14361
+ projectRoot: pinnedRoot,
14362
+ diff: args.diff,
14363
+ changedFiles: args.changedFiles
13771
14364
  });
13772
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
14365
+ const filedIssues = [];
14366
+ if (result.preExistingFindings.length > 0) {
14367
+ const sessionDir2 = args.sessionId ? join23(pinnedRoot, ".story", "sessions", args.sessionId) : null;
14368
+ const alreadyFiled = sessionDir2 ? readFiledPreexisting(sessionDir2) : /* @__PURE__ */ new Set();
14369
+ const sizeBeforeLoop = alreadyFiled.size;
14370
+ for (const f of result.preExistingFindings) {
14371
+ const dedupKey = f.issueKey ?? `${f.file ?? ""}:${f.line ?? 0}:${f.category}`;
14372
+ if (alreadyFiled.has(dedupKey)) continue;
14373
+ try {
14374
+ const { handleIssueCreate: handleIssueCreate2 } = await Promise.resolve().then(() => (init_issue2(), issue_exports));
14375
+ const severityMap = { critical: "critical", major: "high", minor: "medium" };
14376
+ const severity = severityMap[f.severity] ?? "medium";
14377
+ const issueResult = await handleIssueCreate2(
14378
+ {
14379
+ title: `[pre-existing] [${f.category}] ${f.description.slice(0, 60)}`,
14380
+ severity,
14381
+ impact: f.description,
14382
+ components: ["review-lenses"],
14383
+ relatedTickets: [],
14384
+ location: f.file && f.line != null ? [`${f.file}:${f.line}`] : []
14385
+ },
14386
+ "json",
14387
+ pinnedRoot
14388
+ );
14389
+ let issueId;
14390
+ try {
14391
+ const parsed = JSON.parse(issueResult.output ?? "");
14392
+ issueId = parsed?.data?.id;
14393
+ } catch {
14394
+ const match = issueResult.output?.match(/ISS-\d+/);
14395
+ issueId = match?.[0];
14396
+ }
14397
+ if (issueId) {
14398
+ filedIssues.push({ issueKey: dedupKey, issueId });
14399
+ alreadyFiled.add(dedupKey);
14400
+ }
14401
+ } catch {
14402
+ }
14403
+ }
14404
+ if (sessionDir2 && alreadyFiled.size > sizeBeforeLoop) {
14405
+ writeFiledPreexisting(sessionDir2, alreadyFiled);
14406
+ }
14407
+ }
14408
+ const output = { ...result, filedIssues };
14409
+ return { content: [{ type: "text", text: JSON.stringify(output, null, 2) }] };
13773
14410
  } catch (err) {
13774
14411
  const msg = err instanceof Error ? err.message.replace(/\/[^\s]+/g, "<path>") : "unknown error";
13775
14412
  return { content: [{ type: "text", text: `Error synthesizing lens results: ${msg}` }], isError: true };
@@ -13810,7 +14447,23 @@ function registerAllTools(server, pinnedRoot) {
13810
14447
  }
13811
14448
  });
13812
14449
  }
13813
- var INFRASTRUCTURE_ERROR_CODES;
14450
+ function readFiledPreexisting(sessionDir2) {
14451
+ try {
14452
+ const raw = readFileSync12(join23(sessionDir2, FILED_PREEXISTING_FILE), "utf-8");
14453
+ const arr = JSON.parse(raw);
14454
+ return new Set(Array.isArray(arr) ? arr : []);
14455
+ } catch {
14456
+ return /* @__PURE__ */ new Set();
14457
+ }
14458
+ }
14459
+ function writeFiledPreexisting(sessionDir2, keys) {
14460
+ try {
14461
+ mkdirSync5(sessionDir2, { recursive: true });
14462
+ writeFileSync7(join23(sessionDir2, FILED_PREEXISTING_FILE), JSON.stringify([...keys], null, 2));
14463
+ } catch {
14464
+ }
14465
+ }
14466
+ var INFRASTRUCTURE_ERROR_CODES, FILED_PREEXISTING_FILE;
13814
14467
  var init_tools = __esm({
13815
14468
  "src/mcp/tools.ts"() {
13816
14469
  "use strict";
@@ -13843,15 +14496,16 @@ var init_tools = __esm({
13843
14496
  "project_corrupt",
13844
14497
  "version_mismatch"
13845
14498
  ];
14499
+ FILED_PREEXISTING_FILE = "filed-preexisting.json";
13846
14500
  }
13847
14501
  });
13848
14502
 
13849
14503
  // src/core/init.ts
13850
14504
  import { mkdir as mkdir4, stat as stat2, readFile as readFile4, writeFile as writeFile2 } from "fs/promises";
13851
- import { join as join23, resolve as resolve8 } from "path";
14505
+ import { join as join24, resolve as resolve8 } from "path";
13852
14506
  async function initProject(root, options) {
13853
14507
  const absRoot = resolve8(root);
13854
- const wrapDir = join23(absRoot, ".story");
14508
+ const wrapDir = join24(absRoot, ".story");
13855
14509
  let exists = false;
13856
14510
  try {
13857
14511
  const s = await stat2(wrapDir);
@@ -13871,11 +14525,11 @@ async function initProject(root, options) {
13871
14525
  ".story/ already exists. Use --force to overwrite config and roadmap."
13872
14526
  );
13873
14527
  }
13874
- await mkdir4(join23(wrapDir, "tickets"), { recursive: true });
13875
- await mkdir4(join23(wrapDir, "issues"), { recursive: true });
13876
- await mkdir4(join23(wrapDir, "handovers"), { recursive: true });
13877
- await mkdir4(join23(wrapDir, "notes"), { recursive: true });
13878
- await mkdir4(join23(wrapDir, "lessons"), { recursive: true });
14528
+ await mkdir4(join24(wrapDir, "tickets"), { recursive: true });
14529
+ await mkdir4(join24(wrapDir, "issues"), { recursive: true });
14530
+ await mkdir4(join24(wrapDir, "handovers"), { recursive: true });
14531
+ await mkdir4(join24(wrapDir, "notes"), { recursive: true });
14532
+ await mkdir4(join24(wrapDir, "lessons"), { recursive: true });
13879
14533
  const created = [
13880
14534
  ".story/config.json",
13881
14535
  ".story/roadmap.json",
@@ -13915,7 +14569,7 @@ async function initProject(root, options) {
13915
14569
  };
13916
14570
  await writeConfig(config, absRoot);
13917
14571
  await writeRoadmap(roadmap, absRoot);
13918
- const gitignorePath = join23(wrapDir, ".gitignore");
14572
+ const gitignorePath = join24(wrapDir, ".gitignore");
13919
14573
  await ensureGitignoreEntries(gitignorePath, STORY_GITIGNORE_ENTRIES);
13920
14574
  const warnings = [];
13921
14575
  if (options.force && exists) {
@@ -13960,11 +14614,405 @@ var init_init = __esm({
13960
14614
  }
13961
14615
  });
13962
14616
 
14617
+ // src/channel/events.ts
14618
+ import { z as z11 } from "zod";
14619
+ function isValidInboxFilename(filename) {
14620
+ if (filename.includes("/") || filename.includes("\\") || filename.includes("..")) {
14621
+ return false;
14622
+ }
14623
+ return VALID_FILENAME.test(filename);
14624
+ }
14625
+ function formatChannelContent(event) {
14626
+ switch (event.event) {
14627
+ case "ticket_requested":
14628
+ return `User requested ticket ${event.payload.ticketId} be started.`;
14629
+ case "pause_session":
14630
+ return "User requested the autonomous session be paused.";
14631
+ case "resume_session":
14632
+ return "User requested the autonomous session be resumed.";
14633
+ case "cancel_session": {
14634
+ const reason = event.payload.reason ? ` Reason: ${event.payload.reason}` : "";
14635
+ return `User requested the autonomous session be cancelled.${reason}`;
14636
+ }
14637
+ case "priority_changed":
14638
+ return `User changed priority of ticket ${event.payload.ticketId} to order ${event.payload.newOrder}.`;
14639
+ case "permission_response":
14640
+ return `Permission response for request ${event.payload.requestId}: ${event.payload.behavior}.`;
14641
+ }
14642
+ }
14643
+ function formatChannelMeta(event) {
14644
+ const meta = { event: event.event };
14645
+ switch (event.event) {
14646
+ case "ticket_requested":
14647
+ meta.ticketId = event.payload.ticketId;
14648
+ break;
14649
+ case "cancel_session":
14650
+ if (event.payload.reason) meta.reason = event.payload.reason;
14651
+ break;
14652
+ case "priority_changed":
14653
+ meta.ticketId = event.payload.ticketId;
14654
+ meta.newOrder = String(event.payload.newOrder);
14655
+ break;
14656
+ case "permission_response":
14657
+ meta.requestId = event.payload.requestId;
14658
+ meta.behavior = event.payload.behavior;
14659
+ break;
14660
+ }
14661
+ return meta;
14662
+ }
14663
+ var TicketRequestedPayload, PauseSessionPayload, ResumeSessionPayload, CancelSessionPayload, PriorityChangedPayload, PermissionResponsePayload, ChannelEventSchema, VALID_FILENAME;
14664
+ var init_events = __esm({
14665
+ "src/channel/events.ts"() {
14666
+ "use strict";
14667
+ init_esm_shims();
14668
+ TicketRequestedPayload = z11.object({
14669
+ ticketId: z11.string().regex(/^T-\d+[a-z]?$/)
14670
+ });
14671
+ PauseSessionPayload = z11.object({});
14672
+ ResumeSessionPayload = z11.object({});
14673
+ CancelSessionPayload = z11.object({
14674
+ reason: z11.string().max(1e3).optional()
14675
+ });
14676
+ PriorityChangedPayload = z11.object({
14677
+ ticketId: z11.string().regex(/^T-\d+[a-z]?$/),
14678
+ newOrder: z11.number().int()
14679
+ });
14680
+ PermissionResponsePayload = z11.object({
14681
+ requestId: z11.string().regex(/^[a-zA-Z0-9]{5}$/),
14682
+ behavior: z11.enum(["approve", "deny"])
14683
+ });
14684
+ ChannelEventSchema = z11.discriminatedUnion("event", [
14685
+ z11.object({
14686
+ event: z11.literal("ticket_requested"),
14687
+ timestamp: z11.string(),
14688
+ payload: TicketRequestedPayload
14689
+ }),
14690
+ z11.object({
14691
+ event: z11.literal("pause_session"),
14692
+ timestamp: z11.string(),
14693
+ payload: PauseSessionPayload
14694
+ }),
14695
+ z11.object({
14696
+ event: z11.literal("resume_session"),
14697
+ timestamp: z11.string(),
14698
+ payload: ResumeSessionPayload
14699
+ }),
14700
+ z11.object({
14701
+ event: z11.literal("cancel_session"),
14702
+ timestamp: z11.string(),
14703
+ payload: CancelSessionPayload
14704
+ }),
14705
+ z11.object({
14706
+ event: z11.literal("priority_changed"),
14707
+ timestamp: z11.string(),
14708
+ payload: PriorityChangedPayload
14709
+ }),
14710
+ z11.object({
14711
+ event: z11.literal("permission_response"),
14712
+ timestamp: z11.string(),
14713
+ payload: PermissionResponsePayload
14714
+ })
14715
+ ]);
14716
+ VALID_FILENAME = /^[\d]{4}-[\d]{2}-[\d]{2}T[\w.:-]+-[\w]+\.json$/;
14717
+ }
14718
+ });
14719
+
14720
+ // src/channel/inbox-watcher.ts
14721
+ import { watch } from "fs";
14722
+ import { readdir as readdir4, readFile as readFile5, unlink as unlink3, rename as rename2, mkdir as mkdir5 } from "fs/promises";
14723
+ import { join as join25 } from "path";
14724
+ async function startInboxWatcher(root, server) {
14725
+ const inboxPath = join25(root, ".story", INBOX_DIR);
14726
+ if (watcher) {
14727
+ watcher.close();
14728
+ watcher = null;
14729
+ permissionRetryCount.clear();
14730
+ }
14731
+ await mkdir5(inboxPath, { recursive: true });
14732
+ await recoverStaleProcessingFiles(inboxPath);
14733
+ await processInbox(inboxPath, server);
14734
+ try {
14735
+ watcher = watch(inboxPath, (eventType) => {
14736
+ if (eventType === "rename") {
14737
+ scheduleDebouncedProcess(inboxPath, server);
14738
+ }
14739
+ });
14740
+ watcher.on("error", (err) => {
14741
+ process.stderr.write(`claudestory: channel inbox watcher error: ${err.message}
14742
+ `);
14743
+ startPollingFallback(inboxPath, server);
14744
+ });
14745
+ process.stderr.write(`claudestory: channel inbox watcher started at ${inboxPath}
14746
+ `);
14747
+ } catch (err) {
14748
+ const msg = err instanceof Error ? err.message : String(err);
14749
+ process.stderr.write(`claudestory: failed to start inbox watcher, using polling fallback: ${msg}
14750
+ `);
14751
+ startPollingFallback(inboxPath, server);
14752
+ }
14753
+ }
14754
+ function stopInboxWatcher() {
14755
+ if (debounceTimer) {
14756
+ clearTimeout(debounceTimer);
14757
+ debounceTimer = null;
14758
+ }
14759
+ if (watcher) {
14760
+ watcher.close();
14761
+ watcher = null;
14762
+ }
14763
+ if (pollInterval) {
14764
+ clearInterval(pollInterval);
14765
+ pollInterval = null;
14766
+ }
14767
+ permissionRetryCount.clear();
14768
+ }
14769
+ function scheduleDebouncedProcess(inboxPath, server) {
14770
+ if (debounceTimer) clearTimeout(debounceTimer);
14771
+ debounceTimer = setTimeout(() => {
14772
+ debounceTimer = null;
14773
+ processInbox(inboxPath, server).catch((err) => {
14774
+ const msg = err instanceof Error ? err.message : String(err);
14775
+ process.stderr.write(`claudestory: inbox processing error: ${msg}
14776
+ `);
14777
+ });
14778
+ }, DEBOUNCE_MS);
14779
+ }
14780
+ function startPollingFallback(inboxPath, server) {
14781
+ if (pollInterval) return;
14782
+ pollInterval = setInterval(() => {
14783
+ processInbox(inboxPath, server).catch((err) => {
14784
+ const msg = err instanceof Error ? err.message : String(err);
14785
+ process.stderr.write(`claudestory: poll processing error: ${msg}
14786
+ `);
14787
+ });
14788
+ }, 2e3);
14789
+ }
14790
+ async function recoverStaleProcessingFiles(inboxPath) {
14791
+ let entries;
14792
+ try {
14793
+ entries = await readdir4(inboxPath);
14794
+ } catch {
14795
+ return;
14796
+ }
14797
+ for (const f of entries) {
14798
+ if (f.endsWith(".processing")) {
14799
+ const originalName = f.slice(0, -".processing".length);
14800
+ try {
14801
+ await rename2(join25(inboxPath, f), join25(inboxPath, originalName));
14802
+ process.stderr.write(`claudestory: recovered stale processing file: ${f}
14803
+ `);
14804
+ } catch {
14805
+ }
14806
+ }
14807
+ }
14808
+ }
14809
+ async function processInbox(inboxPath, server) {
14810
+ while (true) {
14811
+ let entries;
14812
+ try {
14813
+ entries = await readdir4(inboxPath);
14814
+ } catch {
14815
+ return;
14816
+ }
14817
+ const eventFiles = entries.filter((f) => f.endsWith(".json") && !f.startsWith(".")).sort();
14818
+ if (eventFiles.length === 0) break;
14819
+ const batch = eventFiles.slice(0, MAX_INBOX_DEPTH);
14820
+ if (eventFiles.length > MAX_INBOX_DEPTH) {
14821
+ process.stderr.write(
14822
+ `claudestory: channel inbox has ${eventFiles.length} files, processing batch of ${MAX_INBOX_DEPTH}
14823
+ `
14824
+ );
14825
+ }
14826
+ for (const filename of batch) {
14827
+ await processEventFile(inboxPath, filename, server);
14828
+ }
14829
+ if (eventFiles.length <= MAX_INBOX_DEPTH) break;
14830
+ }
14831
+ await trimFailedDirectory(inboxPath);
14832
+ }
14833
+ async function processEventFile(inboxPath, filename, server) {
14834
+ if (!isValidInboxFilename(filename)) {
14835
+ process.stderr.write(`claudestory: rejecting invalid inbox filename: ${filename}
14836
+ `);
14837
+ await moveToFailed(inboxPath, filename);
14838
+ return;
14839
+ }
14840
+ const filePath = join25(inboxPath, filename);
14841
+ const processingPath = join25(inboxPath, `${filename}.processing`);
14842
+ try {
14843
+ await rename2(filePath, processingPath);
14844
+ } catch {
14845
+ return;
14846
+ }
14847
+ let raw;
14848
+ try {
14849
+ raw = await readFile5(processingPath, "utf-8");
14850
+ } catch {
14851
+ await moveToFailed(inboxPath, `${filename}.processing`, filename);
14852
+ return;
14853
+ }
14854
+ const processingFilename = `${filename}.processing`;
14855
+ let parsed;
14856
+ try {
14857
+ parsed = JSON.parse(raw);
14858
+ } catch {
14859
+ process.stderr.write(`claudestory: invalid JSON in channel event ${filename}
14860
+ `);
14861
+ await moveToFailed(inboxPath, processingFilename, filename);
14862
+ return;
14863
+ }
14864
+ const result = ChannelEventSchema.safeParse(parsed);
14865
+ if (!result.success) {
14866
+ process.stderr.write(`claudestory: invalid channel event schema in ${filename}: ${result.error.message}
14867
+ `);
14868
+ await moveToFailed(inboxPath, processingFilename, filename);
14869
+ return;
14870
+ }
14871
+ const event = result.data;
14872
+ try {
14873
+ if (event.event === "permission_response") {
14874
+ await server.server.sendNotification({
14875
+ method: "notifications/claude/channel/permission",
14876
+ params: {
14877
+ requestId: event.payload.requestId,
14878
+ behavior: event.payload.behavior
14879
+ }
14880
+ });
14881
+ } else {
14882
+ const content = formatChannelContent(event);
14883
+ const meta = formatChannelMeta(event);
14884
+ await server.server.sendNotification({
14885
+ method: "notifications/claude/channel",
14886
+ params: { content, meta }
14887
+ });
14888
+ }
14889
+ process.stderr.write(`claudestory: sent channel event ${event.event}
14890
+ `);
14891
+ permissionRetryCount.delete(filename);
14892
+ eventRetryCount.delete(filename);
14893
+ } catch (err) {
14894
+ const msg = err instanceof Error ? err.message : String(err);
14895
+ if (event.event === "permission_response") {
14896
+ const retries2 = (permissionRetryCount.get(filename) ?? 0) + 1;
14897
+ permissionRetryCount.set(filename, retries2);
14898
+ if (retries2 >= MAX_PERMISSION_RETRIES) {
14899
+ process.stderr.write(`claudestory: permission notification failed after ${retries2} retries, quarantining: ${msg}
14900
+ `);
14901
+ permissionRetryCount.delete(filename);
14902
+ await moveToFailed(inboxPath, processingFilename, filename);
14903
+ return;
14904
+ }
14905
+ try {
14906
+ await rename2(processingPath, filePath);
14907
+ } catch (renameErr) {
14908
+ const renameMsg = renameErr instanceof Error ? renameErr.message : String(renameErr);
14909
+ process.stderr.write(`claudestory: rename-back failed for ${filename}, quarantining: ${renameMsg}
14910
+ `);
14911
+ permissionRetryCount.delete(filename);
14912
+ await moveToFailed(inboxPath, processingFilename, filename);
14913
+ return;
14914
+ }
14915
+ process.stderr.write(`claudestory: permission notification failed (attempt ${retries2}/${MAX_PERMISSION_RETRIES}), keeping for retry: ${msg}
14916
+ `);
14917
+ return;
14918
+ }
14919
+ const eventAge = Date.now() - new Date(event.timestamp).getTime();
14920
+ if (eventAge > EVENT_EXPIRY_MS) {
14921
+ process.stderr.write(`claudestory: channel event ${event.event} expired after ${Math.round(eventAge / 1e3)}s, quarantining: ${msg}
14922
+ `);
14923
+ eventRetryCount.delete(filename);
14924
+ await moveToFailed(inboxPath, processingFilename, filename);
14925
+ return;
14926
+ }
14927
+ const retries = (eventRetryCount.get(filename) ?? 0) + 1;
14928
+ eventRetryCount.set(filename, retries);
14929
+ if (retries >= MAX_EVENT_RETRIES) {
14930
+ process.stderr.write(`claudestory: channel event ${event.event} failed after ${retries} retries, quarantining: ${msg}
14931
+ `);
14932
+ eventRetryCount.delete(filename);
14933
+ await moveToFailed(inboxPath, processingFilename, filename);
14934
+ return;
14935
+ }
14936
+ try {
14937
+ await rename2(processingPath, filePath);
14938
+ } catch (renameErr) {
14939
+ const renameMsg = renameErr instanceof Error ? renameErr.message : String(renameErr);
14940
+ process.stderr.write(`claudestory: rename-back failed for ${filename}, quarantining: ${renameMsg}
14941
+ `);
14942
+ eventRetryCount.delete(filename);
14943
+ await moveToFailed(inboxPath, processingFilename, filename);
14944
+ return;
14945
+ }
14946
+ process.stderr.write(`claudestory: channel event ${event.event} failed (attempt ${retries}/${MAX_EVENT_RETRIES}), keeping for retry: ${msg}
14947
+ `);
14948
+ return;
14949
+ }
14950
+ try {
14951
+ await unlink3(processingPath);
14952
+ } catch {
14953
+ }
14954
+ }
14955
+ async function moveToFailed(inboxPath, sourceFilename, destFilename) {
14956
+ const failedDir = join25(inboxPath, FAILED_DIR);
14957
+ const targetName = destFilename ?? sourceFilename;
14958
+ try {
14959
+ await mkdir5(failedDir, { recursive: true });
14960
+ await rename2(join25(inboxPath, sourceFilename), join25(failedDir, targetName));
14961
+ } catch (err) {
14962
+ try {
14963
+ await unlink3(join25(inboxPath, sourceFilename));
14964
+ } catch {
14965
+ }
14966
+ const msg = err instanceof Error ? err.message : String(err);
14967
+ process.stderr.write(`claudestory: failed to move ${sourceFilename} to .failed/: ${msg}
14968
+ `);
14969
+ }
14970
+ }
14971
+ async function trimFailedDirectory(inboxPath) {
14972
+ const failedDir = join25(inboxPath, FAILED_DIR);
14973
+ let files;
14974
+ try {
14975
+ files = await readdir4(failedDir);
14976
+ } catch {
14977
+ return;
14978
+ }
14979
+ const sorted = files.filter((f) => f.endsWith(".json")).sort();
14980
+ if (sorted.length <= MAX_FAILED_FILES) return;
14981
+ const toDelete = sorted.slice(0, sorted.length - MAX_FAILED_FILES);
14982
+ for (const f of toDelete) {
14983
+ try {
14984
+ await unlink3(join25(failedDir, f));
14985
+ } catch {
14986
+ }
14987
+ }
14988
+ }
14989
+ var INBOX_DIR, FAILED_DIR, MAX_INBOX_DEPTH, MAX_FAILED_FILES, DEBOUNCE_MS, MAX_PERMISSION_RETRIES, MAX_EVENT_RETRIES, EVENT_EXPIRY_MS, watcher, permissionRetryCount, eventRetryCount, debounceTimer, pollInterval;
14990
+ var init_inbox_watcher = __esm({
14991
+ "src/channel/inbox-watcher.ts"() {
14992
+ "use strict";
14993
+ init_esm_shims();
14994
+ init_events();
14995
+ INBOX_DIR = "channel-inbox";
14996
+ FAILED_DIR = ".failed";
14997
+ MAX_INBOX_DEPTH = 50;
14998
+ MAX_FAILED_FILES = 20;
14999
+ DEBOUNCE_MS = 100;
15000
+ MAX_PERMISSION_RETRIES = 15;
15001
+ MAX_EVENT_RETRIES = 30;
15002
+ EVENT_EXPIRY_MS = 6e4;
15003
+ watcher = null;
15004
+ permissionRetryCount = /* @__PURE__ */ new Map();
15005
+ eventRetryCount = /* @__PURE__ */ new Map();
15006
+ debounceTimer = null;
15007
+ pollInterval = null;
15008
+ }
15009
+ });
15010
+
13963
15011
  // src/mcp/index.ts
13964
15012
  var mcp_exports = {};
13965
- import { realpathSync as realpathSync3, existsSync as existsSync13 } from "fs";
13966
- import { resolve as resolve9, join as join24, isAbsolute } from "path";
13967
- import { z as z11 } from "zod";
15013
+ import { realpathSync as realpathSync3, existsSync as existsSync14 } from "fs";
15014
+ import { resolve as resolve9, join as join26, isAbsolute } from "path";
15015
+ import { z as z12 } from "zod";
13968
15016
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
13969
15017
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
13970
15018
  function tryDiscoverRoot() {
@@ -13978,7 +15026,7 @@ function tryDiscoverRoot() {
13978
15026
  const resolved = resolve9(envRoot);
13979
15027
  try {
13980
15028
  const canonical = realpathSync3(resolved);
13981
- if (existsSync13(join24(canonical, CONFIG_PATH2))) {
15029
+ if (existsSync14(join26(canonical, CONFIG_PATH2))) {
13982
15030
  return canonical;
13983
15031
  }
13984
15032
  process.stderr.write(`Warning: No .story/config.json at ${canonical}
@@ -14006,9 +15054,9 @@ function registerDegradedTools(server) {
14006
15054
  const degradedInit = server.registerTool("claudestory_init", {
14007
15055
  description: "Initialize a new .story/ project in the current directory",
14008
15056
  inputSchema: {
14009
- name: z11.string().describe("Project name"),
14010
- type: z11.string().optional().describe("Project type (e.g. npm, macapp, cargo, generic)"),
14011
- language: z11.string().optional().describe("Primary language (e.g. typescript, swift, rust)")
15057
+ name: z12.string().describe("Project name"),
15058
+ type: z12.string().optional().describe("Project type (e.g. npm, macapp, cargo, generic)"),
15059
+ language: z12.string().optional().describe("Primary language (e.g. typescript, swift, rust)")
14012
15060
  }
14013
15061
  }, async (args) => {
14014
15062
  let result;
@@ -14038,6 +15086,12 @@ function registerDegradedTools(server) {
14038
15086
  return { content: [{ type: "text", text: `Initialized .story/ project "${args.name}" at ${result.root}
14039
15087
 
14040
15088
  Warning: tool registration failed. Restart the MCP server for full tool access.` }] };
15089
+ }
15090
+ try {
15091
+ await startInboxWatcher(result.root, server);
15092
+ } catch (watchErr) {
15093
+ process.stderr.write(`claudestory: inbox watcher failed after init: ${watchErr instanceof Error ? watchErr.message : String(watchErr)}
15094
+ `);
14041
15095
  }
14042
15096
  process.stderr.write(`claudestory: initialized at ${result.root}
14043
15097
  `);
@@ -14057,17 +15111,32 @@ async function main() {
14057
15111
  const server = new McpServer(
14058
15112
  { name: "claudestory", version },
14059
15113
  {
14060
- instructions: root ? "Start with claudestory_status for a project overview, then claudestory_ticket_next for the highest-priority work, then claudestory_handover_latest for session context." : "No .story/ project found. Use claudestory_init to initialize a new project, or navigate to a directory with .story/."
15114
+ instructions: root ? "Start with claudestory_status for a project overview, then claudestory_ticket_next for the highest-priority work, then claudestory_handover_latest for session context." : "No .story/ project found. Use claudestory_init to initialize a new project, or navigate to a directory with .story/.",
15115
+ capabilities: {
15116
+ experimental: {
15117
+ "claude/channel": {},
15118
+ "claude/channel/permission": {}
15119
+ }
15120
+ }
14061
15121
  }
14062
15122
  );
14063
15123
  if (root) {
14064
15124
  registerAllTools(server, root);
15125
+ await startInboxWatcher(root, server);
14065
15126
  process.stderr.write(`claudestory MCP server running (root: ${root})
14066
15127
  `);
14067
15128
  } else {
14068
15129
  registerDegradedTools(server);
14069
15130
  process.stderr.write("claudestory MCP server running (no project \u2014 claudestory_init available)\n");
14070
15131
  }
15132
+ process.on("SIGINT", () => {
15133
+ stopInboxWatcher();
15134
+ process.exit(0);
15135
+ });
15136
+ process.on("SIGTERM", () => {
15137
+ stopInboxWatcher();
15138
+ process.exit(0);
15139
+ });
14071
15140
  const transport = new StdioServerTransport();
14072
15141
  await server.connect(transport);
14073
15142
  }
@@ -14079,9 +15148,10 @@ var init_mcp = __esm({
14079
15148
  init_project_root_discovery();
14080
15149
  init_tools();
14081
15150
  init_init();
15151
+ init_inbox_watcher();
14082
15152
  ENV_VAR2 = "CLAUDESTORY_PROJECT_ROOT";
14083
15153
  CONFIG_PATH2 = ".story/config.json";
14084
- version = "0.1.61";
15154
+ version = "0.1.63";
14085
15155
  main().catch((err) => {
14086
15156
  process.stderr.write(`Fatal: ${err instanceof Error ? err.message : String(err)}
14087
15157
  `);
@@ -14117,7 +15187,7 @@ __export(run_exports, {
14117
15187
  runReadCommand: () => runReadCommand,
14118
15188
  writeOutput: () => writeOutput
14119
15189
  });
14120
- import { join as join25 } from "path";
15190
+ import { join as join27 } from "path";
14121
15191
  function writeOutput(text) {
14122
15192
  try {
14123
15193
  process.stdout.write(text + "\n");
@@ -14145,7 +15215,7 @@ async function runReadCommand(format, handler) {
14145
15215
  return;
14146
15216
  }
14147
15217
  const { state, warnings } = await loadProject(root);
14148
- const handoversDir = join25(root, ".story", "handovers");
15218
+ const handoversDir = join27(root, ".story", "handovers");
14149
15219
  const result = await handler({ state, warnings, root, handoversDir, format });
14150
15220
  writeOutput(result.output);
14151
15221
  let exitCode = result.exitCode ?? ExitCode.OK;
@@ -14180,7 +15250,7 @@ async function runDeleteCommand(format, force, handler) {
14180
15250
  return;
14181
15251
  }
14182
15252
  const { state, warnings } = await loadProject(root);
14183
- const handoversDir = join25(root, ".story", "handovers");
15253
+ const handoversDir = join27(root, ".story", "handovers");
14184
15254
  if (!force && hasIntegrityWarnings(warnings)) {
14185
15255
  writeOutput(
14186
15256
  formatError(
@@ -14696,9 +15766,9 @@ __export(setup_skill_exports, {
14696
15766
  removeHook: () => removeHook,
14697
15767
  resolveSkillSourceDir: () => resolveSkillSourceDir
14698
15768
  });
14699
- import { mkdir as mkdir5, writeFile as writeFile3, readFile as readFile5, readdir as readdir4, copyFile, rm, rename as rename2, unlink as unlink3 } from "fs/promises";
14700
- import { existsSync as existsSync14 } from "fs";
14701
- import { join as join26, dirname as dirname5 } from "path";
15769
+ import { mkdir as mkdir6, writeFile as writeFile3, readFile as readFile6, readdir as readdir5, copyFile, rm, rename as rename3, unlink as unlink4 } from "fs/promises";
15770
+ import { existsSync as existsSync15 } from "fs";
15771
+ import { join as join28, dirname as dirname5 } from "path";
14702
15772
  import { homedir } from "os";
14703
15773
  import { execFileSync as execFileSync2 } from "child_process";
14704
15774
  import { fileURLToPath as fileURLToPath4 } from "url";
@@ -14707,10 +15777,10 @@ function log(msg) {
14707
15777
  }
14708
15778
  function resolveSkillSourceDir() {
14709
15779
  const thisDir = dirname5(fileURLToPath4(import.meta.url));
14710
- const bundledPath = join26(thisDir, "..", "src", "skill");
14711
- if (existsSync14(join26(bundledPath, "SKILL.md"))) return bundledPath;
14712
- const sourcePath = join26(thisDir, "..", "..", "skill");
14713
- if (existsSync14(join26(sourcePath, "SKILL.md"))) return sourcePath;
15780
+ const bundledPath = join28(thisDir, "..", "src", "skill");
15781
+ if (existsSync15(join28(bundledPath, "SKILL.md"))) return bundledPath;
15782
+ const sourcePath = join28(thisDir, "..", "..", "skill");
15783
+ if (existsSync15(join28(sourcePath, "SKILL.md"))) return sourcePath;
14714
15784
  throw new Error(
14715
15785
  `Cannot find bundled skill files. Checked:
14716
15786
  ${bundledPath}
@@ -14720,31 +15790,31 @@ function resolveSkillSourceDir() {
14720
15790
  async function copyDirRecursive(srcDir, destDir) {
14721
15791
  const tmpDir = destDir + ".tmp";
14722
15792
  const bakDir = destDir + ".bak";
14723
- if (!existsSync14(destDir) && existsSync14(bakDir)) {
14724
- await rename2(bakDir, destDir);
15793
+ if (!existsSync15(destDir) && existsSync15(bakDir)) {
15794
+ await rename3(bakDir, destDir);
14725
15795
  }
14726
- if (existsSync14(tmpDir)) await rm(tmpDir, { recursive: true, force: true });
14727
- if (existsSync14(bakDir)) await rm(bakDir, { recursive: true, force: true });
14728
- await mkdir5(tmpDir, { recursive: true });
14729
- const entries = await readdir4(srcDir, { withFileTypes: true, recursive: true });
15796
+ if (existsSync15(tmpDir)) await rm(tmpDir, { recursive: true, force: true });
15797
+ if (existsSync15(bakDir)) await rm(bakDir, { recursive: true, force: true });
15798
+ await mkdir6(tmpDir, { recursive: true });
15799
+ const entries = await readdir5(srcDir, { withFileTypes: true, recursive: true });
14730
15800
  const written = [];
14731
15801
  for (const entry of entries) {
14732
15802
  if (!entry.isFile()) continue;
14733
15803
  const parent = entry.parentPath ?? entry.path ?? srcDir;
14734
- const relativePath = join26(parent, entry.name).slice(srcDir.length + 1);
14735
- const srcPath = join26(srcDir, relativePath);
14736
- const destPath = join26(tmpDir, relativePath);
14737
- await mkdir5(dirname5(destPath), { recursive: true });
15804
+ const relativePath = join28(parent, entry.name).slice(srcDir.length + 1);
15805
+ const srcPath = join28(srcDir, relativePath);
15806
+ const destPath = join28(tmpDir, relativePath);
15807
+ await mkdir6(dirname5(destPath), { recursive: true });
14738
15808
  await copyFile(srcPath, destPath);
14739
15809
  written.push(relativePath);
14740
15810
  }
14741
- if (existsSync14(destDir)) {
14742
- await rename2(destDir, bakDir);
15811
+ if (existsSync15(destDir)) {
15812
+ await rename3(destDir, bakDir);
14743
15813
  }
14744
15814
  try {
14745
- await rename2(tmpDir, destDir);
15815
+ await rename3(tmpDir, destDir);
14746
15816
  } catch (err) {
14747
- if (existsSync14(bakDir)) await rename2(bakDir, destDir).catch(() => {
15817
+ if (existsSync15(bakDir)) await rename3(bakDir, destDir).catch(() => {
14748
15818
  });
14749
15819
  await rm(tmpDir, { recursive: true, force: true }).catch(() => {
14750
15820
  });
@@ -14760,11 +15830,11 @@ function isHookWithCommand(entry, command) {
14760
15830
  return e.type === "command" && typeof e.command === "string" && e.command.trim() === command;
14761
15831
  }
14762
15832
  async function registerHook(hookType, hookEntry, settingsPath, matcher) {
14763
- const path2 = settingsPath ?? join26(homedir(), ".claude", "settings.json");
15833
+ const path2 = settingsPath ?? join28(homedir(), ".claude", "settings.json");
14764
15834
  let raw = "{}";
14765
- if (existsSync14(path2)) {
15835
+ if (existsSync15(path2)) {
14766
15836
  try {
14767
- raw = await readFile5(path2, "utf-8");
15837
+ raw = await readFile6(path2, "utf-8");
14768
15838
  } catch {
14769
15839
  process.stderr.write(`Could not read ${path2} \u2014 skipping hook registration.
14770
15840
  `);
@@ -14833,12 +15903,12 @@ async function registerHook(hookType, hookEntry, settingsPath, matcher) {
14833
15903
  const tmpPath = `${path2}.${process.pid}.tmp`;
14834
15904
  try {
14835
15905
  const dir = dirname5(path2);
14836
- await mkdir5(dir, { recursive: true });
15906
+ await mkdir6(dir, { recursive: true });
14837
15907
  await writeFile3(tmpPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
14838
- await rename2(tmpPath, path2);
15908
+ await rename3(tmpPath, path2);
14839
15909
  } catch (err) {
14840
15910
  try {
14841
- await unlink3(tmpPath);
15911
+ await unlink4(tmpPath);
14842
15912
  } catch {
14843
15913
  }
14844
15914
  const message = err instanceof Error ? err.message : String(err);
@@ -14858,11 +15928,11 @@ async function registerStopHook(settingsPath) {
14858
15928
  return registerHook("Stop", { type: "command", command: STOP_HOOK_COMMAND, async: true }, settingsPath);
14859
15929
  }
14860
15930
  async function removeHook(hookType, command, settingsPath) {
14861
- const path2 = settingsPath ?? join26(homedir(), ".claude", "settings.json");
15931
+ const path2 = settingsPath ?? join28(homedir(), ".claude", "settings.json");
14862
15932
  let raw = "{}";
14863
- if (existsSync14(path2)) {
15933
+ if (existsSync15(path2)) {
14864
15934
  try {
14865
- raw = await readFile5(path2, "utf-8");
15935
+ raw = await readFile6(path2, "utf-8");
14866
15936
  } catch {
14867
15937
  return "skipped";
14868
15938
  }
@@ -14891,12 +15961,12 @@ async function removeHook(hookType, command, settingsPath) {
14891
15961
  const tmpPath = `${path2}.${process.pid}.tmp`;
14892
15962
  try {
14893
15963
  const dir = dirname5(path2);
14894
- await mkdir5(dir, { recursive: true });
15964
+ await mkdir6(dir, { recursive: true });
14895
15965
  await writeFile3(tmpPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
14896
- await rename2(tmpPath, path2);
15966
+ await rename3(tmpPath, path2);
14897
15967
  } catch {
14898
15968
  try {
14899
- await unlink3(tmpPath);
15969
+ await unlink4(tmpPath);
14900
15970
  } catch {
14901
15971
  }
14902
15972
  return "skipped";
@@ -14905,8 +15975,8 @@ async function removeHook(hookType, command, settingsPath) {
14905
15975
  }
14906
15976
  async function handleSetupSkill(options = {}) {
14907
15977
  const { skipHooks = false } = options;
14908
- const skillDir = join26(homedir(), ".claude", "skills", "story");
14909
- await mkdir5(skillDir, { recursive: true });
15978
+ const skillDir = join28(homedir(), ".claude", "skills", "story");
15979
+ await mkdir6(skillDir, { recursive: true });
14910
15980
  let srcSkillDir;
14911
15981
  try {
14912
15982
  srcSkillDir = resolveSkillSourceDir();
@@ -14918,31 +15988,31 @@ async function handleSetupSkill(options = {}) {
14918
15988
  process.exitCode = 1;
14919
15989
  return;
14920
15990
  }
14921
- const oldPrimeDir = join26(homedir(), ".claude", "skills", "prime");
14922
- if (existsSync14(oldPrimeDir)) {
15991
+ const oldPrimeDir = join28(homedir(), ".claude", "skills", "prime");
15992
+ if (existsSync15(oldPrimeDir)) {
14923
15993
  await rm(oldPrimeDir, { recursive: true, force: true });
14924
15994
  log("Removed old /prime skill (migrated to /story)");
14925
15995
  }
14926
- const existed = existsSync14(join26(skillDir, "SKILL.md"));
14927
- const skillContent = await readFile5(join26(srcSkillDir, "SKILL.md"), "utf-8");
14928
- await writeFile3(join26(skillDir, "SKILL.md"), skillContent, "utf-8");
15996
+ const existed = existsSync15(join28(skillDir, "SKILL.md"));
15997
+ const skillContent = await readFile6(join28(srcSkillDir, "SKILL.md"), "utf-8");
15998
+ await writeFile3(join28(skillDir, "SKILL.md"), skillContent, "utf-8");
14929
15999
  const supportFiles = ["setup-flow.md", "autonomous-mode.md", "reference.md"];
14930
16000
  const writtenFiles = ["SKILL.md"];
14931
16001
  const missingFiles = [];
14932
16002
  for (const filename of supportFiles) {
14933
- const srcPath = join26(srcSkillDir, filename);
14934
- if (existsSync14(srcPath)) {
14935
- const content = await readFile5(srcPath, "utf-8");
14936
- await writeFile3(join26(skillDir, filename), content, "utf-8");
16003
+ const srcPath = join28(srcSkillDir, filename);
16004
+ if (existsSync15(srcPath)) {
16005
+ const content = await readFile6(srcPath, "utf-8");
16006
+ await writeFile3(join28(skillDir, filename), content, "utf-8");
14937
16007
  writtenFiles.push(filename);
14938
16008
  } else {
14939
16009
  missingFiles.push(filename);
14940
16010
  }
14941
16011
  }
14942
16012
  for (const subdir of ["design", "review-lenses"]) {
14943
- const srcDir = join26(srcSkillDir, subdir);
14944
- if (existsSync14(srcDir)) {
14945
- const destDir = join26(skillDir, subdir);
16013
+ const srcDir = join28(srcSkillDir, subdir);
16014
+ if (existsSync15(srcDir)) {
16015
+ const destDir = join28(skillDir, subdir);
14946
16016
  try {
14947
16017
  const files = await copyDirRecursive(srcDir, destDir);
14948
16018
  for (const f of files) writtenFiles.push(`${subdir}/${f}`);
@@ -15066,8 +16136,8 @@ var hook_status_exports = {};
15066
16136
  __export(hook_status_exports, {
15067
16137
  handleHookStatus: () => handleHookStatus
15068
16138
  });
15069
- import { readFileSync as readFileSync12, writeFileSync as writeFileSync6, renameSync as renameSync3, unlinkSync as unlinkSync4 } from "fs";
15070
- import { join as join27 } from "path";
16139
+ import { readFileSync as readFileSync13, writeFileSync as writeFileSync8, renameSync as renameSync3, unlinkSync as unlinkSync5 } from "fs";
16140
+ import { join as join29 } from "path";
15071
16141
  async function readStdinSilent() {
15072
16142
  try {
15073
16143
  const chunks = [];
@@ -15084,12 +16154,12 @@ async function readStdinSilent() {
15084
16154
  function atomicWriteSync(targetPath, content) {
15085
16155
  const tmp = `${targetPath}.${process.pid}.tmp`;
15086
16156
  try {
15087
- writeFileSync6(tmp, content, "utf-8");
16157
+ writeFileSync8(tmp, content, "utf-8");
15088
16158
  renameSync3(tmp, targetPath);
15089
16159
  return true;
15090
16160
  } catch {
15091
16161
  try {
15092
- unlinkSync4(tmp);
16162
+ unlinkSync5(tmp);
15093
16163
  } catch {
15094
16164
  }
15095
16165
  return false;
@@ -15117,10 +16187,10 @@ function activePayload(session) {
15117
16187
  };
15118
16188
  }
15119
16189
  function ensureGitignore(root) {
15120
- const gitignorePath = join27(root, ".story", ".gitignore");
16190
+ const gitignorePath = join29(root, ".story", ".gitignore");
15121
16191
  let existing = "";
15122
16192
  try {
15123
- existing = readFileSync12(gitignorePath, "utf-8");
16193
+ existing = readFileSync13(gitignorePath, "utf-8");
15124
16194
  } catch {
15125
16195
  }
15126
16196
  const lines = existing.split("\n").map((l) => l.trim());
@@ -15130,13 +16200,13 @@ function ensureGitignore(root) {
15130
16200
  if (content.length > 0 && !content.endsWith("\n")) content += "\n";
15131
16201
  content += missing.join("\n") + "\n";
15132
16202
  try {
15133
- writeFileSync6(gitignorePath, content, "utf-8");
16203
+ writeFileSync8(gitignorePath, content, "utf-8");
15134
16204
  } catch {
15135
16205
  }
15136
16206
  }
15137
16207
  function writeStatus(root, payload) {
15138
16208
  ensureGitignore(root);
15139
- const statusPath = join27(root, ".story", "status.json");
16209
+ const statusPath = join29(root, ".story", "status.json");
15140
16210
  const content = JSON.stringify(payload, null, 2) + "\n";
15141
16211
  atomicWriteSync(statusPath, content);
15142
16212
  }
@@ -15195,8 +16265,8 @@ var config_update_exports = {};
15195
16265
  __export(config_update_exports, {
15196
16266
  handleConfigSetOverrides: () => handleConfigSetOverrides
15197
16267
  });
15198
- import { readFileSync as readFileSync13 } from "fs";
15199
- import { join as join28 } from "path";
16268
+ import { readFileSync as readFileSync14 } from "fs";
16269
+ import { join as join30 } from "path";
15200
16270
  async function handleConfigSetOverrides(root, format, options) {
15201
16271
  const { json: jsonArg, clear } = options;
15202
16272
  if (!clear && !jsonArg) {
@@ -15224,8 +16294,8 @@ async function handleConfigSetOverrides(root, format, options) {
15224
16294
  }
15225
16295
  let resultOverrides = null;
15226
16296
  await withProjectLock(root, { strict: false }, async () => {
15227
- const configPath = join28(root, ".story", "config.json");
15228
- const rawContent = readFileSync13(configPath, "utf-8");
16297
+ const configPath = join30(root, ".story", "config.json");
16298
+ const rawContent = readFileSync14(configPath, "utf-8");
15229
16299
  const raw = JSON.parse(rawContent);
15230
16300
  if (clear) {
15231
16301
  delete raw.recipeOverrides;
@@ -15296,6 +16366,12 @@ async function handleSessionCompactPrepare() {
15296
16366
  `);
15297
16367
  return;
15298
16368
  }
16369
+ writeResumeMarker(root, active.state.sessionId, {
16370
+ ticket: active.state.ticket,
16371
+ completedTickets: active.state.completedTickets,
16372
+ resolvedIssues: active.state.resolvedIssues,
16373
+ preCompactState: active.state.preCompactState ?? active.state.state
16374
+ });
15299
16375
  try {
15300
16376
  const loadResult = await loadProject(root);
15301
16377
  const { saveSnapshot: saveSnapshot2 } = await Promise.resolve().then(() => (init_snapshot(), snapshot_exports));
@@ -15312,7 +16388,10 @@ async function handleSessionResumePrompt() {
15312
16388
  const root = discoverProjectRoot();
15313
16389
  if (!root) return;
15314
16390
  const match = findResumableSession(root);
15315
- if (!match) return;
16391
+ if (!match) {
16392
+ removeResumeMarker(root);
16393
+ return;
16394
+ }
15316
16395
  const { info, stale } = match;
15317
16396
  const sessionId = info.state.sessionId;
15318
16397
  if (stale) {
@@ -15387,6 +16466,7 @@ claudestory_autonomous_guide {"sessionId": "${info.state.sessionId}", "action":
15387
16466
  ticketId: info.state.ticket?.id ?? null
15388
16467
  }
15389
16468
  });
16469
+ removeResumeMarker(root);
15390
16470
  return `Session ${info.state.sessionId} ended (unrecoverable \u2014 invalid preCompactState: ${preCompactState ?? "null"}). Run "start" for a new session.`;
15391
16471
  });
15392
16472
  }
@@ -15442,6 +16522,7 @@ async function handleSessionStop(root, sessionId) {
15442
16522
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
15443
16523
  data: { previousState: info.state.state, ticketId: ticketId ?? null, ticketReleased }
15444
16524
  });
16525
+ removeResumeMarker(root);
15445
16526
  return `Session ${info.state.sessionId} stopped.${ticketReleased ? ` Ticket ${ticketId} released to open.` : ticketId ? ` Ticket ${ticketId} may need manual cleanup.` : ""}`;
15446
16527
  });
15447
16528
  }
@@ -15453,6 +16534,7 @@ var init_session_compact = __esm({
15453
16534
  init_session();
15454
16535
  init_session_types();
15455
16536
  init_project_loader();
16537
+ init_resume_marker();
15456
16538
  }
15457
16539
  });
15458
16540
 
@@ -17568,7 +18650,7 @@ async function runCli() {
17568
18650
  registerSessionCommand: registerSessionCommand2,
17569
18651
  registerRepairCommand: registerRepairCommand2
17570
18652
  } = await Promise.resolve().then(() => (init_register(), register_exports));
17571
- const version2 = "0.1.61";
18653
+ const version2 = "0.1.63";
17572
18654
  class HandledError extends Error {
17573
18655
  constructor() {
17574
18656
  super("HANDLED_ERROR");